/*
* 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.persistence.file;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;
import org.h2.mvstore.MVStore;
import org.h2.mvstore.db.TransactionStore;
import org.modeshape.common.logging.Logger;
import org.modeshape.common.util.FileUtil;
import org.modeshape.common.util.StringUtil;
import org.modeshape.schematic.SchematicDb;
import org.modeshape.schematic.SchematicEntry;
import org.modeshape.schematic.document.Document;
import org.modeshape.schematic.document.EditableDocument;
/**
* {@link SchematicDb} implementation which uses H2's MV Store to store data in memory or on disk.
*
* @author Horia Chiorean (hchiorea@redhat.com)
*/
public class FileDb implements SchematicDb {
private final static Logger LOGGER = Logger.getLogger(FileDb.class);
private final static String FILENAME = "modeshape.repository";
private final static ThreadLocal<String> ACTIVE_TX_ID = new ThreadLocal<>();
private final static String REPOSITORY_CONTENT = "modeshape_data";
private final boolean compress;
private final String path;
private final ConcurrentMap<String, TransactionStore.TransactionMap<String, Document>> transactionalContentById = new ConcurrentHashMap<>();
private MVStore store;
private TransactionStore txStore;
private TransactionStore.TransactionMap<String, Document> persistedContent;
protected static FileDb inMemory(boolean compress) {
return new FileDb(null, compress);
}
protected static FileDb onDisk(boolean compress, String path) {
path = Objects.requireNonNull(path, "The 'path' configuration parameter is required by the FS persistence provider");
return new FileDb(path, compress);
}
private FileDb( String path, boolean compress ) {
this.path = path;
this.compress = compress;
}
@Override
public String id() {
String prefix = "modeshape-file-persistence";
return path == null ? prefix : prefix + "_" + path;
}
@Override
public List<String> keys() {
List<String> keys = new ArrayList<>();
persistedContent.keyIterator(persistedContent.firstKey()).forEachRemaining(keys::add);
TransactionStore.TransactionMap<String, Document> txContent = transactionalContent(false);
if (txContent != null) {
txContent.keyIterator(txContent.firstKey()).forEachRemaining(keys::add);
}
return keys;
}
@Override
public Document get( String key ) {
LOGGER.debug("reading {0}", key);
TransactionStore.TransactionMap<String, Document> txContent = transactionalContent(false);
Document result = txContent != null ? txContent.getLatest(key) : persistedContent.get(key);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("{0} is {1}", key, result);
}
return result;
}
@Override
public List<SchematicEntry> load( Collection<String> keys ) {
final TransactionStore.TransactionMap<String, Document> txContent = transactionalContent(false);
final TransactionStore.TransactionMap<String, Document> actualContent = txContent != null ? txContent : persistedContent;
return keys.stream()
.map(actualContent::get)
.filter(Objects::nonNull)
.map(SchematicEntry::fromDocument)
.collect(Collectors.toList());
}
@Override
public void put( String key, SchematicEntry entry ) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("putting at {0} document {1}", key, entry.source());
}
TransactionStore.TransactionMap<String, Document> txContent = transactionalContent(true);
Document source = entry.source();
Document content = entry.content();
if (content instanceof EditableDocument) {
source = SchematicEntry.create(entry.id(), ((EditableDocument) content).unwrap()).source();
}
txContent.put(key, source);
}
@Override
public EditableDocument editContent( String key, boolean createIfMissing ) {
TransactionStore.TransactionMap<String, Document> txContent = transactionalContent(true);
Document existingTxDoc = txContent.get(key);
if (existingTxDoc == null && createIfMissing) {
existingTxDoc = SchematicEntry.create(key).source();
txContent.put(key, existingTxDoc);
}
if (existingTxDoc == null) {
return null;
}
if (!txContent.isSameTransaction(key)) {
// this transaction is processing this key for the first time, so we need to clone it
existingTxDoc = existingTxDoc.clone();
if (!txContent.trySet(key, existingTxDoc, true)) {
throw new FileProviderException("cannot write new value for the first time");
}
}
return SchematicEntry.content(existingTxDoc).editable();
}
@Override
public SchematicEntry putIfAbsent( String key, Document content ) {
SchematicEntry existingEntry = getEntry(key);
if (existingEntry != null) {
return existingEntry;
} else {
put(key, SchematicEntry.create(key, content));
return null;
}
}
@Override
public boolean remove( String key ) {
TransactionStore.TransactionMap<String, Document> txContent = transactionalContent(true);
Document doc = txContent.remove(key);
if (doc != null) {
LOGGER.debug("removed document at {0}", key);
return true;
}
return false;
}
@Override
public void removeAll() {
TransactionStore.TransactionMap<String, Document> txContent = transactionalContent(true);
txContent.clear();
}
@Override
public void start() {
MVStore.Builder builder = new MVStore.Builder();
builder.autoCommitDisabled();
if (compress) {
builder.compress();
}
if (!StringUtil.isBlank(path)) {
File file = new File(path);
if (!file.exists() || !file.isDirectory() || !file.canRead()) {
FileUtil.delete(file);
try {
Files.createDirectories(Paths.get(path));
} catch (IOException e) {
throw new FileProviderException(e);
}
}
builder.fileName(path + "/" + FILENAME);
}
this.store = builder.open();
this.txStore = new TransactionStore(store);
this.txStore.init();
// start a new transaction (which has READ_COMMITTED isolation) which will give us the view of the latest persisted data
TransactionStore.Transaction tx = this.txStore.begin();
this.persistedContent = tx.openMap(REPOSITORY_CONTENT);
}
@Override
public void stop() {
this.txStore.getOpenTransactions().forEach(TransactionStore.Transaction::rollback);
// close the store
this.store.close();
}
@Override
public void txStarted( String id ) {
LOGGER.debug("New tx '{0}' started...", id);
String currentTx = ACTIVE_TX_ID.get();
if (currentTx != null && !id.equals(currentTx)) {
throw new FileProviderException(
"ModeShape transaction '" + currentTx + "' already associated to current thread; cannot associate new transaction " +
"'" + id + "'");
}
ACTIVE_TX_ID.set(id);
this.transactionalContentById.putIfAbsent(id, this.txStore.begin().openMap(REPOSITORY_CONTENT));
}
@Override
public void txCommitted( String id ) {
LOGGER.debug("Received committed notification for tx '{0}'", id);
try {
TransactionStore.TransactionMap<String, Document> txContent = this.transactionalContentById.remove(id);
TransactionStore.Transaction tx = txContent.getTransaction();
tx.commit();
LOGGER.debug("tx '{0}' committed", id);
} finally {
ACTIVE_TX_ID.remove();
}
}
@Override
public void txRolledback( String id ) {
LOGGER.debug("Received rollback notification for tx '{0}'", id);
try {
TransactionStore.Transaction tx = this.transactionalContentById.remove(id).getTransaction();
tx.rollback();
LOGGER.debug("tx '{0}' rolled back", id);
} finally {
ACTIVE_TX_ID.remove();
}
}
protected TransactionStore.TransactionMap<String, Document> transactionalContent(boolean failIfMissing) {
String currentTxId = ACTIVE_TX_ID.get();
if (currentTxId == null) {
if (failIfMissing) {
throw new FileProviderException("An active transaction is required, but wasn't detected");
} else {
return null;
}
}
TransactionStore.TransactionMap<String, Document> result = this.transactionalContentById.get(currentTxId);
if (result == null) {
if (failIfMissing) {
throw new FileProviderException("No MV store transaction was found for tx id '" + currentTxId +"'");
} else {
LOGGER.debug(
"Found active ModeShape transaction '{0}' without a corresponding MV store transaction; most likely this has been committed off a separate thread",
currentTxId);
ACTIVE_TX_ID.remove();
}
}
return result;
}
}