| /******************************************************************************* |
| * 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(); |
| } |
| } |
| } |