/* * 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 org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.io.InputStream; /** * File-based journal implementation that appends journal records to a single * file. * <p> * It is configured through the following properties: * <ul> * <li><code>revision</code>: the filename where the parent cluster node's revision * file should be written to; this is a required property with no default value</li> * <li><code>directory</code>: the directory where to keep the journal file as * well as the rotated files; this is a required property with no default value</li> * <li><code>basename</code>: the basename of journal files; the default * value is {@link #DEFAULT_BASENAME}</li> * <li><code>maximumSize</code>: the maximum size of an active journal file * before rotating it: the default value is {@link #DEFAULT_MAXSIZE} </li> * </ul> */ public class FileJournal extends AbstractJournal { /** * Default instance revision file name. */ public static final String DEFAULT_INSTANCE_FILE_NAME = "revision.log"; /** * Global revision counter name, located in the journal directory. */ private static final String REVISION_NAME = "revision"; /** * Log extension. */ private static final String LOG_EXTENSION = "log"; /** * Default base name for journal files. */ private static final String DEFAULT_BASENAME = "journal"; /** * Default max size of a journal file (1MB). */ private static final int DEFAULT_MAXSIZE = 1048576; /** * Logger. */ private static Logger log = LoggerFactory.getLogger(FileJournal.class); /** * Directory name, bean property. */ private String directory; /** * Journal file base name, bean property. */ private String basename; /** * Maximum size of a journal file before a rotation takes place, bean property. */ private int maximumSize; /** * Journal root directory. */ private File rootDirectory; /** * Journal file. */ private File journalFile; /** * Global revision counter. */ private LockableFileRevision globalRevision; /** * {@inheritDoc} */ public void init(String id, NamespaceResolver resolver) throws JournalException { super.init(id, resolver); if (getRevision() == null) { File repHome = getRepositoryHome(); if (repHome == null) { String msg = "Revision not specified."; throw new JournalException(msg); } String revision = new File(repHome, DEFAULT_INSTANCE_FILE_NAME).getPath(); log.info("Revision not specified, using: " + revision); setRevision(revision); } if (directory == null) { String msg = "Directory not specified."; throw new JournalException(msg); } if (basename == null) { basename = DEFAULT_BASENAME; } if (maximumSize == 0) { maximumSize = DEFAULT_MAXSIZE; } rootDirectory = new File(directory); // JCR-1341: Cluster Journal directory should be created automatically rootDirectory.mkdirs(); if (!rootDirectory.exists() || !rootDirectory.isDirectory()) { String msg = "Directory specified does either not exist " + "or is not a directory: " + directory; throw new JournalException(msg); } journalFile = new File(rootDirectory, basename + "." + LOG_EXTENSION); globalRevision = new LockableFileRevision(new File(rootDirectory, REVISION_NAME)); log.info("FileJournal initialized at path: " + directory); } /** * {@inheritDoc} */ protected long getGlobalRevision() throws JournalException { return globalRevision.get(); } /** * {@inheritDoc} */ public RecordIterator getRecords(long startRevision) throws JournalException { long stopRevision = getGlobalRevision(); File[] files = null; if (startRevision < stopRevision) { RotatingLogFile[] logFiles = RotatingLogFile.listFiles(rootDirectory, basename); files = new File[logFiles.length]; for (int i = 0; i < files.length; i++) { files[i] = logFiles[i].getFile(); } } return new FileRecordIterator(files, startRevision, stopRevision, getResolver(), getNamePathResolver()); } /** * {@inheritDoc} */ public RecordIterator getRecords() throws JournalException { long stopRevision = getGlobalRevision(); long startRevision = 0; RotatingLogFile[] logFiles = RotatingLogFile.listFiles(rootDirectory, basename); File[] files = new File[logFiles.length]; for (int i = 0; i < files.length; i++) { files[i] = logFiles[i].getFile(); if (i == 0) { try { FileRecordLog log = new FileRecordLog(files[i]); startRevision = log.getPreviousRevision(); } catch (IOException e) { String msg = "Unable to read startRevision from first " + "record log file"; throw new JournalException(msg, e); } } } return new FileRecordIterator(files, startRevision, stopRevision, getResolver(), getNamePathResolver()); } /** * {@inheritDoc} */ protected void doLock() throws JournalException { globalRevision.lock(false); } /** * {@inheritDoc} */ protected void append(AppendRecord record, InputStream in, int length) throws JournalException { try { FileRecordLog recordLog = new FileRecordLog(journalFile); if (recordLog.exceeds(maximumSize)) { rotateLogs(); recordLog = new FileRecordLog(journalFile); } if (recordLog.isNew()) { recordLog.init(globalRevision.get()); } long revision = recordLog.append(getId(), record.getProducerId(), in, length); globalRevision.set(revision); record.setRevision(revision); } catch (IOException e) { String msg = "Unable to append new record to journal '" + journalFile + "'."; throw new JournalException(msg, e); } } /** * {@inheritDoc} */ protected void doUnlock(boolean successful) { globalRevision.unlock(); } /** * {@inheritDoc} */ public void close() { } /** * {@inheritDoc} */ public InstanceRevision getInstanceRevision() throws JournalException { return new FileRevision(new File(getRevision()), true); } /** * Bean getters */ public String getDirectory() { return directory; } public String getBasename() { return basename; } public int getMaximumSize() { return maximumSize; } /** * Bean setters */ public void setDirectory(String directory) { this.directory = directory; } public void setBasename(String basename) { this.basename = basename; } public void setMaximumSize(int maximumSize) { this.maximumSize = maximumSize; } /** * Move away current journal file (and all other files), incrementing their * version counter. A file named <code>journal.N.log</code> gets renamed to * <code>journal.(N+1).log</code>, whereas the main journal file gets renamed * to <code>journal.1.log</code>. */ private void rotateLogs() { RotatingLogFile[] logFiles = RotatingLogFile.listFiles(rootDirectory, basename); for (int i = 0; i < logFiles.length; i++) { logFiles[i].rotate(); } } }