/*
 * Copyright 2015 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.cloud.datastore;

import com.google.protobuf.ByteString;
import java.util.Iterator;
import java.util.List;

/**
 * A Google cloud datastore transaction. Similar to {@link Batch} any write operation that is
 * applied on a transaction will only be sent to the Datastore upon {@link #commit}. A call to
 * {@link #rollback} will invalidate the transaction and discard the changes. Any read operation
 * that is done by a transaction will be part of it and therefore a {@code commit} is guaranteed to
 * fail if an entity was modified outside of the transaction after it was read. Write operation on
 * this transaction will not be reflected by read operation (as the changes are only sent to the
 * Datastore upon {@code commit}. A usage example:
 *
 * <pre>{@code
 * Transaction transaction = datastore.newTransaction();
 * try {
 *   Entity entity = transaction.get(key);
 *   if (!entity.contains("last_name") || entity.isNull("last_name")) {
 *     String[] name = entity.getString("name").split(" ");
 *     entity = Entity.newBuilder(entity)
 *         .remove("name")
 *         .set("first_name", name[0])
 *         .set("last_name", name[1])
 *         .build();
 *     transaction.update(entity);
 *     transaction.commit();
 *   }
 * } finally {
 *   if (transaction.isActive()) {
 *     transaction.rollback();
 *   }
 * }
 * }</pre>
 *
 * @see <a href="https://cloud.google.com/datastore/docs/concepts/transactions">Google Cloud
 *     Datastore transactions</a>
 */
public interface Transaction extends DatastoreBatchWriter, DatastoreReaderWriter {

  interface Response {

    /** Returns a list of keys generated by a transaction. */
    List<Key> getGeneratedKeys();
  }

  /**
   * {@inheritDoc} The requested entity will be part of this Datastore transaction (so a commit is
   * guaranteed to fail if entity was changed by others after it was seen by this transaction) but
   * any write changes in this transaction will not be reflected by the returned entity.
   *
   * <p>Example of getting an entity for a given key.
   *
   * <pre>{@code
   * String keyName = "my_key_name";
   * Key key = datastore.newKeyFactory().setKind("MyKind").newKey(keyName);
   * Entity entity = transaction.get(key);
   * transaction.commit();
   * // Do something with the entity
   * }</pre>
   *
   * @throws DatastoreException upon failure or if no longer active
   */
  @Override
  Entity get(Key key);

  /**
   * {@inheritDoc} The requested entities will be part of this Datastore transaction (so a commit is
   * guaranteed to fail if any of the entities was changed by others after they were seen by this
   * transaction) but any write changes in this transaction will not be reflected by the returned
   * entities.
   *
   * <p>Example of getting entities for several keys.
   *
   * <pre>{@code
   * String firstKeyName = "my_first_key_name";
   * String secondKeyName = "my_second_key_name";
   * KeyFactory keyFactory = datastore.newKeyFactory().setKind("MyKind");
   * Key firstKey = keyFactory.newKey(firstKeyName);
   * Key secondKey = keyFactory.newKey(secondKeyName);
   * Iterator<Entity> entitiesIterator = transaction.get(firstKey, secondKey);
   * List<Entity> entities = Lists.newArrayList();
   * while (entitiesIterator.hasNext()) {
   *   Entity entity = entitiesIterator.next();
   *   // do something with the entity
   *   entities.add(entity);
   * }
   * transaction.commit();
   * }</pre>
   *
   * @throws DatastoreException upon failure or if no longer active
   */
  @Override
  Iterator<Entity> get(Key... key);

  /**
   * {@inheritDoc} The requested entities will be part of this Datastore transaction (so a commit is
   * guaranteed to fail if any of the entities was changed by others after they were seen by this
   * transaction) but any write changes in this transaction will not be reflected by the returned
   * entities.
   *
   * <p>Example of fetching a list of entities for several keys.
   *
   * <pre>{@code
   * String firstKeyName = "my_first_key_name";
   * String secondKeyName = "my_second_key_name";
   * KeyFactory keyFactory = datastore.newKeyFactory().setKind("MyKind");
   * Key firstKey = keyFactory.newKey(firstKeyName);
   * Key secondKey = keyFactory.newKey(secondKeyName);
   * List<Entity> entities = transaction.fetch(firstKey, secondKey);
   * for (Entity entity : entities) {
   *   // do something with the entity
   * }
   * transaction.commit();
   * }</pre>
   *
   * @throws DatastoreException upon failure or if no longer active
   */
  @Override
  List<Entity> fetch(Key... keys);

