/*
* RED5 Open Source Flash Server - http://code.google.com/p/red5/
*
* Copyright 2006-2014 by respective authors (see below). All rights reserved.
*
* 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.red5.server.persistence;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.apache.mina.core.buffer.IoBuffer;
import org.red5.io.amf.Input;
import org.red5.io.amf.Output;
import org.red5.io.object.Deserializer;
import org.red5.server.api.IContext;
import org.red5.server.api.persistence.IPersistable;
import org.red5.server.api.scheduling.IScheduledJob;
import org.red5.server.api.scheduling.ISchedulingService;
import org.red5.server.api.scope.IScope;
import org.red5.server.net.servlet.ServletUtils;
import org.red5.server.so.SharedObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.web.context.support.ServletContextResource;
/**
* Simple file-based persistence for objects. Lowers memory usage if used instead of RAM memory storage.
*
* @author The Red5 Project
* @author Joachim Bauch (jojo@struktur.de)
*/
public class FilePersistence extends RamPersistence {
private Logger log = LoggerFactory.getLogger(FilePersistence.class);
/**
* Scheduler for persistence job.
*/
private ISchedulingService schedulingService;
/**
* Modified objects.
*/
private ConcurrentLinkedQueue<IPersistable> queue = new ConcurrentLinkedQueue<IPersistable>();
/**
* Files path
*/
private String path = "persistence";
/**
* Root directory under file storage path
*/
private String rootDir = "";
/**
* File extension for persistent objects
*/
private String extension = ".red5";
/**
* Whether there's need to check for empty directories
*/
private boolean checkForEmptyDirectories = true;
/**
* Interval to serialize modified objects in milliseconds.
*/
private int persistenceInterval = 10000;
/**
* Name of the job for serializing persistent objects.
*/
private String storeJobName;
/**
* Create file persistence object from given resource pattern resolver
* @param resolver Resource pattern resolver and loader
*/
public FilePersistence(ResourcePatternResolver resolver) {
super(resolver);
setPath(path);
}
/**
* Create file persistence object for given scope
* @param scope Scope
*/
public FilePersistence(IScope scope) {
super(scope);
setPath(path);
IContext ctx = scope.getContext();
if (ctx.hasBean(ISchedulingService.BEAN_NAME)) {
schedulingService = (ISchedulingService) ctx.getBean(ISchedulingService.BEAN_NAME);
} else {
//try the parent
schedulingService = (ISchedulingService) scope.getParent().getContext().getBean(ISchedulingService.BEAN_NAME);
}
// add the job
storeJobName = schedulingService.addScheduledJob(persistenceInterval, new FilePersistenceJob());
}
/**
* Returns the context path.
*
* @param rootFile
* @return context path
*/
private String getContextPath(Resource rootFile) {
String contextPath = null;
if (rootFile instanceof ServletContextResource) {
ServletContextResource servletResource = (ServletContextResource) rootFile;
contextPath = servletResource.getServletContext().getContextPath();
if ("/".equals(contextPath)) {
contextPath = "/root";
}
} else if (resources instanceof IScope) {
contextPath = ((IScope) resources).getContextPath();
if (contextPath == null) {
contextPath = "/root";
}
}
log.debug("Persistence context path: {}", contextPath);
return contextPath;
}
/**
* Initializes the root directory and creates it if it doesn't already exist.
*
* @param rootFile
* @param contextPath
* @throws IOException
*/
private void initRootDir(Resource rootFile, String contextPath) throws IOException {
if (rootFile instanceof ServletContextResource) {
rootDir = String.format("%s/webapps%s", System.getProperty("red5.root"), contextPath);
} else if (resources instanceof IScope) {
rootDir = String.format("%s%s", resources.getResource("/").getFile().getAbsolutePath(), contextPath);
}
log.debug("Persistence directory path: {}", rootDir);
File persistDir = new File(rootDir, path);
if (!persistDir.exists()) {
if (!persistDir.mkdirs()) {
log.warn("Persistence directory creation failed");
} else {
log.debug("Persistence directory access - read: {} write: {}", persistDir.canRead(), persistDir.canWrite());
}
} else {
log.debug("Persistence directory access - read: {} write: {}", persistDir.canRead(), persistDir.canWrite());
}
persistDir = null;
}
/**
* Setter for file path.
*
* @param path New path
*/
public void setPath(String path) {
log.debug("Set path: {}", path);
Resource rootFile = resources.getResource(path);
try {
log.debug("Absolute path: {}", resources.getResource("/").getFile().getAbsolutePath());
// check for existence
if (!rootFile.exists()) {
log.debug("Persistence directory does not exist");
// get context path
String contextPath = getContextPath(rootFile);
// int root dir
initRootDir(rootFile, contextPath);
} else {
rootDir = rootFile.getFile().getAbsolutePath();
}
log.debug("Root dir: {} path: {}", rootDir, path);
// set the path
this.path = path;
} catch (IOException err) {
log.error("I/O exception thrown when setting file path to {}", path, err);
throw new RuntimeException(err);
}
}
/**
* Setter for extension.
*
* @param extension New extension.
*/
public void setExtension(String extension) {
this.extension = extension;
}
/**
* @param checkForEmptyDirectories the checkForEmptyDirectories to set
*/
public void setCheckForEmptyDirectories(boolean checkForEmptyDirectories) {
this.checkForEmptyDirectories = checkForEmptyDirectories;
}
/**
* @return the persistenceInterval
*/
public int getPersistenceInterval() {
return persistenceInterval;
}
/**
* @param persistenceInterval the persistenceInterval to set
*/
public void setPersistenceInterval(int persistenceInterval) {
this.persistenceInterval = persistenceInterval;
}
/**
* Return file path for persistable object
* @param object Object to obtain file path for
* @return Path on disk
*/
private String getObjectFilepath(IPersistable object) {
return getObjectFilepath(object, false);
}
/**
* Return file path for persistable object
* @param object Object to obtain file path for
* @param completePath Whether full path should be returned
* @return Path on disk
*/
private String getObjectFilepath(IPersistable object, boolean completePath) {
StringBuilder result = new StringBuilder(path);
result.append('/');
result.append(object.getType());
result.append('/');
String objectPath = object.getPath();
log.debug("Object path: {}", objectPath);
result.append(objectPath);
if (!objectPath.endsWith("/")) {
result.append('/');
}
if (completePath) {
String name = object.getName();
log.debug("Object name: {}", name);
int pos = name.lastIndexOf('/');
if (pos >= 0) {
result.append(name.substring(0, pos));
}
}
//fix up path
int idx = -1;
if (File.separatorChar != '/') {
while ((idx = result.indexOf(File.separator)) != -1) {
result.deleteCharAt(idx);
result.insert(idx, '/');
}
}
//remove any './'
if ((idx = result.indexOf("./")) != -1) {
result.delete(idx, idx + 2);
}
//remove any '//'
while ((idx = result.indexOf("//")) != -1) {
result.deleteCharAt(idx);
}
if (log.isDebugEnabled()) {
log.debug("Adjusted object path: {}", result.toString());
}
return result.toString();
}
/** {@inheritDoc} */
@Override
protected String getObjectPath(String id, String name) {
if (id.startsWith(path)) {
id = id.substring(path.length() + 1);
}
return super.getObjectPath(id, name);
}
/**
* Get filename for persistable object
* @param object Persistable object
* @return Name of file where given object is persisted to
*/
private String getObjectFilename(IPersistable object) {
String path = getObjectFilepath(object);
String name = object.getName();
if (name == null) {
name = PERSISTENCE_NO_NAME;
}
return path + name + extension;
}
/**
* Load resource with given name
* @param name Resource name
* @return Persistable object
*/
private IPersistable doLoad(String name) {
return doLoad(name, null);
}
/**
* Load resource with given name and attaches to persistable object
* @param name Resource name
* @param object Object to attach to
* @return Persistable object
*/
private IPersistable doLoad(String name, IPersistable object) {
log.debug("doLoad - name: {} object: {}", name, object);
IPersistable result = object;
// if (log.isTraceEnabled()) {
// try {
// log.trace("Relative #1: {}", (resources.getResource(name) != null ? resources.getResource(name).getFile().getAbsolutePath() : "Not found"));
// log.trace("Absolute #2: {}", (resources.getResource("file://" + rootDir + '/' + name) != null ? resources.getResource("file://" + rootDir + '/' + name).getFile()
// .getAbsolutePath() : "Not found"));
// } catch (IOException e) {
// log.warn("", e);
// }
// }
Resource data = resources.getResource(name);
if (data == null || !data.exists()) {
// no such file
log.debug("Resource / data was not found");
// try again with full path
data = resources.getResource("file://" + rootDir + '/' + name);
if (data == null || !data.exists()) {
log.debug("Resource / data was not found (full path)");
return null;
}
}
FileInputStream input;
String filename;
try {
File fp = data.getFile();
if (fp.length() == 0) {
// File is empty
log.error("The file at {} is empty", data.getFilename());
return null;
}
filename = fp.getAbsolutePath();
input = new FileInputStream(filename);
} catch (FileNotFoundException e) {
log.error("The file at {} does not exist", data.getFilename());
return null;
} catch (IOException e) {
log.error("Could not load file from {}", data.getFilename(), e);
return null;
}
try {
IoBuffer buf = IoBuffer.allocate(input.available());
try {
ServletUtils.copy(input, buf.asOutputStream());
buf.flip();
Input in = new Input(buf);
String className = Deserializer.deserialize(in, String.class);
if (result == null) {
// we need to create the object first
try {
Class<?> theClass = Class.forName(className);
Constructor<?> constructor = null;
try {
// try to create object by calling constructor with Input stream as parameter
for (Class<?> interfaceClass : in.getClass().getInterfaces()) {
constructor = theClass.getConstructor(new Class[] { interfaceClass });
if (constructor != null) {
break;
}
}
if (constructor == null) {
throw new NoSuchMethodException();
}
result = (IPersistable) constructor.newInstance(in);
} catch (NoSuchMethodException err) {
// no valid constructor found, use empty constructor
result = (IPersistable) theClass.newInstance();
result.deserialize(in);
} catch (InvocationTargetException err) {
// error while invoking found constructor, use empty constructor
result = (IPersistable) theClass.newInstance();
result.deserialize(in);
}
} catch (ClassNotFoundException cnfe) {
log.error("Unknown class {}", className);
return null;
} catch (IllegalAccessException iae) {
log.error("Illegal access", iae);
return null;
} catch (InstantiationException ie) {
log.error("Could not instantiate class {}", className);
return null;
}
// set object's properties
log.debug("Name (after load): {}", result.getName());
result.setPath(getObjectPath(name, result.getName()));
} else {
// Initialize existing object
String resultClass = result.getClass().getName();
if (!resultClass.equals(className)) {
log.error("The classes differ: {} != {}", resultClass, className);
return null;
}
result.deserialize(in);
}
} finally {
buf.free();
buf = null;
}
if (result.getStore() != this) {
result.setStore(this);
}
super.save(result);
log.debug("Loaded persistent object {} from {}", result, filename);
} catch (IOException e) {
log.error("Could not load file at {}", filename);
return null;
}
return result;
}
/** {@inheritDoc} */
@Override
public IPersistable load(String name) {
log.debug("load - name: {}", name);
IPersistable result = super.load(name);
if (result != null) {
// Object has already been loaded
return result;
}
return doLoad(path + '/' + name + extension);
}
/** {@inheritDoc} */
@Override
public boolean load(IPersistable object) {
log.debug("load - name: {}", object);
if (object.isPersistent()) {
// already loaded
return true;
}
return (doLoad(getObjectFilename(object), object) != null);
}
/**
* Save persistable object
* @param object Persistable object
* @return <code>true</code> on success, <code>false</code> otherwise
*/
protected boolean saveObject(IPersistable object) {
log.debug("saveObject - object: {}", object);
boolean result = true;
String path = getObjectFilepath(object, true);
log.trace("Path: {}", path);
Resource resPath = resources.getResource(path);
boolean exists = resPath.exists();
log.debug("Resource (relative dir) exists: {}", exists);
File dir = null;
try {
if (!exists) {
resPath = resources.getResource("classpath:" + path);
exists = resPath.exists();
log.debug("Resource (classpath dir) exists: {}", exists);
if (!exists) {
StringBuilder root = new StringBuilder(rootDir);
//fix up path
int idx = -1;
if (File.separatorChar != '/') {
while ((idx = root.indexOf(File.separator)) != -1) {
root.deleteCharAt(idx);
root.insert(idx, '/');
}
}
resPath = resources.getResource("file://" + root.toString() + File.separatorChar + path);
exists = resPath.exists();
log.debug("Resource (absolute dir) exists: {}", exists);
}
}
dir = resPath.getFile();
log.debug("Resulting absolute path: {}", dir.getAbsolutePath());
if (!dir.isDirectory() && !dir.mkdirs()) {
log.error("Could not create directory {}", dir.getAbsolutePath());
result = false;
}
} catch (IOException err) {
log.error("Could not create resource file for path {}", path, err);
result = false;
}
//if we made it this far and everything seems ok
if (result) {
// if it's a persistent SharedObject and it's empty don't write it to disk. APPSERVER-364
if (object instanceof SharedObject) {
SharedObject soRef = (SharedObject) object;
if (soRef.getAttributes().size() == 0) {
// return true to trick the server into thinking everything is just fine :P
return true;
}
}
String filename = getObjectFilename(object);
log.debug("File name: {}", filename);
//strip path
if (filename.indexOf('/') != -1) {
filename = filename.substring(filename.lastIndexOf('/'));
log.debug("New file name: {}", filename);
}
File file = new File(dir, filename);
//Resource resFile = resources.getResource(filename);
//log.debug("Resource (file) check #1 - file name: {} exists: {}", resPath.getFilename(), exists);
IoBuffer buf = null;
try {
int initialSize = 8192;
if (file.exists()) {
// We likely also need the original file size when writing object
initialSize += (int) file.length();
}
buf = IoBuffer.allocate(initialSize);
buf.setAutoExpand(true);
Output out = new Output(buf);
out.writeString(object.getClass().getName());
object.serialize(out);
buf.flip();
FileOutputStream output = new FileOutputStream(file.getAbsolutePath());
ServletUtils.copy(buf.asInputStream(), output);
output.close();
log.debug("Stored persistent object {} at {}", object, filename);
} catch (IOException e) {
log.error("Could not create / write file {}", filename, e);
log.warn("Exception {}", e);
result = false;
} finally {
if (buf != null) {
buf.free();
buf = null;
}
file = null;
dir = null;
}
}
return result;
}
/** {@inheritDoc} */
@Override
public boolean save(IPersistable object) {
if (super.save(object)) {
return queue.add(object);
}
return false;
}
/**
* Remove empty dirs
* @param base Base directory
*/
protected void checkRemoveEmptyDirectories(String base) {
if (checkForEmptyDirectories) {
String dir;
Resource resFile = resources.getResource(base.substring(0, base.lastIndexOf('/')));
try {
dir = resFile.getFile().getAbsolutePath();
} catch (IOException err) {
return;
}
while (!dir.equals(rootDir)) {
File fp = new File(dir);
if (!fp.isDirectory()) {
// This should never happen
break;
}
if (fp.list().length != 0) {
// Directory is not empty
break;
}
if (!fp.delete()) {
// Could not remove directory
break;
}
// Move up one directory
dir = fp.getParent();
}
}
}
/** {@inheritDoc} */
@Override
public boolean remove(String name) {
super.remove(name);
boolean result = true;
String filename = path + '/' + name + extension;
Resource resFile = resources.getResource(filename);
if (resFile.exists()) {
try {
result = resFile.getFile().delete();
if (result) {
checkRemoveEmptyDirectories(filename);
}
} catch (IOException err) {
result = false;
}
}
return result;
}
/** {@inheritDoc} */
@Override
public boolean remove(IPersistable object) {
return remove(getObjectId(object));
}
/** {@inheritDoc} */
@Override
public void notifyClose() {
// stop the job
if (storeJobName != null) {
schedulingService.removeScheduledJob(storeJobName);
storeJobName = null;
}
// write any pending objects
persist();
//
super.notifyClose();
}
private void persist() {
IPersistable persistable = null;
while (!queue.isEmpty()) {
try {
persistable = queue.poll();
if (!saveObject(persistable)) {
log.warn("Object persist failed for: {}", persistable);
}
} catch (Throwable e) {
log.error("Error while saving {} in {}. {}", new Object[] { persistable, this, e });
}
}
}
private final class FilePersistenceJob implements IScheduledJob {
public void execute(ISchedulingService svc) {
persist();
}
}
}