/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.jackrabbit.core.journal; import java.io.File; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import org.apache.jackrabbit.core.util.XAReentrantWriterPreferenceReadWriteLock; import org.apache.jackrabbit.core.version.InternalVersionManagerImpl; import org.apache.jackrabbit.core.version.VersioningLock; import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Base journal implementation. */ public abstract class AbstractJournal implements Journal { /** * Logger. */ private static Logger log = LoggerFactory.getLogger(AbstractJournal.class); /** * Journal id. */ private String id; /** * Namespace resolver. */ private NamespaceResolver resolver; /** * NamePathResolver */ private NamePathResolver npResolver; /** * Map of registered consumers. */ private final Map<String, RecordConsumer> consumers = new HashMap<String, RecordConsumer>(); /** * Map of registered producers. */ private final Map<String, RecordProducer> producers = new HashMap<String, RecordProducer>(); /** * Journal lock, allowing multiple readers (synchronizing their contents) * but only one writer (appending a new entry). */ private final XAReentrantWriterPreferenceReadWriteLock rwLock = new XAReentrantWriterPreferenceReadWriteLock(); /** * The path of the local revision file on disk. Configurable through the repository.xml. * * Note: this field is not located in the FileJournal class for upgrade reasons (before * JCR-1087 had been fixed all Journals used a revision file on the local file system. * Also see {@link DatabaseJournal#initInstanceRevisionAndJanitor()}). */ private String revision; /** * Repository home. */ private File repHome; /** * Internal version manager. */ private InternalVersionManagerImpl internalVersionManager; /** * {@inheritDoc} */ public void init(String id, NamespaceResolver resolver) throws JournalException { this.id = id; this.resolver = resolver; this.npResolver = new DefaultNamePathResolver(resolver, true); } /** * {@inheritDoc} */ public void register(RecordConsumer consumer) throws JournalException { synchronized (consumers) { String consumerId = consumer.getId(); if (consumers.containsKey(consumerId)) { String msg = "Record consumer with identifier '" + consumerId + "' already registered."; throw new JournalException(msg); } consumers.put(consumerId, consumer); } } /** * {@inheritDoc} */ public boolean unregister(RecordConsumer consumer) { synchronized (consumers) { String consumerId = consumer.getId(); return consumers.remove(consumerId) != null; } } /** * Return the consumer given its identifier. * * @param identifier identifier * @return consumer associated with identifier; * <code>null</code> if no consumer is associated with identifier */ public RecordConsumer getConsumer(String identifier) { synchronized (consumers) { return consumers.get(identifier); } } /** * {@inheritDoc} */ public RecordProducer getProducer(String identifier) { synchronized (producers) { RecordProducer producer = producers.get(identifier); if (producer == null) { producer = createProducer(identifier); producers.put(identifier, producer); } return producer; } } /** * Create the record producer for a given identifier. May be overridden * by subclasses. * * @param identifier producer identifier */ protected RecordProducer createProducer(String identifier) { return new DefaultRecordProducer(this, identifier); } /** * Return the minimal revision of all registered consumers. */ private long getMinimalRevision() { long minimalRevision = Long.MAX_VALUE; synchronized (consumers) { for (RecordConsumer consumer : consumers.values()) { if (consumer.getRevision() < minimalRevision) { minimalRevision = consumer.getRevision(); } } } return minimalRevision; } /** * {@inheritDoc} */ public void sync(boolean startup) throws JournalException { for (;;) { if (internalVersionManager != null) { VersioningLock.ReadLock lock = internalVersionManager.acquireReadLock(); try { internalSync(startup); } finally { lock.release(); } } else { internalSync(startup); } // startup sync already done, don't do it again startup = false; if (syncAgainOnNewRecords()) { // sync again if there are more records available RecordIterator it = getRecords(getMinimalRevision()); try { if (it.hasNext()) { continue; } } finally { it.close(); } } break; } } private void internalSync(boolean startup) throws JournalException { try { rwLock.readLock().acquire(); } catch (InterruptedException e) { String msg = "Unable to acquire read lock."; throw new JournalException(msg, e); } try { doSync(getMinimalRevision(), startup); } finally { rwLock.readLock().release(); } } protected void doSync(long startRevision, boolean startup) throws JournalException { // by default ignore startup parameter for backwards compatibility // only needed for persistence backend that need special treatment on startup. doSync(startRevision); } /** * * Synchronize contents from journal. May be overridden by subclasses. * * @param startRevision start point (exclusive) * @throws JournalException if an error occurs */ protected void doSync(long startRevision) throws JournalException { RecordIterator iterator = getRecords(startRevision); long stopRevision = Long.MIN_VALUE; try { while (iterator.hasNext()) { Record record = iterator.nextRecord(); if (record.getJournalId().equals(id)) { log.debug("Record with revision '" + record.getRevision() + "' created by this journal, skipped."); } else { RecordConsumer consumer = getConsumer(record.getProducerId()); if (consumer != null) { consumer.consume(record); } } stopRevision = record.getRevision(); } } catch (IllegalStateException e) { log.error("Could not synchronize to revision: " + (stopRevision + 1) + " due illegal state of RecordConsumer."); } finally { iterator.close(); } if (stopRevision > 0) { for (RecordConsumer consumer : consumers.values()) { consumer.setRevision(stopRevision); } log.debug("Synchronized from revision " + startRevision + " to revision: " + stopRevision); } } /** * Return a flag indicating whether synchronization should continue * in a loop until no more new records are found. Subclass overridable. * * @return <code>true</code> if synchronization should continue; * <code>false</code> otherwise */ protected boolean syncAgainOnNewRecords() { return false; } /** * Lock the journal revision, disallowing changes from other sources until * {@link #unlock} has been called, and synchronizes to the latest change. * * @throws JournalException if an error occurs */ public void lockAndSync() throws JournalException { if (internalVersionManager != null) { VersioningLock.ReadLock lock = internalVersionManager.acquireReadLock(); try { internalLockAndSync(); } finally { lock.release(); } } else { internalLockAndSync(); } } private void internalLockAndSync() throws JournalException { try { rwLock.writeLock().acquire(); } catch (InterruptedException e) { String msg = "Unable to acquire write lock."; throw new JournalException(msg, e); } boolean succeeded = false; try { // lock doLock(); try { // and sync doSync(getMinimalRevision()); succeeded = true; } finally { if (!succeeded) { doUnlock(false); } } } finally { if (!succeeded) { rwLock.writeLock().release(); } } } /** * Unlock the journal revision. * * @param successful flag indicating whether the update process was * successful */ public void unlock(boolean successful) { try { doUnlock(successful); } finally { //Should not happen that a RuntimeException will be thrown in subCode, but it's safer //to release the rwLock in finally block. rwLock.writeLock().release(); } } /** * Lock the journal revision. Subclass responsibility. * * @throws JournalException if an error occurs */ protected abstract void doLock() throws JournalException; /** * Notification method called by an appended record at creation time. * May be overridden by subclasses to save some context information * inside the appended record. * * @param record record that was appended */ protected void appending(AppendRecord record) { // nothing to be done here } /** * Append a record backed by a file. On exit, the new revision must have * been set inside the appended record. Subclass responsibility. * * @param record record to append * @param in input stream * @param length number of bytes in input stream * * @throws JournalException if an error occurs */ protected abstract void append(AppendRecord record, InputStream in, int length) throws JournalException; /** * Unlock the journal revision. Subclass responsibility. * * @param successful flag indicating whether the update process was * successful */ protected abstract void doUnlock(boolean successful); /** * Return this journal's identifier. * * @return journal identifier */ public String getId() { return id; } /** * Return this journal's namespace resolver. * * @return namespace resolver */ public NamespaceResolver getResolver() { return resolver; } /** * Return this journal's NamePathResolver. * * @return name and path resolver */ public NamePathResolver getNamePathResolver() { return npResolver; } /** * Set the repository home. * * @param repHome repository home * @since JR 1.5 */ public void setRepositoryHome(File repHome) { this.repHome = repHome; } /** * Set the version manager. */ public void setInternalVersionManager(InternalVersionManagerImpl internalVersionManager) { this.internalVersionManager = internalVersionManager; } /** * Return the repository home. * * @return the repository home * @since JR 1.5 */ public File getRepositoryHome() { return repHome; } /* * Bean getters and setters. */ /** * @return the path of the cluster node's local revision file */ public String getRevision() { return revision; } /** * @param revision the path of the cluster node's local revision file to set */ public void setRevision(String revision) { this.revision = revision; } }