blob: 0827f9c52cf43e0c8fe42782005bb2e6303fc5ff [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;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.commons.classloader.ClassLoaderWriter;
import org.apache.sling.commons.compiler.CompilationResult;
import org.apache.sling.commons.compiler.CompilationUnit;
import org.apache.sling.commons.compiler.CompilerMessage;
import org.apache.sling.commons.compiler.JavaCompiler;
import org.apache.sling.commons.compiler.Options;
import org.apache.sling.scripting.api.resource.ScriptingResourceResolverProvider;
import org.apache.sling.scripting.sightly.SightlyException;
import org.apache.sling.scripting.sightly.impl.engine.compiled.SourceIdentifier;
import org.apache.sling.scripting.sightly.impl.utils.Patterns;
import org.apache.sling.scripting.sightly.impl.utils.ScriptUtils;
import org.apache.sling.scripting.sightly.render.RenderContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@code SightlyJavaCompiler} allows for simple instantiation of arbitrary classes that are either stored in the repository
* or in regular OSGi bundles. It also compiles Java sources on-the-fly and can discover class' source files based on
* {@link Resource}s (typically Sling components). It supports Sling Resource type inheritance.
*/
@Component(
service = SightlyJavaCompilerService.class
)
public class SightlyJavaCompilerService {
private static final Logger LOG = LoggerFactory.getLogger(SightlyJavaCompilerService.class);
@Reference
private ClassLoaderWriter classLoaderWriter = null;
@Reference
private JavaCompiler javaCompiler = null;
@Reference
private ResourceBackedPojoChangeMonitor resourceBackedPojoChangeMonitor = null;
@Reference
private SightlyEngineConfiguration sightlyEngineConfiguration = null;
@Reference
private ScriptingResourceResolverProvider scriptingResourceResolverProvider = null;
private Map<String, Lock> compilationLocks = new HashMap<>();
private Options options;
/**
* 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) {
LOG.debug("Attempting to load class {}.", className);
try {
if (className.contains(".")) {
Resource pojoResource =
SourceIdentifier.getPOJOFromFQCN(scriptingResourceResolverProvider.getRequestScopedResourceResolver()
, null, 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;
}
/**
* 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
*/
public 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;
}
CompilationUnit compilationUnit = new SightlyCompilationUnit(sourceCode, fqcn);
long start = System.currentTimeMillis();
CompilationResult compilationResult = javaCompiler.compile(new CompilationUnit[]{compilationUnit}, options);
long end = System.currentTimeMillis();
List<CompilerMessage> errors = compilationResult.getErrors();
if (errors != null && errors.size() > 0) {
throw new SightlyException(createErrorMsg(errors));
}
if (compilationResult.didCompile()) {
LOG.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 (Exception 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();
}
}
}
@Activate
protected void activate() {
LOG.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);
}
//---------------------------------- private -----------------------------------
private String createErrorMsg(List<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 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 static class SightlyCompilationUnit implements CompilationUnit {
private String fqcn;
private String sourceCode;
SightlyCompilationUnit(String sourceCode, String fqcn) throws Exception {
this.sourceCode = sourceCode;
this.fqcn = fqcn;
}
@Override
public Reader getSource() throws IOException {
return new InputStreamReader(IOUtils.toInputStream(sourceCode, "UTF-8"), "UTF-8");
}
@Override
public String getMainClassName() {
return fqcn;
}
@Override
public long getLastModified() {
return System.currentTimeMillis();
}
}
}