/*
* Copyright 2013 Martin Kouba
*
* 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.trimou.engine.locator;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.trimou.exception.MustacheException;
import org.trimou.exception.MustacheProblem;
import org.trimou.util.Files;
import org.trimou.util.ImmutableSet;
import org.trimou.util.ImmutableSet.ImmutableSetBuilder;
import org.trimou.util.Strings;
/**
* Classpath template locator. There is a special {@link Builder} for
* convenience.
* <p>
* Note that for a WAR archive, the classpath root is WEB-INF/classes (other
* parts of the archive are not available). Consider using
* <code>org.trimou.servlet.locator.ServletContextTemplateLocator</code> from
* the trimou-extension-servlet.
* <p>
* By default, the locator will attempt to scan the classpath to get all
* available template identifiers. However, only roots representing a directory
* on a filesystem will be processed. The scanning of JARs and other sources is
* not trivial. Moreover, it's legal for JARs to have no directory entries at
* all. Scanning can be entirely disabled - see also
* {@link Builder#setScanClasspath(boolean)}.
*
* @author Martin Kouba
*/
public class ClassPathTemplateLocator extends PathTemplateLocator<String> {
private static final Logger LOGGER = LoggerFactory
.getLogger(ClassPathTemplateLocator.class);
private final ClassLoader classLoader;
private final boolean scanClasspath;
/**
*
* @param priority
* @param rootPath
* If null, no templates will be available for precompilation
*/
public ClassPathTemplateLocator(int priority, String rootPath) {
this(priority, rootPath, null, null, true);
}
/**
*
* @param priority
* @param suffix
* If null, a full template name must be used
* @param rootPath
* If null, no templates will be available for precompilation
*/
public ClassPathTemplateLocator(int priority, String rootPath,
String suffix) {
this(priority, rootPath, suffix, null, true);
}
/**
*
* @param priority
* @param rootPath
* If null, no templates will be available for precompilation
* @param classLoader
* If null, use the TCCL or the CL of this class
*/
public ClassPathTemplateLocator(int priority, String rootPath,
ClassLoader classLoader) {
this(priority, rootPath, null, classLoader, true);
}
/**
*
* @param priority
* @param suffix
* If null, a full template name must be used
* @param rootPath
* If null, no templates will be available for precompilation
* @param classLoader
* If null, use the TCCL or the CL of this class
* @param scanClasspath
* If set to <code>true</code> the locator will attempt to scan
* the classpath to get all available template identifiers.
*/
public ClassPathTemplateLocator(int priority, String rootPath,
String suffix, ClassLoader classLoader, boolean scanClasspath) {
super(priority, rootPath, suffix);
if (classLoader == null) {
classLoader = SecurityActions.getContextClassLoader();
if (classLoader == null) {
classLoader = SecurityActions
.getClassLoader(ClassPathTemplateLocator.class);
}
}
this.classLoader = classLoader;
this.scanClasspath = scanClasspath;
}
@Override
public Reader locate(String templateId) {
return locateRealPath(toRealPath(templateId));
}
@Override
public Set<String> getAllIdentifiers() {
if (!scanClasspath || getRootPath() == null) {
return Collections.emptySet();
}
ImmutableSetBuilder<String> builder = ImmutableSet.builder();
try {
// Find all roots
Enumeration<URL> resources = classLoader
.getResources(getRootPath());
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
if (Strings.URL_PROCOTOL_FILE.equals(resource.getProtocol())) {
// Right now only files are supported
try {
File root = Paths.get(resource.toURI()).toFile();
if (!Files.isDirectoryUsable(root)) {
continue;
}
List<File> files = Files.listFiles(root, getSuffix());
if (!files.isEmpty()) {
for (File file : files) {
if (Files.isFileUsable(file)) {
String id = stripSuffix(
constructVirtualPath(root, file));
builder.add(id);
LOGGER.debug("Template available: {}", id);
}
}
}
} catch (URISyntaxException e) {
LOGGER.warn("Unable to process root path: {}", resource,
e);
}
} else {
LOGGER.debug(
"Protocol not supported - root resource is ignored: {}",
resource);
}
}
} catch (IOException e) {
throw new MustacheException(MustacheProblem.TEMPLATE_LOADING_ERROR,
e);
}
return builder.build();
}
@Override
protected String constructVirtualPath(String source) {
// Retain for backwards compatibility
throw new UnsupportedOperationException();
}
private Reader locateRealPath(String realPath) {
final String name = getRootPath() != null
? getRootPath() + addSuffix(realPath) : addSuffix(realPath);
Reader reader = null;
try {
Enumeration<URL> resources = classLoader.getResources(name);
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
if (reader != null) {
LOGGER.warn("Another/duplicit template for {} ignored: {}",
name, resource);
} else {
reader = new InputStreamReader(resource.openStream(),
getDefaultFileEncoding());
LOGGER.debug("Template {} located: {}", name, resource);
}
}
} catch (IOException e) {
throw new MustacheException(MustacheProblem.TEMPLATE_LOADING_ERROR,
e);
}
return reader;
}
private String constructVirtualPath(File root, File source) {
File parent = source.getParentFile();
List<String> parts = new ArrayList<>();
if (parent == null) {
throw new IllegalStateException(
"Unable to construct virtual path - no parent directory found");
}
parts.add(source.getName());
while (!root.equals(parent)) {
parts.add(parent.getName());
parent = parent.getParentFile();
}
Collections.reverse(parts);
StringBuilder name = new StringBuilder();
for (Iterator<String> iterator = parts.iterator(); iterator
.hasNext();) {
name.append(iterator.next());
if (iterator.hasNext()) {
name.append(getVirtualPathSeparator());
}
}
return name.toString();
}
/**
*
* @param priority
* @return a new instance of builder
*/
public static Builder builder() {
return new Builder();
}
/**
*
* @param priority
* @return a new instance of builder
*/
public static Builder builder(int priority) {
return new Builder().setPriority(priority);
}
/**
*
* @author Martin Kouba
*/
public static class Builder {
private ClassLoader classLoader;
private int priority;
private String rootPath;
private String suffix;
private boolean scanClasspath = true;
private Builder() {
this.priority = TemplateLocator.DEFAULT_PRIORITY;
}
/**
* @param priority
* the priority to set
*/
public Builder setPriority(int priority) {
this.priority = priority;
return this;
}
/**
* If not set, use the TCCL or the CL of this class.
*
* @param classLoader
* @return builder
*/
public Builder setClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
return this;
}
/**
* If not set, no templates will be available for precompilation.
*
* @param rootPath
* @return builder
*/
public Builder setRootPath(String rootPath) {
this.rootPath = rootPath;
return this;
}
/**
* If set to <code>true</code> the locator will attempt to scan the
* classpath to get all available template identifiers.
*
* @param scanClasspath
* @return builder
* @see TemplateLocator#getAllIdentifiers()
*/
public Builder setScanClasspath(boolean scanClasspath) {
this.scanClasspath = scanClasspath;
return this;
}
/**
* If not set, a full template name must be used.
*
* @param suffix
* @return builder
*/
public Builder setSuffix(String suffix) {
this.suffix = suffix;
return this;
}
public ClassPathTemplateLocator build() {
ClassLoader cl;
if (classLoader != null) {
cl = classLoader;
} else {
cl = SecurityActions.getContextClassLoader();
if (cl == null) {
cl = ClassPathTemplateLocator.class.getClassLoader();
}
}
return new ClassPathTemplateLocator(priority, rootPath, suffix, cl,
scanClasspath);
}
}
}