/**
* Copyright 2012 Universitat Pompeu Fabra.
*
* 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.onexus.resource.manager.internal.providers;
import freemarker.core.TemplateElement;
import freemarker.template.*;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.io.filefilter.HiddenFileFilter;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
import org.apache.commons.io.monitor.FileAlterationMonitor;
import org.apache.commons.io.monitor.FileAlterationObserver;
import org.onexus.data.api.Data;
import org.onexus.resource.api.*;
import org.onexus.resource.api.exceptions.ResourceNotFoundException;
import org.onexus.resource.api.exceptions.UnserializeException;
import org.onexus.resource.manager.internal.PluginLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.tree.TreeNode;
import java.io.*;
import java.security.InvalidParameterException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public abstract class AbstractProjectProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractProjectProvider.class);
public static final String ONEXUS_EXTENSION = "onx";
public static final String ONEXUS_PROJECT_FILE = "onexus-project." + ONEXUS_EXTENSION;
private IResourceSerializer serializer;
private PluginLoader pluginLoader;
private Set<Long> bundleDependencies = new HashSet<Long>();
private Map<String, Set<File>> includeDependencies = new HashMap<String, Set<File>>();
private String projectName;
private String projectUrl;
private File projectFolder;
// FreeMarker
private Configuration freemarkerConfig = new Configuration();
private FileAlterationObserver observer;
private Project project;
private Properties projectAlias;
private Map<ORI, Resource> resources;
private List<IResourceListener> listeners = new ArrayList<IResourceListener>();
public AbstractProjectProvider(String projectName, String projectUrl, File projectFolder, FileAlterationMonitor monitor, List<IResourceListener> listeners) {
super();
this.projectName = projectName;
this.projectUrl = projectUrl;
this.projectFolder = projectFolder;
this.listeners = listeners;
// Don't watch hidden folders and files
IOFileFilter notHiddenDirectoryFilter = FileFilterUtils.notFileFilter(
FileFilterUtils.or(
FileFilterUtils.and(
FileFilterUtils.directoryFileFilter(),
HiddenFileFilter.HIDDEN
),
HiddenFileFilter.HIDDEN
)
);
String MONITOR_PROJECTS = System.getProperty("onexus.monitor.projects");
if (MONITOR_PROJECTS == null || Boolean.valueOf(MONITOR_PROJECTS)) {
observer = new FileAlterationObserver(projectFolder, notHiddenDirectoryFilter);
observer.addListener(new FileAlterationListenerAdaptor() {
@Override
public void onDirectoryCreate(File directory) {
LOGGER.info("Creating folder '" + directory.getName() + "'");
onResourceCreate(loadFile(directory));
}
@Override
public void onDirectoryDelete(File directory) {
LOGGER.info("Deleting folder '" + directory.getName() + "'");
ORI resourceOri = convertFileToORI(directory);
onResourceDelete(resources.remove(resourceOri));
}
@Override
public void onFileChange(File file) {
LOGGER.info("Reloading file '" + file.getName() + "'");
onResourceChange(loadFile(file));
}
@Override
public void onFileCreate(File file) {
LOGGER.info("Creating file '" + file.getName() + "'");
onResourceCreate(loadFile(file));
}
@Override
public void onFileDelete(File file) {
LOGGER.info("Deleting file '" + file.getName() + "'");
ORI resourceOri = convertFileToORI(file);
onResourceDelete(resources.remove(resourceOri));
}
});
monitor.addObserver(observer);
}
// Initialize template engine
try {
freemarkerConfig.setDirectoryForTemplateLoading(projectFolder);
} catch (IOException e) {
LOGGER.error("At template engine configuration. Project folder: '" + projectFolder + "'", e);
throw new RuntimeException(e);
}
freemarkerConfig.setObjectWrapper(new DefaultObjectWrapper());
freemarkerConfig.setDefaultEncoding("UTF-8");
freemarkerConfig.setTemplateExceptionHandler(TemplateExceptionHandler.HTML_DEBUG_HANDLER);
freemarkerConfig.setIncompatibleImprovements(new Version(2, 3, 20)); // FreeMarker 2.3.20
}
public Project getProject() {
if (project == null) {
loadProject();
}
return project;
}
public File getProjectFolder() {
return projectFolder;
}
protected void setProjectFolder(File projectFolder) {
this.projectFolder = projectFolder;
}
public synchronized void loadProject() {
File projectOnx = new File(projectFolder, ONEXUS_PROJECT_FILE);
if (!projectOnx.exists()) {
throw new InvalidParameterException("No Onexus project in " + projectFolder.getAbsolutePath());
}
this.project = (Project) loadResource(projectOnx);
this.project.setName(projectName);
this.bundleDependencies.clear();
if (project.getPlugins() != null) {
for (Plugin plugin : project.getPlugins()) {
//TODO Remove this property
if (plugin.getParameter("location") == null && plugin.getParameters() != null) {
plugin.getParameters().add(new Parameter("location", projectFolder.getAbsolutePath()));
}
try {
long bundleId = pluginLoader.load(plugin);
bundleDependencies.add(bundleId);
} catch (InvalidParameterException e) {
LOGGER.error(e.getMessage());
}
}
}
if (project.getAlias() != null && !project.getAlias().isEmpty()) {
this.projectAlias = new Properties();
try {
this.projectAlias.load(new StringReader(project.getAlias()));
} catch (IOException e) {
LOGGER.error("Malformed project alias", e);
this.projectAlias = null;
}
}
this.resources = null;
}
private synchronized void loadResources() {
this.resources = new ConcurrentHashMap<ORI, Resource>();
Collection<File> files = addFilesRecursive(new ArrayList<File>(), projectFolder);
for (File file : files) {
// Skip project file
if (ONEXUS_PROJECT_FILE.equals(file.getName())) {
continue;
}
Resource resource = loadResource(file);
if (resource != null) {
if (resources == null) {
return;
}
resources.put(resource.getORI(), resource);
}
}
}
public Resource getResource(ORI resourceUri) {
if (resourceUri.getPath() == null && projectUrl.equals(resourceUri.getProjectUrl())) {
return getProject();
}
if (resources == null) {
loadResources();
}
if (!resources.containsKey(resourceUri)) {
throw new ResourceNotFoundException(resourceUri);
}
return resources.get(resourceUri);
}
public <T extends Resource> List<T> getResourceChildren(IAuthorizationManager authorizationManager, Class<T> resourceType, ORI parentURI) {
if (resources == null) {
loadResources();
}
List<T> children = new ArrayList<T>();
if (resources != null) {
for (Resource resource : resources.values()) {
if (parentURI.isChild(resource.getORI())
&& resourceType.isAssignableFrom(resource.getClass())
&& authorizationManager.check(IAuthorizationManager.READ, resource.getORI())) {
children.add((T) resource);
}
}
}
return children;
}
public void syncProject() {
loadProject();
loadResources();
}
public void updateProject() {
importProject();
syncProject();
}
protected abstract void importProject();
private Resource loadResource(File resourceFile) {
Resource resource;
if (ONEXUS_EXTENSION.equalsIgnoreCase(FilenameUtils.getExtension(resourceFile.getName()))) {
StringWriter out = null;
try {
String relativePath = projectFolder.toURI().relativize(resourceFile.toURI()).getPath();
Template resourceTemplate = freemarkerConfig.getTemplate(relativePath);
out = new StringWriter((int) resourceFile.length() / 4);
resourceTemplate.process(projectAlias, out);
InputStream input = new ByteArrayInputStream(out.toString().getBytes("UTF-8"));
resource = serializer.unserialize(Resource.class, input);
for (String include : getIncludes(resourceTemplate)) {
addIncludeDependency(include, resourceFile);
}
} catch (FileNotFoundException e) {
resource = createErrorResource(resourceFile, "File '" + resourceFile.getPath() + "' not found.");
} catch (UnserializeException e) {
String msg = "Parsing file " + resourceFile.getPath() + " at line " + e.getLine() + " on " + e.getPath();
if (out != null) {
String[] lines = out.toString().split(System.getProperty("line.separator"));
int error = Integer.valueOf(e.getLine());
int from = (error-1 > 0 ? error-1 : 0);
int to = (error+1 < lines.length ? error+1 : lines.length);
for (int i=from ; i <= to; i++) {
msg = msg + "\n " + i + " >> " + lines[i];
}
}
resource = createErrorResource(resourceFile, msg);
} catch (Exception e) {
resource = createErrorResource(resourceFile, e.getMessage());
}
} else {
if (resourceFile.isDirectory()) {
resource = new Folder();
} else {
resource = createDataResource(resourceFile);
}
}
if (resource == null) {
return null;
}
resource.setORI(convertFileToORI(resourceFile));
return resource;
}
private ORI convertFileToORI(File file) {
String projectPath = projectFolder.getAbsolutePath() + File.separator;
String filePath = file.getAbsolutePath();
String relativePath = filePath.replace(projectPath, "");
if (relativePath.equals(ONEXUS_PROJECT_FILE)) {
relativePath = null;
} else {
relativePath = relativePath.replace("." + ONEXUS_EXTENSION, "");
}
return new ORI(projectUrl, relativePath);
}
private Resource createErrorResource(File resourceFile, String msg) {
LOGGER.error(msg);
Resource errorResource = createDataResource(resourceFile);
errorResource.setDescription("ERROR: " + msg);
return errorResource;
}
private Resource createDataResource(File resourceFile) {
Data data = new Data();
Loader loader = new Loader();
loader.setParameters(new ArrayList<Parameter>());
loader.getParameters().add(new Parameter("data-url", resourceFile.toURI().toString()));
data.setLoader(loader);
return data;
}
private static Collection<File> addFilesRecursive(Collection<File> files, File parentFolder) {
if (parentFolder.isDirectory()) {
File[] inFiles = parentFolder.listFiles();
if (inFiles != null) {
for (File file : inFiles) {
if (!file.isHidden()) {
files.add(file);
if (file.isDirectory()) {
addFilesRecursive(files, file);
}
}
}
}
}
return files;
}
public void save(Resource resource) {
if (resource == null) {
return;
}
if (resource instanceof Project) {
throw new IllegalArgumentException("Cannot create a project '" + resource.getORI() + "' inside project '" + projectUrl + "'");
}
String resourcePath = resource.getORI().getPath();
File file;
if (resource instanceof Folder) {
file = new File(projectFolder, resourcePath);
file.mkdirs();
return;
}
file = new File(projectFolder, resourcePath + "." + ONEXUS_EXTENSION);
try {
if (!file.exists()) {
file.createNewFile();
}
FileOutputStream os = new FileOutputStream(file);
serializer.serialize(resource, os);
os.close();
} catch (IOException e) {
LOGGER.error("Saving resource '" + resource.getORI() + "' in file '" + file.getAbsolutePath() + "'", e);
}
if (observer != null) {
observer.checkAndNotify();
}
}
public String getProjectUrl() {
return projectUrl;
}
public PluginLoader getPluginLoader() {
return pluginLoader;
}
public void setPluginLoader(PluginLoader pluginLoader) {
this.pluginLoader = pluginLoader;
}
public IResourceSerializer getSerializer() {
return serializer;
}
public void setSerializer(IResourceSerializer serializer) {
this.serializer = serializer;
}
protected void onResourceCreate(Resource resource) {
if (resource == null) {
return;
}
LOGGER.info("Resource '" + resource.getName() + "' created.");
for (IResourceListener listener : listeners) {
listener.onResourceCreate(resource);
}
}
protected void onResourceChange(Resource resource) {
if (resource == null) {
return;
}
LOGGER.info("Resource '" + resource.getName() + "' changed.");
for (IResourceListener listener : listeners) {
listener.onResourceChange(resource);
}
// If it's an include dependency reload parent resources
String resourcePath = resource.getORI().getPath().substring(1);
if (includeDependencies.containsKey(resourcePath)) {
freemarkerConfig.clearTemplateCache();
for (File parentResourceFile : includeDependencies.get(resourcePath)) {
onResourceChange(loadFile(parentResourceFile));
}
}
}
protected void onResourceDelete(Resource resource) {
if (resource == null) {
return;
}
LOGGER.info("Resource '" + resource.getName() + "' deleted.");
for (IResourceListener listener : listeners) {
listener.onResourceDelete(resource);
}
}
public boolean dependsOnBundle(long bundleId) {
return bundleDependencies.contains(Long.valueOf(bundleId));
}
private List<String> getIncludes(Template resourceTemplate) {
List<String> includes = new ArrayList<String>();
TemplateElement root = resourceTemplate.getRootTreeNode();
for (int i=0; i < root.getChildCount(); i++) {
TreeNode node = root.getChildAt(i);
if (node instanceof TemplateElement) {
String tag = ((TemplateElement) node).getCanonicalForm();
if (tag != null && tag.startsWith("<#include")) {
String include = tag.replace("<#include \"", "").replace("\"/>", "");
String templatePath = FilenameUtils.getFullPath(resourceTemplate.getName());
includes.add(templatePath + include);
try {
Template includeTemplate = freemarkerConfig.getTemplate(templatePath + include);
includes.addAll( getIncludes(includeTemplate));
} catch (IOException e) {
LOGGER.error(e.getMessage(), e);
}
}
}
}
return includes;
}
private void addIncludeDependency(String templateName, File parentResourceFile) {
if (!includeDependencies.containsKey(templateName)) {
includeDependencies.put(templateName, new HashSet<File>());
}
includeDependencies.get(templateName).add(parentResourceFile);
}
private Resource loadFile(File file) {
// Skip project file
if (ONEXUS_PROJECT_FILE.equals(file.getName())) {
return null;
}
Resource resource = loadResource(file);
if (resource != null) {
resources.put(resource.getORI(), resource);
}
return resource;
}
}