/**
* Copyright (C) 2013-2016 The Rythm Engine project
* for LICENSE and other details see:
* https://github.com/rythmengine/rythmengine
*/
package org.rythmengine.resource;
import org.rythmengine.RythmEngine;
import org.rythmengine.Sandbox;
import org.rythmengine.conf.RythmConfiguration;
import org.rythmengine.conf.RythmConfigurationKey;
import org.rythmengine.extension.ICodeType;
import org.rythmengine.extension.ITemplateResourceLoader;
import org.rythmengine.internal.RythmThreadFactory;
import org.rythmengine.internal.compiler.ParamTypeInferencer;
import org.rythmengine.internal.compiler.TemplateClass;
import org.rythmengine.logger.ILogger;
import org.rythmengine.logger.Logger;
import org.rythmengine.utils.S;
import java.io.File;
import java.net.URI;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
/**
* The template resource manager manages all template resource loaders and also cache the resource after they
* get loaded
*/
public class TemplateResourceManager {
@SuppressWarnings("unused")
private static final ILogger logger = Logger.get(TemplateResourceManager.class);
/**
* NULL TemplateResource
*/
@SuppressWarnings("serial")
public static final ITemplateResource NULL = new ITemplateResource() {
// this NULL TemplateResource can actually carry an error
// might not be necessary but is there in any case
private Throwable error;
@Override
public Object getKey() {
return null;
}
@Override
public String getSuggestedClassName() {
return null;
}
@Override
public String asTemplateContent() {
return null;
}
@Override
public boolean refresh() {
return false;
}
@Override
public boolean isValid() {
return false;
}
@Override
public ICodeType codeType(RythmEngine engine) {
return null;
}
@Override
public ITemplateResourceLoader getLoader() {
return null;
}
@Override
public Throwable getError() {
return error;
}
@Override
public void setError(Throwable error) {
this.error=error;
}
};
private RythmEngine engine;
private Map<Object, ITemplateResource> cache = new HashMap<Object, ITemplateResource>();
private List<ITemplateResourceLoader> loaders;
private FileResourceLoader adhocFileLoader = null;
// the <key, loader> map allows
private Map<Object, ITemplateResourceLoader> whichLoader = new HashMap<Object, ITemplateResourceLoader>();
private boolean typeInference;
/**
* Store the String that is NOT a resource
*/
private static Set<String> blackList = new HashSet<String>();
private static ThreadLocal<Stack<Set<String>>> tmpBlackList = new ThreadLocal<Stack<Set<String>>>() {
@Override
protected Stack<Set<String>> initialValue() {
return new Stack<Set<String>>();
}
};
public static void setUpTmpBlackList() {
tmpBlackList.get().push(new HashSet<String>());
}
public static void reportNonResource(String str) {
Stack<Set<String>> ss = tmpBlackList.get();
if (ss.isEmpty()) {
// invoked dynamically when running @invoke(...)
tmpBlackList.remove();
blackList.add(str);
} else {
ss.peek().add(str);
}
}
public static void commitTmpBlackList() {
Stack<Set<String>> sss = tmpBlackList.get();
if (!sss.isEmpty()) {
Set<String> ss = sss.pop();
blackList.addAll(ss);
}
if (sss.isEmpty()) {
tmpBlackList.remove();
}
}
public static void rollbackTmpBlackList() {
Stack<Set<String>> sss = tmpBlackList.get();
if (!sss.isEmpty()) {
sss.pop();
}
if (sss.isEmpty()) {
tmpBlackList.remove();
}
}
public static void cleanUpTmplBlackList() {
// Stack<Set<String>> ss = tmpBlackList.get();
// if (null != ss) {
// ss.clear();
// }
tmpBlackList.remove();
}
/**
* construct the TemplateResourceManager for the give engine
* @param engine
*/
public TemplateResourceManager(RythmEngine engine) {
this.engine = engine;
RythmConfiguration conf = engine.conf();
typeInference = conf.typeInferenceEnabled();
loaders = new ArrayList(conf.getList(RythmConfigurationKey.RESOURCE_LOADER_IMPLS, ITemplateResourceLoader.class));
if (!loaders.isEmpty()) {
for (ITemplateResourceLoader loader: loaders) {
loader.setEngine(this.engine);
}
Boolean defLoader = conf.get(RythmConfigurationKey.RESOURCE_DEF_LOADER_ENABLED);
if (!defLoader) {
return;
}
}
List<URI> roots = conf.templateHome();
for (URI root : roots) {
if (null == root) continue;
String scheme = root.getScheme();
if (S.eq(scheme, "jar")) {
String s = root.getSchemeSpecificPart();
int pos = s.indexOf(".jar!");
String home = s.substring(pos + 5);
ClasspathResourceLoader crl = new ClasspathResourceLoader(engine, home);
loaders.add(crl);
} else if (S.eq(scheme, "file")) {
FileResourceLoader frl = new FileResourceLoader(engine, new File(root.getPath()));
if (null == adhocFileLoader) {
adhocFileLoader = frl;
}
loaders.add(frl);
}
}
}
public void addResourceLoader(ITemplateResourceLoader loader) {
if (!loaders.contains(loader)) loaders.add(loader);
}
public void prependResourceLoader(ITemplateResourceLoader loader) {
if (!loaders.contains(loader)) loaders.add(0, loader);
}
private ITemplateResource cache(ITemplateResource resource) {
if (resource.isValid()) {
cache.put(resource.getKey(), resource);
}
return resource;
}
public TemplateClass tryLoadTemplate(String tmplName, TemplateClass callerClass, ICodeType codeType) {
if (blackList.contains(tmplName)) {
//logger.info(">>> %s is in the black list", tmplName);
return null;
}
TemplateClass tc = null;
RythmEngine engine = this.engine;
if (null != callerClass) {
ITemplateResourceLoader loader = whichLoader(callerClass.templateResource);
if (null != loader) {
return loader.tryLoadTemplate(tmplName, engine, callerClass, codeType);
}
}
for (ITemplateResourceLoader loader : loaders) {
tc = loader.tryLoadTemplate(tmplName, engine, callerClass, codeType);
if (null != tc) {
break;
}
}
return tc;
}
public ITemplateResource get(File file) {
return cache(new FileTemplateResource(file, adhocFileLoader));
}
public ITemplateResource get(String str) {
ITemplateResource resource = getResource(str);
if (!resource.isValid()) {
resource = new StringTemplateResource(str);
}
return cache(resource);
}
public ITemplateResourceLoader whichLoader(ITemplateResource resource) {
return whichLoader.get(resource.getKey());
}
public ITemplateResource getResource(String str) {
ITemplateResource resource = cache.get(str);
if (null != resource) return resource;
if (Sandbox.isRestricted()) return NULL;
for (ITemplateResourceLoader loader : loaders) {
resource = loader.load(str);
if (null != resource && resource.isValid()) {
whichLoader.put(resource.getKey(), loader);
break;
}
}
return null == resource ? NULL : cache(resource);
}
public void scan() {
for (ITemplateResourceLoader loader : loaders) {
loader.scan(this);
}
}
public void resourceLoaded(final ITemplateResource resource) {
resourceLoaded(resource, true);
}
public TemplateClass resourceLoaded(final ITemplateResource resource, boolean async) {
final ITemplateResourceLoader loader = resource.getLoader();
//if (!async) { no async load at the moment
if (true) {
whichLoader.put(resource.getKey(), loader);
return _resourceLoaded(resource);
} else {
loadingService.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
whichLoader.put(resource.getKey(), loader);
_resourceLoaded(resource);
return null;
}
});
return null;
}
}
private TemplateClass _resourceLoaded(ITemplateResource resource) {
if (!resource.isValid()) return null;
String key = S.str(resource.getKey());
if (typeInference) {
key += ParamTypeInferencer.uuid();
}
RythmEngine engine = this.engine;
TemplateClass tc = engine.classes().getByTemplate(key);
if (null == tc) {
tc = new TemplateClass(resource, engine);
}
tc.asTemplate(engine);
return tc;
}
private static class ScannerThreadFactory extends RythmThreadFactory {
private ScannerThreadFactory() {
super("rythm-scanner");
}
}
/*
* At the moment we don't support parsing templates in parallel, so ...
*/
private ScheduledExecutorService loadingService = new ScheduledThreadPoolExecutor(1, new ScannerThreadFactory());
public void shutdown() {
loadingService.shutdown();
}
}