  /**
   * {@inheritDoc} The entities returned by the result of this query will be part of this Datastore
   * transaction (so a commit is guaranteed to fail if any of the entities was changed by others
   * after the query was performed) but any write changes in this transaction will not be reflected
   * by the result.
   *
   * <p>Example of running a query to find all entities with an ancestor.
   *
   * <pre>{@code
   * String parentKeyName = "my_parent_key_name";
   * KeyFactory keyFactory = datastore.newKeyFactory().setKind("ParentKind");
   * Key parentKey = keyFactory.newKey(parentKeyName);
   * // Build a query
   * Query<Entity> query = Query.newEntityQueryBuilder()
   *     .setKind("MyKind")
   *     .setFilter(PropertyFilter.hasAncestor(parentKey))
   *     .build();
   * QueryResults<Entity> results = transaction.run(query);
   * List<Entity> entities = Lists.newArrayList();
   * while (results.hasNext()) {
   *   Entity result = results.next();
   *   // do something with result
   *   entities.add(result);
   * }
   * transaction.commit();
   * }</pre>
   *
   * @throws DatastoreException upon failure or if no longer active
   */
  @Override
  <T> QueryResults<T> run(Query<T> query);

  /**
   * Datastore add operation. This method will also allocate id for any entity with an incomplete
   * key. As opposed to {@link #add(FullEntity)} and {@link #add(FullEntity...)}, this method will
   * defer any necessary id allocation to commit time.
   *
   * <p>Example of adding multiple entities with deferred id allocation.
   *
   * <pre>{@code
   * IncompleteKey key1 = datastore.newKeyFactory().setKind("MyKind").newKey();
   * FullEntity.Builder entityBuilder1 = FullEntity.newBuilder(key1);
   * entityBuilder1.set("propertyName", "value1");
   * FullEntity entity1 = entityBuilder1.build();
   *
   * IncompleteKey key2 = datastore.newKeyFactory().setKind("MyKind").newKey();
   * FullEntity.Builder entityBuilder2 = FullEntity.newBuilder(key2);
   * entityBuilder2.set("propertyName", "value2");
   * FullEntity entity2 = entityBuilder2.build();
   *
   * transaction.addWithDeferredIdAllocation(entity1, entity2);
   * Response response = transaction.commit();
   * }</pre>
   *
   * @throws DatastoreException if a given entity with a complete key was already added to this
   *     transaction or if the transaction is no longer active
   */
  void addWithDeferredIdAllocation(FullEntity<?>... entities);

  /**
   * {@inheritDoc}
   *
   * <p>Example of adding a single entity.
   *
   * <pre>{@code
   * String keyName = "my_key_name";
   * Key key = datastore.newKeyFactory().setKind("MyKind").newKey(keyName);
   * Entity.Builder entityBuilder = Entity.newBuilder(key);
   * entityBuilder.set("propertyName", "value");
   * Entity entity = entityBuilder.build();
   * transaction.add(entity);
   * transaction.commit();
   * }</pre>
   *
   * @throws DatastoreException if a given entity with the same complete key was already added to
   *     this writer, if the transaction is no longer active or if id allocation for an entity with
   *     an incomplete key failed
   */
  @Override
  Entity add(FullEntity<?> entity);

