/*
* JBoss, Home of Professional Open Source
* Copyright 2010, Red Hat Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.ejb3.timerservice.persistence.filestore;
import org.jboss.as.controller.services.path.PathManager;
import org.jboss.as.ejb3.logging.EjbLogger;
import org.jboss.as.ejb3.component.stateful.CurrentSynchronizationCallback;
import org.jboss.as.ejb3.timerservice.TimerImpl;
import org.jboss.as.ejb3.timerservice.TimerServiceImpl;
import org.jboss.as.ejb3.timerservice.TimerState;
import org.jboss.as.ejb3.timerservice.persistence.TimerPersistence;
import org.jboss.marshalling.MarshallerFactory;
import org.jboss.marshalling.MarshallingConfiguration;
import org.jboss.marshalling.ModularClassResolver;
import org.jboss.marshalling.river.RiverMarshallerFactory;
import org.jboss.modules.ModuleLoader;
import org.jboss.msc.service.Service;
import org.jboss.msc.service.StartContext;
import org.jboss.msc.service.StopContext;
import org.jboss.msc.value.InjectedValue;
import org.jboss.staxmapper.XMLExtendedStreamWriter;
import org.jboss.staxmapper.XMLMapper;
import org.wildfly.security.manager.WildFlySecurityManager;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.TransactionManager;
import javax.transaction.TransactionSynchronizationRegistry;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilePermission;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static java.security.AccessController.doPrivileged;
import static org.jboss.as.ejb3.logging.EjbLogger.ROOT_LOGGER;
/**
* File based persistent timer store.
* <p/>
* TODO: this is fairly hackey at the moment, it should be registered as an XA resource to support proper XA semantics
*
* @author Stuart Douglas
*/
public class FileTimerPersistence implements TimerPersistence, Service<FileTimerPersistence> {
private static final FilePermission FILE_PERMISSION = new FilePermission("<<ALL FILES>>", "read,write,delete");
private static final XMLInputFactory INPUT_FACTORY = XMLInputFactory.newInstance();
private final boolean createIfNotExists;
private MarshallerFactory factory;
private MarshallingConfiguration configuration;
private final InjectedValue<TransactionManager> transactionManager = new InjectedValue<TransactionManager>();
private final InjectedValue<TransactionSynchronizationRegistry> transactionSynchronizationRegistry = new InjectedValue<TransactionSynchronizationRegistry>();
private final InjectedValue<ModuleLoader> moduleLoader = new InjectedValue<ModuleLoader>();
private final InjectedValue<PathManager> pathManager = new InjectedValue<PathManager>();
private final String path;
private final String pathRelativeTo;
private File baseDir;
private PathManager.Callback.Handle callbackHandle;
private final ConcurrentMap<String, Lock> locks = new ConcurrentHashMap<String, Lock>();
private final ConcurrentMap<String, String> directories = new ConcurrentHashMap<String, String>();
public FileTimerPersistence(final boolean createIfNotExists, final String path, final String pathRelativeTo) {
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(FILE_PERMISSION);
}
this.createIfNotExists = createIfNotExists;
this.path = path;
this.pathRelativeTo = pathRelativeTo;
}
@Override
public void start(final StartContext context) {
if (WildFlySecurityManager.isChecking()) {
WildFlySecurityManager.doUnchecked(new PrivilegedAction<Void>() {
public Void run() {
doStart();
return null;
}
});
} else {
doStart();
}
}
private void doStart() {
final RiverMarshallerFactory factory = new RiverMarshallerFactory();
final MarshallingConfiguration configuration = new MarshallingConfiguration();
configuration.setClassResolver(ModularClassResolver.getInstance(moduleLoader.getValue()));
this.configuration = configuration;
this.factory = factory;
if (pathRelativeTo != null) {
callbackHandle = pathManager.getValue().registerCallback(pathRelativeTo, PathManager.ReloadServerCallback.create(), PathManager.Event.UPDATED, PathManager.Event.REMOVED);
}
baseDir = new File(pathManager.getValue().resolveRelativePathEntry(path, pathRelativeTo));
if (!baseDir.exists()) {
if (createIfNotExists) {
if (!baseDir.mkdirs()) {
throw EjbLogger.ROOT_LOGGER.failToCreateTimerFileStoreDir(baseDir);
}
} else {
throw EjbLogger.ROOT_LOGGER.timerFileStoreDirNotExist(baseDir);
}
}
if (!baseDir.isDirectory()) {
throw EjbLogger.ROOT_LOGGER.invalidTimerFileStoreDir(baseDir);
}
}
@Override
public void stop(final StopContext context) {
locks.clear();
directories.clear();
if (callbackHandle != null) {
callbackHandle.remove();
}
factory = null;
configuration = null;
}
@Override
public FileTimerPersistence getValue() throws IllegalStateException, IllegalArgumentException {
return this;
}
@Override
public void addTimer(final TimerImpl TimerImpl) {
if(WildFlySecurityManager.isChecking()) {
WildFlySecurityManager.doUnchecked(new PrivilegedAction<Object>() {
@Override
public Object run() {
persistTimer(TimerImpl, true);
return null;
}
});
} else {
persistTimer(TimerImpl, true);
}
}
@Override
public void persistTimer(final TimerImpl TimerImpl) {
if(WildFlySecurityManager.isChecking()) {
WildFlySecurityManager.doUnchecked(new PrivilegedAction<Object>() {
@Override
public Object run() {
persistTimer(TimerImpl, false);
return null;
}
});
} else {
persistTimer(TimerImpl, false);
}
}
@Override
public boolean shouldRun(TimerImpl timer, TransactionManager tm) {
return true;
}
private void persistTimer(final TimerImpl timer, boolean newTimer) {
final Lock lock = getLock(timer.getTimedObjectId());
try {
final int status = transactionManager.getValue().getStatus();
if (status == Status.STATUS_MARKED_ROLLBACK || status == Status.STATUS_ROLLEDBACK ||
status == Status.STATUS_ROLLING_BACK) {
//no need to persist anyway
return;
}
lock.lock();
if (status == Status.STATUS_NO_TRANSACTION ||
status == Status.STATUS_UNKNOWN || isBeforeCompletion()
|| status == Status.STATUS_COMMITTED) {
Map<String, TimerImpl> map = getTimers(timer.getTimedObjectId(), timer.getTimerService());
if (timer.getState() == TimerState.CANCELED ||
timer.getState() == TimerState.EXPIRED) {
map.remove(timer.getId());
writeFile(timer);
} else if (newTimer || map.containsKey(timer.getId())) {
//if it is not a new timer and is not in the map then it has
//been removed by another thread.
map.put(timer.getId(), timer);
writeFile(timer);
}
} else {
final String key = timerTransactionKey(timer);
Object existing = transactionSynchronizationRegistry.getValue().getResource(key);
//check is there is already a persist sync for this timer
if (existing == null) {
transactionSynchronizationRegistry.getValue().registerInterposedSynchronization(new PersistTransactionSynchronization(lock, key, newTimer));
}
//update the most recent version of the timer to be persisted
transactionSynchronizationRegistry.getValue().putResource(key, timer);
}
} catch (SystemException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
private String timerTransactionKey(final TimerImpl TimerImpl) {
return "org.jboss.as.ejb3.timerTransactionKey." + TimerImpl.getId();
}
@Override
public void timerUndeployed(final String timedObjectId) {
final Lock lock = getLock(timedObjectId);
try {
lock.lock();
locks.remove(timedObjectId);
directories.remove(timedObjectId);
} finally {
lock.unlock();
}
}
private boolean isBeforeCompletion() {
final CurrentSynchronizationCallback.CallbackType type = CurrentSynchronizationCallback.get();
if (type != null) {
return type == CurrentSynchronizationCallback.CallbackType.BEFORE_COMPLETION;
}
return false;
}
@Override
public List<TimerImpl> loadActiveTimers(final String timedObjectId, final TimerServiceImpl timerService) {
final Lock lock = getLock(timedObjectId);
try {
lock.lock();
final Map<String, TimerImpl> timers = getTimers(timedObjectId, timerService);
final List<TimerImpl> entities = new ArrayList<TimerImpl>();
for (Map.Entry<String, TimerImpl> entry : timers.entrySet()) {
entities.add(mostRecentEntityVersion(entry.getValue()));
}
return entities;
} finally {
lock.unlock();
}
}
@Override
public Closeable registerChangeListener(String timedObjectId, TimerChangeListener listener) {
return new Closeable() {
@Override
public void close() throws IOException {
}
};
}
/**
* Returns either the loaded entity or the most recent version of the entity that has
* been persisted in this transaction.
*/
private TimerImpl mostRecentEntityVersion(final TimerImpl timerImpl) {
try {
final int status = transactionManager.getValue().getStatus();
if (status == Status.STATUS_UNKNOWN ||
status == Status.STATUS_NO_TRANSACTION) {
return timerImpl;
}
final String key = timerTransactionKey(timerImpl);
TimerImpl existing = (TimerImpl) transactionSynchronizationRegistry.getValue().getResource(key);
return existing != null ? existing : timerImpl;
} catch (SystemException e) {
throw new RuntimeException(e);
}
}
private Lock getLock(final String timedObjectId) {
Lock lock = locks.get(timedObjectId);
if (lock == null) {
final Lock addedLock = new ReentrantLock();
lock = locks.putIfAbsent(timedObjectId, addedLock);
if (lock == null) {
lock = addedLock;
}
}
return lock;
}
/**
* Gets the timer map, loading from the persistent store if necessary. Should be called under lock
*
* @param timedObjectId The timed object id
* @return The timers for the object
*/
private Map<String, TimerImpl> getTimers(final String timedObjectId, final TimerServiceImpl timerService) {
return loadTimersFromFile(timedObjectId, timerService);
}
private Map<String, TimerImpl> loadTimersFromFile(String timedObjectId, TimerServiceImpl timerService) {
Map<String, TimerImpl> timers = new HashMap<>();
String directory = getDirectory(timedObjectId);
timers.putAll(LegacyFileStore.loadTimersFromFile(timedObjectId, timerService, directory, factory, configuration));
for(Map.Entry<String, TimerImpl> entry : timers.entrySet()) {
writeFile(entry.getValue()); //write legacy timers into the new format
//the legacy code handling code will write a marker file, to make sure that the old timers will not be loaded on next restart.
}
final File file = new File(directory);
if (!file.exists()) {
//no timers exist yet
return timers;
} else if (!file.isDirectory()) {
ROOT_LOGGER.failToRestoreTimers(file);
return timers;
}
final XMLMapper mapper = createMapper(timerService);
for (File timerFile : file.listFiles()) {
if (!timerFile.getName().endsWith(".xml")) {
continue;
}
FileInputStream in = null;
try {
in = new FileInputStream(timerFile);
final XMLInputFactory inputFactory = INPUT_FACTORY;
setIfSupported(inputFactory, XMLInputFactory.IS_VALIDATING, Boolean.FALSE);
setIfSupported(inputFactory, XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
final XMLStreamReader streamReader = inputFactory.createXMLStreamReader(in);
try {
List<TimerImpl> timerList = new ArrayList<>();
mapper.parseDocument(timerList, streamReader);
for (TimerImpl timer : timerList) {
timers.put(timer.getId(), timer);
}
} finally {
safeClose(in);
}
} catch (Exception e) {
ROOT_LOGGER.failToRestoreTimersFromFile(timerFile, e);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
ROOT_LOGGER.failToCloseFile(e);
}
}
}
}
return timers;
}
private XMLMapper createMapper(TimerServiceImpl timerService) {
final XMLMapper mapper = XMLMapper.Factory.create();
mapper.registerRootElement(new QName(EjbTimerXmlParser_1_0.NAMESPACE, EjbTimerXmlPersister.TIMERS), new EjbTimerXmlParser_1_0(timerService, factory, configuration, timerService.getTimedObjectInvoker().getValue().getClassLoader()));
return mapper;
}
private File fileName(String timedObjectId, String timerId) {
return new File(getDirectory(timedObjectId) + File.separator + timerId.replace(File.separator, "-") + ".xml");
}
/**
* Gets the directory for a given timed object, making sure it exists.
*
* @param timedObjectId The timed object
* @return The directory
*/
private String getDirectory(String timedObjectId) {
String dirName = directories.get(timedObjectId);
if (dirName == null) {
dirName = baseDir.getAbsolutePath() + File.separator + timedObjectId.replace(File.separator, "-");
File file = new File(dirName);
if (!file.exists()) {
if (!file.mkdirs()) {
ROOT_LOGGER.failToCreateDirectoryForPersistTimers(file);
}
}
directories.put(timedObjectId, dirName);
}
return dirName;
}
private final class PersistTransactionSynchronization implements Synchronization {
private final String transactionKey;
private final Lock lock;
private final boolean newTimer;
private volatile TimerImpl timer;
public PersistTransactionSynchronization(final Lock lock, final String transactionKey, final boolean newTimer) {
this.lock = lock;
this.transactionKey = transactionKey;
this.newTimer = newTimer;
}
@Override
public void beforeCompletion() {
//get the latest version of the entity
timer = (TimerImpl) transactionSynchronizationRegistry.getValue().getResource(transactionKey);
if (timer == null) {
return;
}
}
@Override
public void afterCompletion(final int status) {
doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (timer == null) {
return null;
}
try {
lock.lock();
if (status == Status.STATUS_COMMITTED) {
final Map<String, TimerImpl> map = getTimers(timer.getTimedObjectId(), timer.getTimerService());
if (timer.getState() == TimerState.CANCELED ||
timer.getState() == TimerState.EXPIRED) {
map.remove(timer.getId());
} else {
if (newTimer || map.containsKey(timer.getId())) {
//if an existing timer is not in the map it has been cancelled by another thread
map.put(timer.getId(), timer);
}
}
writeFile(timer);
}
} finally {
lock.unlock();
}
return null;
}
});
}
}
private void writeFile(TimerImpl timer) {
final File file = fileName(timer.getTimedObjectId(), timer.getId());
//if the timer is expired or cancelled delete the file
if (timer.getState() == TimerState.CANCELED ||
timer.getState() == TimerState.EXPIRED) {
if (file.exists()) {
file.delete();
}
return;
}
try {
FileOutputStream out = new FileOutputStream(file);
try {
XMLStreamWriter writer = XMLOutputFactory.newInstance().createXMLStreamWriter(out);
XMLMapper mapper = createMapper(timer.getTimerService());
mapper.deparseDocument(new EjbTimerXmlPersister(factory, configuration), Collections.singletonList(timer), writer);
writer.flush();
writer.close();
} finally {
safeClose(out);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public InjectedValue<TransactionManager> getTransactionManager() {
return transactionManager;
}
public InjectedValue<TransactionSynchronizationRegistry> getTransactionSynchronizationRegistry() {
return transactionSynchronizationRegistry;
}
public InjectedValue<ModuleLoader> getModuleLoader() {
return moduleLoader;
}
public InjectedValue<PathManager> getPathManager() {
return pathManager;
}
private void setIfSupported(final XMLInputFactory inputFactory, final String property, final Object value) {
if (inputFactory.isPropertySupported(property)) {
inputFactory.setProperty(property, value);
}
}
private static void safeClose(final Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
// ignore
}
}
}
public static XMLExtendedStreamWriter create(XMLStreamWriter writer) throws Exception {
// Use reflection to access package protected class FormattingXMLStreamWriter
// TODO: at some point the staxmapper API could be enhanced to make this unnecessary
Class<?> clazz = Class.forName("org.jboss.staxmapper.FormattingXMLStreamWriter");
Constructor<?> ctr = clazz.getConstructor(XMLStreamWriter.class);
ctr.setAccessible(true);
return (XMLExtendedStreamWriter) ctr.newInstance(new Object[]{writer});
}
}