/*
* 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 com.sun.jini.mahalo.log;
import com.sun.jini.logging.Levels;
import com.sun.jini.mahalo.TxnManager;
import com.sun.jini.system.FileSystem;
import net.jini.admin.Administrable;
import java.io.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* @author Sun Microsystems, Inc.
*
*/
public class MultiLogManager
implements LogManager, FileModes, Administrable, MultiLogManagerAdmin {
private static final String LOG_FILE = "Log.";
/** Logger for persistence related messages */
private static final Logger persistenceLogger =
Logger.getLogger(TxnManager.MAHALO + ".persistence");
/** Logger for operations related messages */
private static final Logger operationsLogger =
Logger.getLogger(TxnManager.MAHALO + ".operations");
/** Logger for initialization related messages */
private static final Logger initLogger =
Logger.getLogger(TxnManager.MAHALO + ".init");
/** Client called during log recovery to process log objects */
private final LogRecovery client;
/** Map of log files keyed by their associated cookie */
private final Map logByID = new HashMap();
/** Lock object used for coordinating access to logByID */
private final Object logByIDLock = new Object();
/** Flag that is set to true upon destruction */
private boolean destroyed = false;
/** Persistence directory */
private String directory = null;
private final static FilenameFilter filter =
new FilenameFilter() {
public boolean accept(File dir, String name) {
if (operationsLogger.isLoggable(Level.FINER)) {
operationsLogger.entering(FilenameFilter.class.getName(),
"accept", new Object[] {dir, name});
}
final boolean isLog = name.startsWith(LOG_FILE);
if (operationsLogger.isLoggable(Level.FINER)) {
operationsLogger.exiting(FilenameFilter.class.getName(),
"accept", Boolean.valueOf(isLog));
}
return isLog;
};
};
/**
* Callback interface for log files to remove themselves from
* this manager
*/
public static interface LogRemovalManager {
public void release(long cookie);
}
/**
* Capability object passed to log files, which is called back upon
* log removal.
*/
final LogRemovalManager logMgrRef = new LogRemovalManager() {
public void release(long cookie) {
MultiLogManager.this.release(cookie);
}
};
/**
* Create a non-persistent <code>MultiLogManager</code>.
*/
public MultiLogManager() {
directory = null; // just for insurance
client = null;
}
/**
* Create a <code>MultiLogManager</code>.
*
* @param client who to inform during recovery.
*
* @param path where to store logging information.
*/
public MultiLogManager(LogRecovery client, String path) {
if (path == null)
throw new IllegalArgumentException("MultiLogManager: must use " +
"non-null path");
if (client == null)
throw new IllegalArgumentException("MultiLogManager: must use " +
"non-null recovery client");
this.client = client;
directory = path;
if (!directory.endsWith(File.separator))
directory = directory.concat(File.separator);
if (persistenceLogger.isLoggable(Level.FINEST)) {
persistenceLogger.log(Level.FINEST,
"directory = {0}", directory);
}
File tmpfile = new File(directory);
//If you attempt to access the Version file and
//it does not exist, start with zero.
try {
if (!tmpfile.exists())
if (!tmpfile.mkdirs())
if (persistenceLogger.isLoggable(Level.SEVERE)) {
persistenceLogger.log(Level.SEVERE,
"Could not create {0}", tmpfile);
}
//TODO - ignore???
} catch (SecurityException se) {
if (persistenceLogger.isLoggable(Level.SEVERE)) {
persistenceLogger.log(Level.SEVERE,
"Error accessing Version File", se);
}
//TODO ignore? throw (SecurityException)se.fillInStackTrace();
}
}
// javadoc inherited from supertype
public ClientLog logFor(long cookie) throws LogException {
if (operationsLogger.isLoggable(Level.FINER)) {
operationsLogger.entering(MultiLogManager.class.getName(),
"logFor", new Long(cookie));
}
ClientLog cl = null;
Long key = new Long(cookie);
Object prev = null;
synchronized(logByIDLock) {
if (destroyed)
throw new LogException("Manger has been destroyed");
cl = (ClientLog)logByID.get(key);
if (cl == null) {
cl = (directory==null)?
(ClientLog)new TransientLogFile(cookie, logMgrRef):
(ClientLog)new SimpleLogFile(
directory + LOG_FILE + cookie,
cookie, logMgrRef);
if (persistenceLogger.isLoggable(Level.FINEST)) {
persistenceLogger.log(Level.FINEST,
"Created ClientLog: {0}",
directory + LOG_FILE + cookie);
}
prev = logByID.put(key, cl);
}
if (persistenceLogger.isLoggable(Level.FINEST)) {
persistenceLogger.log(Level.FINEST,
"Currently managing {0} logs.",
new Integer(logByID.size()));
}
}
if (prev != null)
throw new LogException("Previous mapping for cookie("
+ cookie + ") -- internal table corrupt?");
if (persistenceLogger.isLoggable(Level.FINEST)) {
persistenceLogger.log(Level.FINEST,
"Using ClientLog {0} for cookie {1}",
new Object[] {cl, new Long(cookie)});
}
if (operationsLogger.isLoggable(Level.FINER)) {
operationsLogger.exiting(MultiLogManager.class.getName(),
"logFor", cl);
}
return cl;
}
// javadoc inherited from supertype
private void release(long cookie) {
if (operationsLogger.isLoggable(Level.FINER)) {
operationsLogger.entering(MultiLogManager.class.getName(),
"release", new Long(cookie));
}
Object prev = null;
synchronized(logByIDLock) {
if (destroyed)
return;
if (persistenceLogger.isLoggable(Level.FINEST)) {
persistenceLogger.log(Level.FINEST,
"Releasing ClientLog for cookie {0}",
new Long(cookie));
}
prev = logByID.remove(new Long(cookie));
if (persistenceLogger.isLoggable(Level.FINEST)) {
persistenceLogger.log(Level.FINEST,
"Currently managing {0} logs.",
new Integer(logByID.size()));
}
}
if (persistenceLogger.isLoggable(Level.FINEST)) {
if (prev == null) {
persistenceLogger.log(Level.FINEST,
"Note: ClientLog already removed");
}
}
if (operationsLogger.isLoggable(Level.FINER)) {
operationsLogger.exiting(MultiLogManager.class.getName(),
"release");
}
}
/**
* Consumes the log file and re-constructs a system's
* state.
*/
public void recover() throws LogException {
if (operationsLogger.isLoggable(Level.FINER)) {
operationsLogger.entering(MultiLogManager.class.getName(),
"recover");
}
// Short-circuit for non-persistent mode
if (directory == null) return;
Log log = null;
File tmpfile = null;
String [] filenames = null;
/* Called by initialization thread only,
* so don't need to check for destroyed.
*/
try {
tmpfile = new File(directory);
filenames = tmpfile.list(filter);
if (filenames.length == 0)
return;
String logName;
for (int i = 0; i < filenames.length; i++ ) {
logName = directory + filenames[i];
log = new SimpleLogFile(logName, logMgrRef);
if (persistenceLogger.isLoggable(Level.FINEST)) {
persistenceLogger.log(Level.FINEST,
"Recovering log: {0}", logName);
}
try {
log.recover(client);
/* Called by initialization thread only,
* so doesn't need to be synchronized here.
*/
logByID.put(new Long(log.cookie()),log);
} catch (LogException le) {
if(persistenceLogger.isLoggable(Level.WARNING)) {
persistenceLogger.log(Level.WARNING,
"Unable to recover log state", le);
}
}
}
} catch (SecurityException se) { // TODO - shouldn't this percolate back up?
if(persistenceLogger.isLoggable(Level.WARNING)) {
persistenceLogger.log(Level.WARNING,
"Unable to recover log state", se);
}
}
if (operationsLogger.isLoggable(Level.FINER)) {
operationsLogger.exiting(MultiLogManager.class.getName(),
"recover");
}
}
/**
* Retrieves the administration interface for the
* <code>MultiLogManager</code>
*
*/
public Object getAdmin() {
// TBD - pass capability object instead?
if (operationsLogger.isLoggable(Level.FINER)) {
operationsLogger.entering(MultiLogManager.class.getName(),
"getAdmin");
}
if (operationsLogger.isLoggable(Level.FINER)) {
operationsLogger.exiting(MultiLogManager.class.getName(),
"getAdmin", this);
}
return (MultiLogManagerAdmin)this;
}
/**
* Clean up all <code>LogFile</code> objects on behalf of caller.
*
* @see com.sun.jini.admin.DestroyAdmin
* @see com.sun.jini.system.FileSystem
*/
public void destroy() {
if (operationsLogger.isLoggable(Level.FINER)) {
operationsLogger.entering(MultiLogManager.class.getName(),
"destroy");
}
// TBD - Set destroy flag? used by logFor()/release()
// TBD - Loop over values enum?
/**
* Loop over know logs and invalidate them.
*/
synchronized(logByIDLock) {
if (destroyed) // return silently to avoids retries
return;
if (logByID.size() > 0) {
/* Can't use iterator because slf.invalidate() calls back into
* this.release() in order to remove itself from logByID,
* which would cause a concurrent modification exception.
*/
Object [] vals = logByID.values().toArray();
Log slf = null;
for (int i=0; i<vals.length; i++) {
try {
slf = (Log)vals[i];
if (slf != null)
slf.invalidate();
else {
if (persistenceLogger.isLoggable(Level.FINEST)) {
persistenceLogger.log(Level.FINEST,
"Observed a null log file entry for: {0}", slf);
}
}
} catch (LogException le) {
if(persistenceLogger.isLoggable(Levels.HANDLED)) {
persistenceLogger.log(Levels.HANDLED,
"Unable to recover log state", le);
}
} catch (java.util.NoSuchElementException nsee) {
if(persistenceLogger.isLoggable(Levels.HANDLED)) {
persistenceLogger.log(Levels.HANDLED,
"Problem enumerating internal log state", nsee);
}
}
}
logByID.clear();
destroyed = true;
}
}
if (operationsLogger.isLoggable(Level.FINER)) {
operationsLogger.exiting(MultiLogManager.class.getName(),
"destroy");
}
}
}