  /**
   * {@inheritDoc}
   *
   * <p>Example of adding multiple entities.
   *
   * <pre>{@code
   * String keyName1 = "my_key_name1";
   * String keyName2 = "my_key_name2";
   * Key key1 = datastore.newKeyFactory().setKind("MyKind").newKey(keyName1);
   * Entity.Builder entityBuilder1 = Entity.newBuilder(key1);
   * entityBuilder1.set("propertyName", "value1");
   * Entity entity1 = entityBuilder1.build();
   *
   * Key key2 = datastore.newKeyFactory().setKind("MyKind").newKey(keyName2);
   * Entity.Builder entityBuilder2 = Entity.newBuilder(key2);
   * entityBuilder2.set("propertyName", "value2");
   * Entity entity2 = entityBuilder2.build();
   *
   * transaction.add(entity1, entity2);
   * transaction.commit();
   * }</pre>
   *
   * @throws DatastoreException if a given entity with the same complete key was already added to
   *     this writer, if the transaction is no longer active or if id allocation for an entity with
   *     an incomplete key failed
   */
  @Override
  List<Entity> add(FullEntity<?>... entities);

  /**
   * {@inheritDoc} This operation will be converted to {@link #put} operation for entities that were
   * already added or put in this writer.
   *
   * <p>Example of updating multiple entities.
   *
   * <pre>{@code
   * String keyName1 = "my_key_name1";
   * String keyName2 = "my_key_name2";
   * Key key1 = datastore.newKeyFactory().setKind("MyKind").newKey(keyName1);
   * Entity.Builder entityBuilder1 = Entity.newBuilder(key1);
   * entityBuilder1.set("propertyName", "value3");
   * Entity entity1 = entityBuilder1.build();
   *
   * Key key2 = datastore.newKeyFactory().setKind("MyKind").newKey(keyName2);
   * Entity.Builder entityBuilder2 = Entity.newBuilder(key2);
   * entityBuilder2.set("propertyName", "value4");
   * Entity entity2 = entityBuilder2.build();
   *
   * transaction.update(entity1, entity2);
   * transaction.commit();
   * }</pre>
   *
   * @throws DatastoreException if an entity is marked for deletion in this transaction or if the
   *     transaction is no longer active
   */
  @Override
  void update(Entity... entities);

  /**
   * {@inheritDoc} This operation will also remove from this transaction any prior writes for
   * entities with the same keys.
   *
   * <p>Example of deleting multiple entities.
   *
   * <pre>{@code
   * String keyName1 = "my_key_name1";
   * String keyName2 = "my_key_name2";
   * Key key1 = datastore.newKeyFactory().setKind("MyKind").newKey(keyName1);
   * Key key2 = datastore.newKeyFactory().setKind("MyKind").newKey(keyName2);
   * transaction.delete(key1, key2);
   * transaction.commit();
   * }</pre>
   *
   * @throws DatastoreException upon failure or if no longer active
   */
  @Override
  void delete(Key... keys);

  /**
   * Datastore put operation. This method will also allocate id for any entity with an incomplete
   * key. As opposed to {@link #put(FullEntity)} and {@link #put(FullEntity...)}, this method will
   * defer any necessary id allocation to commit time.
   *
   * <p>Example of putting multiple entities with deferred id allocation.
   *
   * <pre>{@code
   * IncompleteKey key1 = datastore.newKeyFactory().setKind("MyKind").newKey();
   * FullEntity.Builder entityBuilder1 = FullEntity.newBuilder(key1);
   * entityBuilder1.set("propertyName", "value1");
   * FullEntity entity1 = entityBuilder1.build();
   *
   * IncompleteKey key2 = datastore.newKeyFactory().setKind("MyKind").newKey();
   * FullEntity.Builder entityBuilder2 = FullEntity.newBuilder(key2);
   * entityBuilder2.set("propertyName", "value2");
   * FullEntity entity2 = entityBuilder2.build();
   *
   * transaction.putWithDeferredIdAllocation(entity1, entity2);
   * Response response = transaction.commit();
   * }</pre>
   *
   * @throws IllegalArgumentException if any of the given entities is missing a key
   * @throws DatastoreException if no longer active
   */
  void putWithDeferredIdAllocation(FullEntity<?>... entities);

