/*
* ModeShape (http://www.modeshape.org)
*
* 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.modeshape.jcr.cache.document;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.function.BiFunction;
import javax.transaction.NotSupportedException;
import javax.transaction.SystemException;
import org.modeshape.common.SystemFailureException;
import org.modeshape.common.util.CheckArg;
import org.modeshape.jcr.RepositoryEnvironment;
import org.modeshape.jcr.cache.SessionCache;
import org.modeshape.jcr.locking.LockingService;
import org.modeshape.jcr.txn.Transactions;
import org.modeshape.jcr.value.Name;
import org.modeshape.jcr.value.binary.ExternalBinaryValue;
import org.modeshape.schematic.SchematicDb;
import org.modeshape.schematic.SchematicEntry;
import org.modeshape.schematic.annotation.RequiresTransaction;
import org.modeshape.schematic.document.Document;
import org.modeshape.schematic.document.EditableDocument;
/**
* An implementation of {@link DocumentStore} which always uses the local cache to store/retrieve data and which provides some
* additional methods for exposing local cache information.
*
* @author Horia Chiorean (hchiorea@redhat.com)
*/
public class LocalDocumentStore implements DocumentStore {
private final SchematicDb database;
private final RepositoryEnvironment repoEnv;
private String localSourceKey;
/**
* Creates a new local store with the given database
*
* @param database a {@link SchematicDb} instance which must be non-null.
* @param repoEnv a {@link RepositoryEnvironment} instance which must be non-null
*/
public LocalDocumentStore(SchematicDb database, RepositoryEnvironment repoEnv) {
CheckArg.isNotNull(database, "database");
this.database = database;
CheckArg.isNotNull(repoEnv, "repoEnv");
this.repoEnv = repoEnv;
}
@Override
public boolean containsKey( String key ) {
return database.containsKey(key);
}
/**
* Returns all the keys which are held by this store.
*
* @return a {@link Set} of keys, never {@code null}
*/
public List<String> keys() {
return database.keys();
}
@Override
public List<SchematicEntry> load(Collection<String> keys) {
return database.load(keys);
}
@Override
public SchematicEntry get( String key ) {
return database.getEntry(key);
}
@Override
public SchematicEntry storeIfAbsent(String key,
Document document) {
return database.putIfAbsent(key, document);
}
@Override
public void updateDocument( String key,
Document document,
SessionNode sessionNode ) {
// do nothing, the way the local store updates is via editing schematic entry literals
}
@Override
public String newDocumentKey( String parentKey,
Name documentName,
Name documentPrimaryType ) {
// the local store doesn't generate explicit keys for new nodes
return null;
}
/**
* Store the supplied document and metadata at the given key.
*
* @param key the key or identifier for the document
* @param document the document that is to be stored
* @see SchematicDb#put(String, Document)
*/
@RequiresTransaction
public void put( String key,
Document document ) {
database.put(key, document);
}
/**
* Store the supplied document in the local db
*
* @param entryDocument the document that contains the metadata document, content document, and key
*/
@RequiresTransaction
public void put( Document entryDocument ) {
database.putEntry(entryDocument);
}
@Override
public boolean remove( String key ) {
return database.remove(key);
}
/**
* Removes all the contents of the document store (i.e. all the documents)
*
* Note that for this to work, it is expected that the caller will've already started a transaction.
*/
@RequiresTransaction
public void removeAll() {
database.removeAll();
}
@Override
public boolean lockDocuments( Collection<String> keys ) {
return lockDocuments(keys.toArray(new String[keys.size()]));
}
@Override
public boolean lockDocuments(String... keys) {
Transactions.Transaction tx = repoEnv.getTransactions().currentTransaction();
if (tx == null) {
throw new IllegalStateException("Cannot attempt to lock documents without an existing ModeShape transaction");
}
try {
LockingService lockingService = repoEnv.lockingService();
boolean locked = lockingService.tryLock(keys);
if (locked) {
tx.uponCompletion(() -> lockingService.unlock(keys));
}
return locked;
} catch (RuntimeException rt) {
throw rt;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public EditableDocument edit( String key,
boolean createIfMissing ) {
return database.editContent(key, createIfMissing);
}
@Override
public LocalDocumentStore localStore() {
return this;
}
@Override
public void setLocalSourceKey( String sourceKey ) {
this.localSourceKey = sourceKey;
}
@Override
public String getLocalSourceKey() {
return this.localSourceKey;
}
@Override
public String createExternalProjection( String projectedNodeKey,
String sourceName,
String externalPath,
String alias,
SessionCache systemSession) {
throw new UnsupportedOperationException("External projections are not supported in the local document store");
}
@Override
public Document getChildrenBlock( String key ) {
// Look up the information in the database ...
SchematicEntry entry = get(key);
if (entry == null) {
// There is no such node ...
return null;
}
return entry.content();
}
@Override
public Document getChildReference( String parentKey,
String childKey ) {
return null; // don't support this
}
@Override
public ExternalBinaryValue getExternalBinary( String sourceName,
String id ) {
throw new UnsupportedOperationException("External binaries are only supported by the federated document store");
}
/**
* Returns the id of the database.
*
* @return an identifier string, never {@code null}
*/
public String databaseId() {
return database.id();
}
/**
* Perform the supplied operation on each stored document that is accessible within this process. Each document will be
* operated upon in a separate transaction, which will be committed if the operation is successful or rolledback if the
* operation cannot be complete successfully.
* <p>
* Generally, this method executes the operation upon all documents. If there is an error processing a single document, that
* document is skipped and the execution will continue with the next document(s). However, if there is an exception with the
* transactions or another system failure, this method will terminate with an exception.
*
* @param operation the operation to be performed
* @return the summary of the number of documents that were affected
*/
public DocumentOperationResults performOnEachDocument( BiFunction<String, EditableDocument, Boolean> operation ) {
DocumentOperationResults results = new DocumentOperationResults();
database.keys().forEach(key ->
runInTransaction(() -> {
// We operate upon each document within a transaction ...
try {
EditableDocument doc = edit(key, false);
if (doc != null) {
if (operation.apply(key, doc)) {
results.recordModified();
} else {
results.recordUnmodified();
}
}
} catch (Throwable t) {
results.recordFailure();
}
return null;
}, 1, key));
return results;
}
/**
* Runs the given operation within a transaction, after optionally locking some keys.
*
* @param operation a {@link Callable} instance; may not be null
* @param retryCountOnLockTimeout the number of times the operation should be retried if a timeout occurs while trying
* to obtain the locks
* @param keysToLock an optional {@link String[]} representing the keys to lock before performing the operation
* @param <V> the return type of the operation
* @return the result of operation
*/
public <V> V runInTransaction( Callable<V> operation, int retryCountOnLockTimeout, String... keysToLock ) {
// Start a transaction ...
Transactions txns = repoEnv.getTransactions();
int retryCount = retryCountOnLockTimeout;
try {
Transactions.Transaction txn = txns.begin();
if (keysToLock.length > 0) {
List<String> keysList = Arrays.asList(keysToLock);
boolean locksAcquired = false;
while (!locksAcquired && retryCountOnLockTimeout-- >= 0) {
locksAcquired = lockDocuments(keysList);
}
if (!locksAcquired) {
txn.rollback();
throw new org.modeshape.jcr.TimeoutException(
"Cannot acquire locks on: " + Arrays.toString(keysToLock) + " after " + retryCount + " attempts");
}
}
try {
V result = operation.call();
txn.commit();
return result;
} catch (Exception e) {
// always rollback
txn.rollback();
// throw as is (see below)
throw e;
}
} catch (IllegalStateException | SystemException | NotSupportedException err) {
throw new SystemFailureException(err);
} catch (RuntimeException re) {
throw re;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static class DocumentOperationResults implements Serializable {
private static final long serialVersionUID = 1L;
private long modifiedCount;
private long unmodifiedCount;
private long skipCount;
private long failureCount;
/**
* Return the number of documents that were successfully updated/modified by the operation.
*
* @return the number of modified documents
*/
public long getModifiedCount() {
return modifiedCount;
}
/**
* Return the number of documents that were not updated/modified by the operation.
*
* @return the number of unmodified documents
*/
public long getUnmodifiedCount() {
return unmodifiedCount;
}
/**
* Return the number of documents that caused some failure.
*
* @return the number of failed documents
*/
public long getFailureCount() {
return failureCount;
}
/**
* Return the number of documents that were skipped by the operation because the document could not be obtained in an
* timely fashion.
*
* @return the number of skipped documents
*/
public long getSkipCount() {
return skipCount;
}
protected void recordModified() {
++modifiedCount;
}
protected void recordUnmodified() {
++unmodifiedCount;
}
protected void recordFailure() {
++failureCount;
}
protected void recordSkipped() {
++skipCount;
}
protected DocumentOperationResults combine( DocumentOperationResults other ) {
if (other != null) {
this.modifiedCount += other.modifiedCount;
this.unmodifiedCount += other.unmodifiedCount;
this.skipCount += other.skipCount;
this.failureCount += other.failureCount;
}
return this;
}
@Override
public String toString() {
return "" + modifiedCount + " documents changed, " + unmodifiedCount + " unchanged, " + skipCount + " skipped, and "
+ failureCount + " resulted in errors or failures";
}
}
}