/* * 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; } }