  /**
   * {@inheritDoc} This operation will also remove from this transaction any prior writes for the
   * same entity.
   *
   * <p>Example of putting a single entity.
   *
   * <pre>{@code
   * String keyName = "my_key_name";
   * Key key = datastore.newKeyFactory().setKind("MyKind").newKey(keyName);
   * Entity.Builder entityBuilder = Entity.newBuilder(key);
   * entityBuilder.set("propertyName", "value");
   * Entity entity = entityBuilder.build();
   * transaction.put(entity);
   * transaction.commit();
   * }</pre>
   *
   * @throws DatastoreException if id allocation for an entity with an incomplete key failed or if
   *     the transaction is no longer active
   */
  @Override
  Entity put(FullEntity<?> entity);

  /**
   * {@inheritDoc} This operation will also remove from this transaction any prior writes for the
   * same entities.
   *
   * <p>Example of putting multiple entities.
   *
   * <pre>{@code
   * String keyName1 = "my_key_name1";
   * String keyName2 = "my_key_name2";
   * Key key1 = datastore.newKeyFactory().setKind("MyKind").newKey(keyName1);
   * Entity.Builder entityBuilder1 = Entity.newBuilder(key1);
   * entityBuilder1.set("propertyName", "value1");
   * Entity entity1 = entityBuilder1.build();
   *
   * Key key2 = datastore.newKeyFactory().setKind("MyKind").newKey(keyName2);
   * Entity.Builder entityBuilder2 = Entity.newBuilder(key2);
   * entityBuilder2.set("propertyName", "value2");
   * Entity entity2 = entityBuilder2.build();
   *
   * transaction.put(entity1, entity2);
   * transaction.commit();
   * }</pre>
   *
   * @throws DatastoreException if id allocation for an entity with an incomplete key failed or if
   *     the transaction is no longer active
   */
  @Override
  List<Entity> put(FullEntity<?>... entities);

  /**
   * Commit the transaction.
   *
   * <p>Example of committing a transaction.
   *
   * <pre>{@code
   * // create an entity
   * KeyFactory keyFactory = datastore.newKeyFactory().setKind("MyKind");
   * Key key = datastore.allocateId(keyFactory.newKey());
   * Entity entity = Entity.newBuilder(key).set("description", "commit()").build();
   *
   * // add the entity and commit
   * try {
   *   transaction.put(entity);
   *   transaction.commit();
   * } catch (DatastoreException ex) {
   *   // handle exception
   * }
   * }</pre>
   *
   * @throws DatastoreException if could not commit the transaction or if no longer active
   */
  Response commit();

  /**
   * Rollback the transaction.
   *
   * <p>Example of rolling back a transaction.
   *
   * <pre>{@code
   * // create an entity
   * KeyFactory keyFactory = datastore.newKeyFactory().setKind("MyKind");
   * Key key = datastore.allocateId(keyFactory.newKey());
   * Entity entity = Entity.newBuilder(key).set("description", "rollback()").build();
   *
   * // add the entity and rollback
   * transaction.put(entity);
   * transaction.rollback();
   * // calling transaction.commit() now would fail
   * }</pre>
   *
   * @throws DatastoreException if transaction was already committed
   */
  void rollback();

  /**
   * Returns {@code true} if the transaction is still active (was not committed or rolledback).
   *
   * <p>Example of verifying if a transaction is active.
   *
   * <pre>{@code
   * // create an entity
   * KeyFactory keyFactory = datastore.newKeyFactory().setKind("MyKind");
   * Key key = datastore.allocateId(keyFactory.newKey());
   * Entity entity = Entity.newBuilder(key).set("description", "active()").build();
   * // calling transaction.active() now would return true
   * try {
   *   // add the entity and commit
   *   transaction.put(entity);
   *   transaction.commit();
   * } finally {
   *   // if committing succeeded
   *   // then transaction.active() will be false
   *   if (transaction.isActive()) {
   *     // otherwise it's true and we need to rollback
   *     transaction.rollback();
   *   }
   * }
   * }</pre>
   */
  @Override
  boolean isActive();

  /** Returns the transaction associated {@link Datastore}. */
  Datastore getDatastore();

  ByteString getTransactionId();
}
