blob: bdd1f7df39a5d8e23d23fd44922f4eb8b077d0e3 [file] [log] [blame]
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you 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.apache.sling.scripting.sightly.impl.engine.compiled;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.scripting.SlingScriptHelper;
import org.apache.sling.commons.classloader.ClassLoaderWriter;
import org.apache.sling.commons.classloader.DynamicClassLoaderManager;
import org.apache.sling.commons.compiler.JavaCompiler;
import org.apache.sling.commons.compiler.Options;
import org.apache.sling.scripting.api.ScriptNameAware;
import org.apache.sling.scripting.api.resource.ScriptingResourceResolverProvider;
import org.apache.sling.scripting.sightly.SightlyException;
import org.apache.sling.scripting.sightly.compiler.CompilationResult;
import org.apache.sling.scripting.sightly.compiler.CompilationUnit;
import org.apache.sling.scripting.sightly.compiler.CompilerMessage;
import org.apache.sling.scripting.sightly.compiler.SightlyCompiler;
import org.apache.sling.scripting.sightly.impl.engine.ResourceBackedPojoChangeMonitor;
import org.apache.sling.scripting.sightly.impl.engine.SightlyCompiledScript;
import org.apache.sling.scripting.sightly.impl.engine.SightlyEngineConfiguration;
import org.apache.sling.scripting.sightly.impl.engine.SightlyScriptEngine;
import org.apache.sling.scripting.sightly.impl.utils.BindingsUtils;
import org.apache.sling.scripting.sightly.impl.utils.Patterns;
import org.apache.sling.scripting.sightly.impl.utils.ScriptUtils;
import org.apache.sling.scripting.sightly.java.compiler.GlobalShadowCheckBackendCompiler;
import org.apache.sling.scripting.sightly.java.compiler.JavaClassBackendCompiler;
import org.apache.sling.scripting.sightly.java.compiler.JavaEscapeUtils;
import org.apache.sling.scripting.sightly.render.RenderContext;
import org.apache.sling.scripting.sightly.render.RenderUnit;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferencePolicyOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component(
service = SlingHTLMasterCompiler.class
)
public class SlingHTLMasterCompiler {
private static final Logger LOGGER = LoggerFactory.getLogger(SlingHTLMasterCompiler.class);
@Reference(policyOption = ReferencePolicyOption.GREEDY)
private DynamicClassLoaderManager dynamicClassLoaderManager;
@Reference(policyOption = ReferencePolicyOption.GREEDY)
private ClassLoaderWriter classLoaderWriter;
@Reference(policyOption = ReferencePolicyOption.GREEDY)
private SightlyCompiler sightlyCompiler;
@Reference(policyOption = ReferencePolicyOption.GREEDY)
private JavaCompiler javaCompiler;
@Reference(policyOption = ReferencePolicyOption.GREEDY)
private ScriptingResourceResolverProvider scriptingResourceResolverProvider;
@Reference
private SightlyEngineConfiguration sightlyEngineConfiguration;
@Reference
private ResourceBackedPojoChangeMonitor resourceBackedPojoChangeMonitor;
private static final String NO_SCRIPT = "NO_SCRIPT";
private static final Pattern MANGLED_CHAR_PATTERN = Pattern.compile("(.*)(__[0-9a-f]{4}__)(.*)");
private static final char[] ambiguousSymbols = new char[]{'-', '_', '.'};
static final String SIGHTLY_CONFIG_FILE = "/sightly.config";
private final Map<String, Lock> compilationLocks = new HashMap<>();
private Options options;
@Activate
void activate() {
LOGGER.info("Activating {}", getClass().getName());
String version = System.getProperty("java.specification.version");
options = new Options();
options.put(Options.KEY_GENERATE_DEBUG_INFO, true);
options.put(Options.KEY_SOURCE_VERSION, version);
options.put(Options.KEY_TARGET_VERSION, version);
options.put(Options.KEY_CLASS_LOADER_WRITER, classLoaderWriter);
options.put(Options.KEY_FORCE_COMPILATION, true);
InputStream is;
boolean newVersion = true;
String versionInfo = null;
String newVersionString = sightlyEngineConfiguration.getEngineVersion();
try {
is = classLoaderWriter.getInputStream(SIGHTLY_CONFIG_FILE);
if (is != null) {
versionInfo = IOUtils.toString(is, "UTF-8");
if (newVersionString.equals(versionInfo)) {
newVersion = false;
} else {
LOGGER.info("Detected stale classes generated by Apache Sling Scripting HTL engine version {}.", versionInfo);
}
IOUtils.closeQuietly(is);
}
} catch (IOException e) {
// do nothing; if we didn't find any previous version information we're considering our version to be new
}
if (newVersion) {
OutputStream os = classLoaderWriter.getOutputStream(SIGHTLY_CONFIG_FILE);
try {
IOUtils.write(sightlyEngineConfiguration.getEngineVersion(), os, "UTF-8");
} catch (IOException e) {
// ignore
} finally {
IOUtils.closeQuietly(os);
}
String scratchFolder = sightlyEngineConfiguration.getScratchFolder();
boolean scratchFolderDeleted = classLoaderWriter.delete(scratchFolder);
if (scratchFolderDeleted) {
if (StringUtils.isNotEmpty(versionInfo)) {
LOGGER.info("Deleted stale classes generated by Apache Sling Scripting HTL engine version {}.", versionInfo);
}
}
}
sightlyCompiler = SightlyCompiler.withKnownExpressionOptions(sightlyEngineConfiguration.getAllowedExpressionOptions());
}
/**
* This method returns an Object instance based on a {@link Resource}-backed class that is either found through regular classloading
* mechanisms or on-the-fly compilation. In case the requested class does not denote a fully qualified class name, this service will
* try to find the class through Sling's resource resolution mechanism and compile the class on-the-fly if required.
*
* @param renderContext the render context
* @param className name of class to use for object instantiation
* @return object instance of the requested class or {@code null} if the specified class is not backed by a {@link Resource}
*/
public Object getResourceBackedUseObject(RenderContext renderContext, String className) {
LOGGER.debug("Attempting to load class {}.", className);
try {
if (className.contains(".")) {
Resource pojoResource = getPOJOFromFQCN(scriptingResourceResolverProvider.getRequestScopedResourceResolver(), className);
if (pojoResource != null) {
return getUseObjectAndRecompileIfNeeded(pojoResource);
}
} else {
Resource pojoResource = ScriptUtils.resolveScript(
scriptingResourceResolverProvider.getRequestScopedResourceResolver(),
renderContext,
className + ".java"
);
if (pojoResource != null) {
return getUseObjectAndRecompileIfNeeded(pojoResource);
}
}
} catch (Exception e) {
throw new SightlyException("Cannot obtain an instance for class " + className + ".", e);
}
return null;
}
public SightlyCompiledScript compileHTLScript(final SightlyScriptEngine engine,
final Reader script,
final ScriptContext scriptContext) throws ScriptException {
ClassLoader old = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(dynamicClassLoaderManager.getDynamicClassLoader());
try {
String sName = NO_SCRIPT;
if (script instanceof ScriptNameAware) {
sName = ((ScriptNameAware) script).getScriptName();
}
if (sName.equals(NO_SCRIPT)) {
sName = getScriptName(scriptContext);
}
final String scriptName = sName;
CompilationUnit compilationUnit = new CompilationUnit() {
@Override
public String getScriptName() {
return scriptName;
}
@Override
public Reader getScriptReader() {
return script;
}
};
GlobalShadowCheckBackendCompiler shadowCheckBackendCompiler = null;
SlingJavaImportsAnalyser importsAnalyser = new SlingJavaImportsAnalyser(scriptingResourceResolverProvider);
JavaClassBackendCompiler javaClassBackendCompiler = new JavaClassBackendCompiler(importsAnalyser);
if (scriptContext != null) {
Bindings bindings = scriptContext.getBindings(ScriptContext.ENGINE_SCOPE);
Set<String> globals = bindings.keySet();
shadowCheckBackendCompiler =
new GlobalShadowCheckBackendCompiler(javaClassBackendCompiler, globals);
}
CompilationResult result =
shadowCheckBackendCompiler == null ? sightlyCompiler.compile(compilationUnit, javaClassBackendCompiler) :
sightlyCompiler.compile(compilationUnit, shadowCheckBackendCompiler);
if (result.getWarnings().size() > 0) {
for (CompilerMessage warning : result.getWarnings()) {
LOGGER.warn("Script {} {}:{}: {}", warning.getScriptName(), warning.getLine(), warning.getColumn(),
warning.getMessage());
}
}
if (result.getErrors().size() > 0) {
CompilerMessage error = result.getErrors().get(0);
throw new ScriptException(error.getMessage(), error.getScriptName(), error.getLine(), error.getColumn());
}
SourceIdentifier sourceIdentifier = new SourceIdentifier(sightlyEngineConfiguration, scriptName);
String javaSourceCode = javaClassBackendCompiler.build(sourceIdentifier);
Object renderUnit = compileSource(sourceIdentifier, javaSourceCode);
if (renderUnit instanceof RenderUnit) {
return new SightlyCompiledScript(engine, (RenderUnit) renderUnit);
} else {
throw new SightlyException("Expected a RenderUnit.");
}
} finally {
Thread.currentThread().setContextClassLoader(old);
}
}
public ClassLoader getClassLoader() {
return classLoaderWriter.getClassLoader();
}
/**
* Given a {@code fullyQualifiedClassName} and optionally a sub-package that should be stripped ({@code slashSubpackage}), this
* method will try to locate a {@code Resource} in the repository that provides the source code for the Java class.
*
* @param resolver a resource resolver with access to the script paths
* @param fullyQualifiedClassName the FQCN
* @return the {@code Resource} backing the class, or {@code null} if one cannot be found
*/
Resource getPOJOFromFQCN(ResourceResolver resolver, String fullyQualifiedClassName) {
StringBuilder pathElements = new StringBuilder("/");
String[] classElements = StringUtils.split(fullyQualifiedClassName, '.');
for (int i = 0; i < classElements.length; i++) {
String classElem = classElements[i];
Matcher matcher = MANGLED_CHAR_PATTERN.matcher(classElem);
if (matcher.matches()) {
String group = matcher.group(2);
char unmangled = JavaEscapeUtils.unmangle(group);
classElem = classElem.replaceAll(group, Character.toString(unmangled));
while (matcher.find()) {
group = matcher.group(2);
unmangled = JavaEscapeUtils.unmangle(group);
classElem = classElem.replaceAll(group, Character.toString(unmangled));
}
} else {
int underscoreIndex = classElem.indexOf('_');
if (underscoreIndex > -1) {
if (underscoreIndex == classElem.length() - 1) {
classElem = classElem.substring(0, classElem.length() -1);
} else if (underscoreIndex == 0 && !Character.isJavaIdentifierStart(classElem.charAt(1))){
classElem = classElem.substring(1);
}
}
}
pathElements.append(classElem);
if (i < classElements.length - 1) {
pathElements.append("/");
}
}
Set<String> possiblePOJOPaths = getPossiblePojoPaths(pathElements.toString() + ".java");
for (String possiblePath : possiblePOJOPaths) {
Resource r = resolver.getResource(possiblePath);
if (r != null) {
return r;
}
}
return null;
}
/**
* For a JCR path obtained from expanding a generated class name this method generates all the alternative path names that can be
* obtained by expanding the mentioned class' name.
*
* @param originalPath one of the possible paths
* @return a {@link Set} containing all the alternative paths if symbol replacement was needed; otherwise the set will contain just
* the {@code originalPath}
*/
private static Set<String> getPossiblePojoPaths(String originalPath) {
Set<String> possiblePaths = new LinkedHashSet<>();
possiblePaths.add(originalPath);
Map<Integer, Character> chars = new HashMap<>();
for (char symbol : ambiguousSymbols) {
String pathCopy = originalPath.substring(0, originalPath.lastIndexOf("/"));
int actualIndex = 0;
boolean firstPass = true;
while (pathCopy.indexOf(symbol) != -1) {
int pos = pathCopy.indexOf(symbol);
actualIndex += pos;
if (!firstPass) {
actualIndex += 1;
}
chars.put(actualIndex, symbol);
pathCopy = pathCopy.substring(pos + 1);
firstPass = false;
}
}
if (chars.size() > 0) {
ArrayList<char[]> possibleArrangements = new ArrayList<>();
populateArray(possibleArrangements, new char[chars.size()], 0);
Integer[] indexes = chars.keySet().toArray(new Integer[0]);
for (char[] arrangement : possibleArrangements) {
char[] possiblePath = originalPath.toCharArray();
for (int i = 0; i < arrangement.length; i++) {
char currentSymbol = arrangement[i];
int currentIndex = indexes[i];
possiblePath[currentIndex] = currentSymbol;
}
possiblePaths.add(new String(possiblePath));
}
}
return possiblePaths;
}
/**
* Given an initial array with its size equal to the number of elements of a needed arrangement, this method will generate all
* the possible arrangements of values for this array in the provided {@code arrayCollection}. The values with which the array is
* populated are the {@link #ambiguousSymbols} characters.
*
* @param arrayCollection the collection that will store the arrays
* @param symbolsArrangementArray an initial array that will be used for collecting the results
* @param index the initial index of the array that will be populated (needed for recursion purposes; start with 0 for the initial call)
*/
private static void populateArray(ArrayList<char[]> arrayCollection, char[] symbolsArrangementArray, int
index) {
if (symbolsArrangementArray.length > 0) {
if (index == symbolsArrangementArray.length) {
arrayCollection.add(symbolsArrangementArray.clone());
} else {
for (char symbol : ambiguousSymbols) {
symbolsArrangementArray[index] = symbol;
populateArray(arrayCollection, symbolsArrangementArray, index + 1);
}
}
}
}
/**
* Compiles a class using the passed fully qualified class name and its source code.
*
* @param sourceIdentifier the source identifier
* @param sourceCode the source code from which to generate the class
* @return object instance of the class to compile
*/
private Object compileSource(SourceIdentifier sourceIdentifier, String sourceCode) {
Lock lock;
final String fqcn = sourceIdentifier.getFullyQualifiedClassName();
synchronized (compilationLocks) {
lock = compilationLocks.get(fqcn);
if (lock == null) {
lock = new ReentrantLock();
compilationLocks.put(fqcn, lock);
}
}
lock.lock();
try {
if (sightlyEngineConfiguration.keepGenerated()) {
String path = "/" + fqcn.replaceAll("\\.", "/") + ".java";
OutputStream os = classLoaderWriter.getOutputStream(path);
IOUtils.write(sourceCode, os, "UTF-8");
IOUtils.closeQuietly(os);
}
String[] sourceCodeLines = sourceCode.split("\\r\\n|[\\n\\x0B\\x0C\\r\\u0085\\u2028\\u2029]");
boolean foundPackageDeclaration = false;
for (String line : sourceCodeLines) {
Matcher matcher = Patterns.JAVA_PACKAGE_DECLARATION.matcher(line);
if (matcher.matches()) {
/*
* This matching might return false positives like:
* // package a.b.c;
*
* where from a syntactic point of view the source code doesn't have a package declaration and the expectancy is that our
* SightlyJavaCompilerService will add one.
*/
foundPackageDeclaration = true;
break;
}
}
if (!foundPackageDeclaration) {
sourceCode = "package " + sourceIdentifier.getPackageName() + ";\n" + sourceCode;
}
org.apache.sling.commons.compiler.CompilationUnit
compilationUnit = new SightlyCompilationUnit(sourceCode, fqcn);
long start = System.currentTimeMillis();
org.apache.sling.commons.compiler.CompilationResult
compilationResult = javaCompiler.compile(new org.apache.sling.commons.compiler.CompilationUnit[]{compilationUnit}, options);
long end = System.currentTimeMillis();
List<org.apache.sling.commons.compiler.CompilerMessage> errors = compilationResult.getErrors();
if (errors != null && errors.size() > 0) {
throw new SightlyException(createErrorMsg(errors));
}
if (compilationResult.didCompile()) {
LOGGER.debug("Class {} was compiled in {}ms.", fqcn, end - start);
}
/*
* the class loader might have become dirty, so let the {@link ClassLoaderWriter} decide which class loader to return
*/
return classLoaderWriter.getClassLoader().loadClass(fqcn).newInstance();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IOException e) {
throw new SightlyException(e);
} finally {
lock.unlock();
}
}
private Object getUseObjectAndRecompileIfNeeded(Resource pojoResource)
throws IOException, InstantiationException, IllegalAccessException, ClassNotFoundException {
SourceIdentifier sourceIdentifier = new SourceIdentifier(sightlyEngineConfiguration, pojoResource.getPath());
long sourceLastModifiedDateFromCache =
resourceBackedPojoChangeMonitor.getLastModifiedDateForJavaUseObject(pojoResource.getPath());
long classLastModifiedDate = classLoaderWriter.getLastModified("/" + sourceIdentifier.getFullyQualifiedClassName()
.replaceAll("\\.", "/") + ".class");
if (sourceLastModifiedDateFromCache == 0) {
// first access; let's check the real last modified date of the source
long sourceLastModifiedDate = pojoResource.getResourceMetadata().getModificationTime();
resourceBackedPojoChangeMonitor.recordLastModifiedTimestamp(pojoResource.getPath(), sourceLastModifiedDate);
if (classLastModifiedDate < 0 || sourceLastModifiedDate > classLastModifiedDate) {
return compileSource(sourceIdentifier, IOUtils.toString(pojoResource.adaptTo(InputStream.class), "UTF-8"));
} else {
return classLoaderWriter.getClassLoader().loadClass(sourceIdentifier.getFullyQualifiedClassName()).newInstance();
}
} else {
if (sourceLastModifiedDateFromCache > classLastModifiedDate) {
return compileSource(sourceIdentifier, IOUtils.toString(pojoResource.adaptTo(InputStream.class), "UTF-8"));
} else {
return classLoaderWriter.getClassLoader().loadClass(sourceIdentifier.getFullyQualifiedClassName()).newInstance();
}
}
}
//---------------------------------- private -----------------------------------
private String createErrorMsg(List<org.apache.sling.commons.compiler.CompilerMessage> errors) {
final StringBuilder buffer = new StringBuilder();
buffer.append("Compilation errors in ");
buffer.append(errors.get(0).getFile());
buffer.append(":");
StringBuilder errorsBuffer = new StringBuilder();
boolean duplicateVariable = false;
for (final org.apache.sling.commons.compiler.CompilerMessage e : errors) {
if (!duplicateVariable) {
if (e.getMessage().contains("Duplicate local variable")) {
duplicateVariable = true;
buffer.append(" Maybe you defined more than one identical block elements without defining a different variable for "
+ "each one?");
}
}
errorsBuffer.append("\nLine ");
errorsBuffer.append(e.getLine());
errorsBuffer.append(", column ");
errorsBuffer.append(e.getColumn());
errorsBuffer.append(" : ");
errorsBuffer.append(e.getMessage());
}
buffer.append(errorsBuffer);
return buffer.toString();
}
private String getScriptName(ScriptContext scriptContext) {
if (scriptContext != null) {
Bindings bindings = scriptContext.getBindings(ScriptContext.ENGINE_SCOPE);
String scriptName = (String) bindings.get(ScriptEngine.FILENAME);
if (scriptName != null && !"".equals(scriptName)) {
return scriptName;
}
SlingScriptHelper sling = BindingsUtils.getHelper(bindings);
if (sling != null) {
return sling.getScript().getScriptResource().getPath();
}
}
return NO_SCRIPT;
}
}