/* * Copyright 2004-2010 the Seasar Foundation and the Others. * * 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 org.slim3.datastore; import java.util.ArrayList; import java.util.Collection; import java.util.ConcurrentModificationException; import java.util.List; import java.util.Map; import com.google.appengine.api.datastore.AsyncDatastoreService; import com.google.appengine.api.datastore.Entity; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.KeyFactory; import com.google.appengine.api.datastore.Transaction; import com.google.appengine.api.datastore.Query.FilterOperator; /** * A class to lock a target entity. * * @author higa * @since 1.0.0 * */ public class Lock { /** * The kind of lock entity. */ public static final String KIND = "slim3.Lock"; /** * The globalTransactionKey property name. */ public static final String GLOBAL_TRANSACTION_KEY_PROPERTY = "globalTransactionKey"; /** * The timestamp property name. */ public static final String TIMESTAMP_PROPERTY = "timestampType"; /** * The timeout. */ protected static final long TIMEOUT = 30 * 1000; /** * The asynchronous datastore service. */ protected AsyncDatastoreService ds; /** * The key. */ protected Key key; /** * The root key. */ protected Key rootKey; /** * The time-stamp. */ protected long timestamp; /** * The global transaction key. */ protected Key globalTransactionKey; /** * Creates a key for lock. * * @param rootKey * the root key * @return a key * @throws NullPointerException * if the targetKey parameter is null */ public static Key createKey(Key rootKey) throws NullPointerException { if (rootKey == null) { throw new NullPointerException("The target key must not be null."); } if (rootKey.getParent() != null) { throw new IllegalArgumentException("The key(" + rootKey + ") must be a root."); } return KeyFactory.createKey(rootKey, KIND, 1); } /** * Converts the entity to a {@link Lock}. * * @param ds * the asynchronous datastore service * @param entity * an entity * * @return a {@link Lock} * @throws NullPointerException * if the ds parameter is null or if the entity property is null * @throws IllegalArgumentException * if the kind of the entity is not slim3.Lock */ public static Lock toLock(AsyncDatastoreService ds, Entity entity) throws NullPointerException, IllegalArgumentException { if (entity == null) { throw new NullPointerException( "The entity parameter must not be null."); } if (!KIND.equals(entity.getKind())) { throw new IllegalArgumentException("The kind(" + entity.getKind() + ") of the entity(" + entity.getKey() + ") must be " + KIND + "."); } Key globalTransactionKey = (Key) entity.getProperty(GLOBAL_TRANSACTION_KEY_PROPERTY); Long timestamp = (Long) entity.getProperty(TIMESTAMP_PROPERTY); return new Lock( ds, globalTransactionKey, entity.getKey().getParent(), timestamp); } /** * Returns a {@link Lock} specified by the key. Returns null if no entity is * found. * * @param ds * the asynchronous datastore service * @param tx * the transaction * @param key * the key * * @return a {@link Lock} specified by the key * @throws NullPointerException * if the ds parameter is null */ public static Lock getOrNull(AsyncDatastoreService ds, Transaction tx, Key key) throws NullPointerException { Entity entity = DatastoreUtil.getOrNull(ds, tx, key); if (entity == null) { return null; } return toLock(ds, entity); } /** * Returns keys specified by the global transaction key. * * @param ds * the asynchronous datastore service * @param globalTransactionKey * the global transaction key * @return a list of keys * @throws NullPointerException * if the ds parameter is null or if the globalTransactionKey * parameter is null */ public static List<Key> getKeys(AsyncDatastoreService ds, Key globalTransactionKey) throws NullPointerException { if (ds == null) { throw new NullPointerException("The ds parameter must not be null."); } if (globalTransactionKey == null) { throw new NullPointerException( "The globalTransactionKey parameter must not be null."); } return new EntityQuery(ds, KIND).filter( GLOBAL_TRANSACTION_KEY_PROPERTY, FilterOperator.EQUAL, globalTransactionKey).asKeyList(); } /** * Deletes entities specified by the global transaction key in transaction. * * @param ds * the asynchronous datastore service * @param globalTransactionKey * the global transaction key * @throws NullPointerException * if the ds parameter is null or if the globalTransactionKey * parameter is null * */ public static void deleteInTx(AsyncDatastoreService ds, Key globalTransactionKey) throws NullPointerException { if (ds == null) { throw new NullPointerException("The ds parameter must not be null."); } if (globalTransactionKey == null) { throw new NullPointerException( "The globalTransactionKey parameter must not be null."); } for (Key key : getKeys(ds, globalTransactionKey)) { deleteInTx(ds, globalTransactionKey, key); } } /** * Deletes the locks from the datastore in transaction. * * @param ds * the asynchronous datastore service * @param globalTransactionKey * the global transaction key * @param locks * the locks * @throws NullPointerException * if the ds parameter is null or if the globalTransactionKey * parameter is null or if the locks parameter is null * */ public static void deleteInTx(AsyncDatastoreService ds, Key globalTransactionKey, Iterable<Lock> locks) throws NullPointerException { if (ds == null) { throw new NullPointerException("The ds parameter must not be null."); } if (globalTransactionKey == null) { throw new NullPointerException( "The globalTransactionKey parameter must not be null."); } if (locks == null) { throw new NullPointerException( "The locks parameter must not be null."); } for (Lock lock : locks) { deleteInTx(ds, globalTransactionKey, lock.key); } } /** * Deletes an entity specified by the key in transaction. * * @param ds * the asynchronous datastore service * @param globalTransactionKey * the global transaction key * @param key * the key * * @throws NullPointerException * if the ds parameter is null or if the globalTransactionKey * parameter is null or if the key parameter is null */ protected static void deleteInTx(AsyncDatastoreService ds, Key globalTransactionKey, Key key) throws NullPointerException { if (ds == null) { throw new NullPointerException("The ds parameter must not be null."); } if (globalTransactionKey == null) { throw new NullPointerException( "The globalTransactionKey parameter must not be null."); } if (key == null) { throw new NullPointerException( "The key parameter must not be null."); } Transaction tx = DatastoreUtil.beginTransaction(ds); try { Lock lock = getOrNull(ds, tx, key); if (lock != null && globalTransactionKey.equals(lock.globalTransactionKey)) { DatastoreUtil.delete(ds, tx, key); tx.commit(); } } finally { if (tx.isActive()) { tx.rollback(); } } } /** * Deletes entities specified by the global transaction key without * transaction. * * @param ds * the asynchronous datastore service * @param globalTransactionKey * the global transaction key * @throws NullPointerException * if the ds parameter is null or if the globalTransactionKey * parameter is null */ public static void deleteWithoutTx(AsyncDatastoreService ds, Key globalTransactionKey) throws NullPointerException { DatastoreUtil.delete(ds, null, getKeys(ds, globalTransactionKey)); } /** * Deletes the locks without transaction. * * @param ds * the asynchronous datastore service * @param locks * the locks * @throws NullPointerException * if the ds parameter is null or if the locks parameter is null */ public static void deleteWithoutTx(AsyncDatastoreService ds, Iterable<Lock> locks) throws NullPointerException { if (ds == null) { throw new NullPointerException("The ds parameter must not be null."); } if (locks == null) { throw new NullPointerException( "The locks parameter must not be null."); } List<Key> keys = new ArrayList<Key>(); for (Lock lock : locks) { keys.add(lock.key); } if (keys.isEmpty()) { return; } DatastoreUtil.delete(ds, null, keys); } /** * Verifies lock specified by the root key and returns entities specified by * the keys as map. * * @param ds * the asynchronous datastore service * @param tx * the transaction * @param rootKey * the root key * @param keys * the keys * @return an entity * @throws NullPointerException * if the ds parameter is null or if the tx parameter is null or * if the rootKey parameter is null or if the keys parameter is * null * * @throws ConcurrentModificationException * if locking the entity group failed */ public static Map<Key, Entity> verifyAndGetAsMap(AsyncDatastoreService ds, Transaction tx, Key rootKey, Collection<Key> keys) throws NullPointerException, ConcurrentModificationException { if (ds == null) { throw new NullPointerException("The ds parameter must not be null."); } if (tx == null) { throw new NullPointerException("The tx parameter must not be null."); } if (rootKey == null) { throw new NullPointerException( "The rootKey parameter must not be null."); } if (keys == null) { throw new NullPointerException( "The keys parameter must not be null."); } Key lockKey = createKey(rootKey); List<Key> keyList = new ArrayList<Key>(keys.size() + 1); keyList.addAll(keys); keyList.add(lockKey); Map<Key, Entity> map = DatastoreUtil.getAsMap(ds, tx, keyList); if (map.containsKey(lockKey)) { throw createConcurrentModificationException(rootKey); } return map; } /** * Creates a {@link ConcurrentModificationException}. * * @param rootKey * the root key * * @return a {@link ConcurrentModificationException} * @throws NullPointerException * if the cause parameter is null */ protected static ConcurrentModificationException createConcurrentModificationException( Key rootKey) throws NullPointerException { if (rootKey == null) { throw new NullPointerException( "The rootKey parameter must not be null."); } return new ConcurrentModificationException("Locking the entity group(" + rootKey + ") failed."); } /** * Creates a {@link ConcurrentModificationException}. * * @param rootKey * the root key * @param cause * the cause * * @return a {@link ConcurrentModificationException} * @throws NullPointerException * if the rootKey parameter is null or if the cause parameter is * null */ protected static ConcurrentModificationException createConcurrentModificationException( Key rootKey, Throwable cause) throws NullPointerException { if (rootKey == null) { throw new NullPointerException( "The rootKey parameter must not be null."); } if (cause == null) { throw new NullPointerException( "The cause parameter must not be null."); } ConcurrentModificationException cme = new ConcurrentModificationException("Locking the entity group(" + rootKey + ") failed."); cme.initCause(cause); return cme; } /** * Constructor. * * @param ds * the asynchronous datastore service * @param globalTransactionKey * the global transaction key * @param rootKey * the root key * @param timestamp * the time-stamp * * @throws NullPointerException * if the ds parameter is null or if the rootKey parameter is * null or if the timestamp parameter is null or if the * globalTransactionKey parameter is null */ public Lock(AsyncDatastoreService ds, Key globalTransactionKey, Key rootKey, Long timestamp) throws NullPointerException { if (ds == null) { throw new NullPointerException("The ds parameter must not be null."); } if (globalTransactionKey == null) { throw new NullPointerException( "The globalTranactionKey parameter must not be null."); } if (rootKey == null) { throw new NullPointerException( "The rootKey parameter must not be null."); } if (timestamp == null) { throw new NullPointerException( "The timestamp parameter must not be null."); } this.ds = ds; this.globalTransactionKey = globalTransactionKey; this.key = createKey(rootKey); this.rootKey = rootKey; this.timestamp = timestamp; } /** * Converts this instance to an entity. * * @return an entity */ public Entity toEntity() { Entity entity = new Entity(key); entity.setProperty( GLOBAL_TRANSACTION_KEY_PROPERTY, globalTransactionKey); entity.setProperty(TIMESTAMP_PROPERTY, timestamp); return entity; } /** * Locks the entity group. * * @throws ConcurrentModificationException * if locking the entity failed */ public void lock() throws ConcurrentModificationException { Transaction tx = DatastoreUtil.beginTransaction(ds); try { Lock other = getOrNull(ds, tx, key); if (other != null) { verify(other); } DatastoreUtil.put(ds, tx, toEntity()); tx.commit(); return; } finally { if (tx.isActive()) { tx.rollback(); } } } /** * Locks the entity group and returns entities specified by the target keys. * * @param targetKeys * the target keys * @return an entity specified by the target keys * @throws NullPointerException * if the targetKeys parameter is null * @throws ConcurrentModificationException * if locking the entity failed */ public Map<Key, Entity> lockAndGetAsMap(Collection<Key> targetKeys) throws NullPointerException, ConcurrentModificationException { Map<Key, Entity> map = null; Transaction tx = DatastoreUtil.beginTransaction(ds); try { List<Key> keyList = new ArrayList<Key>(targetKeys.size() + 1); keyList.addAll(targetKeys); keyList.add(key); map = DatastoreUtil.getAsMap(ds, tx, keyList); Entity otherEntity = map.remove(key); if (otherEntity != null) { Lock other = toLock(ds, otherEntity); verify(other); } DatastoreUtil.put(ds, tx, toEntity()); tx.commit(); return map; } finally { if (tx.isActive()) { tx.rollback(); } } } /** * Verifies the other {@link Lock}. * * @param other * the other {@link Lock} * @throws NullPointerException * if the other parameter is null * @throws ConcurrentModificationException * if the entity group is locked by the other one */ protected void verify(Lock other) throws NullPointerException, ConcurrentModificationException { if (other == null) { throw new NullPointerException( "The other parameter must not be null."); } if (globalTransactionKey.equals(other.globalTransactionKey)) { return; } if (timestamp <= other.getTimestamp() + TIMEOUT) { throw createConcurrentModificationException(rootKey); } Transaction tx = DatastoreUtil.beginTransaction(ds); try { GlobalTransaction gtx = GlobalTransaction.getOrNull(ds, tx, other.globalTransactionKey); if (gtx != null) { if (gtx.valid) { throw createConcurrentModificationException(rootKey); } return; } gtx = new GlobalTransaction(ds, other.globalTransactionKey, false); GlobalTransaction.put(ds, tx, gtx); tx.commit(); } catch (ConcurrentModificationException e) { throw createConcurrentModificationException(rootKey, e); } finally { if (tx.isActive()) { tx.rollback(); } } } /** * Returns the global transaction key. * * @return the global transaction key */ public Key getGlobalTransactionKey() { return globalTransactionKey; } /** * Returns the key. * * @return the key */ public Key getKey() { return key; } /** * Returns the root key. * * @return the root key */ public Key getRootKey() { return rootKey; } /** * Returns the time-stamp. * * @return the time-stamp */ public long getTimestamp() { return timestamp; } }