| /* |
| * 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.cocoon.components.language.generator; |
| |
| import org.apache.avalon.framework.activity.Disposable; |
| import org.apache.avalon.framework.component.ComponentException; |
| import org.apache.avalon.framework.component.ComponentManager; |
| import org.apache.avalon.framework.component.ComponentSelector; |
| import org.apache.avalon.framework.component.Composable; |
| import org.apache.avalon.framework.component.Recomposable; |
| import org.apache.avalon.framework.context.Context; |
| import org.apache.avalon.framework.context.ContextException; |
| import org.apache.avalon.framework.context.Contextualizable; |
| import org.apache.avalon.framework.logger.AbstractLogEnabled; |
| import org.apache.avalon.framework.parameters.ParameterException; |
| import org.apache.avalon.framework.parameters.Parameterizable; |
| import org.apache.avalon.framework.parameters.Parameters; |
| import org.apache.avalon.framework.thread.ThreadSafe; |
| import org.apache.cocoon.Constants; |
| import org.apache.cocoon.ProcessingException; |
| import org.apache.cocoon.components.classloader.ClassLoaderManager; |
| import org.apache.cocoon.components.language.LanguageException; |
| import org.apache.cocoon.components.language.markup.MarkupLanguage; |
| import org.apache.cocoon.components.language.programming.CodeFormatter; |
| import org.apache.cocoon.components.language.programming.Program; |
| import org.apache.cocoon.components.language.programming.ProgrammingLanguage; |
| import org.apache.cocoon.environment.SourceResolver; |
| import org.apache.cocoon.util.IOUtils; |
| import org.apache.excalibur.source.Source; |
| |
| import java.io.File; |
| import java.net.MalformedURLException; |
| |
| /** |
| * The default implementation of <code>ProgramGenerator</code> |
| * |
| * @author <a href="mailto:ricardo@apache.org">Ricardo Rocha</a> |
| * @author <a href="mailto:vgritsenko@apache.org">Vadim Gritsenko</a> |
| * @author <a href="mailto:tcurdt@apache.org">Torsten Curdt</a> |
| * @version CVS $Id$ |
| */ |
| public class ProgramGeneratorImpl extends AbstractLogEnabled |
| implements ProgramGenerator, Contextualizable, Composable, Parameterizable, |
| Disposable, ThreadSafe { |
| |
| /** The auto-reloading option */ |
| protected boolean autoReload = true; |
| |
| /** The pre-loading option */ |
| protected boolean preload = false; |
| |
| /** The check for manual source changes in the repository*/ |
| protected boolean watchSource = false; |
| |
| /** |
| * The ComponentSelector for programs. Caches Program by program |
| * source file. |
| */ |
| protected GeneratorSelector cache; |
| |
| /** The component manager */ |
| protected ComponentManager manager; |
| |
| /** The markup language component selector */ |
| protected ComponentSelector markupSelector; |
| |
| /** The programming language component selector */ |
| protected ComponentSelector languageSelector; |
| |
| /** The working directory */ |
| protected File workDir; |
| |
| /** The ClassLoaderManager */ |
| protected ClassLoaderManager classManager; |
| |
| /** The root package */ |
| protected String rootPackage; |
| |
| /** Servlet Context Directory */ |
| protected String contextDir; |
| |
| |
| /** Contextualize this class */ |
| public void contextualize(Context context) throws ContextException { |
| if (this.workDir == null) { |
| this.workDir = (File) context.get(Constants.CONTEXT_WORK_DIR); |
| } |
| |
| if (this.contextDir == null) { |
| org.apache.cocoon.environment.Context ctx = |
| (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); |
| |
| // Determine the context directory, preferably as a file |
| // FIXME (SW) - this is purposely redundant with some code in CocoonServlet |
| // to have the same rootPath. How to avoid this ? |
| try { |
| String rootPath = ctx.getRealPath("/"); |
| if (rootPath != null) { |
| this.contextDir = new File(rootPath).toURL().toExternalForm(); |
| } else { |
| String webInf = ctx.getResource("/WEB-INF").toExternalForm(); |
| this.contextDir = webInf.substring(0, webInf.length() - "WEB-INF".length()); |
| } |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Context directory is " + this.contextDir); |
| } |
| } catch (MalformedURLException e) { |
| getLogger().warn("Could not get context directory", e); |
| this.contextDir = ""; |
| } |
| } |
| } |
| |
| /** |
| * Set the global component manager. This method also sets the |
| * <code>ComponentSelector</code> used as language factory for both markup |
| * and programming languages. |
| * @param manager The global component manager |
| */ |
| public void compose(ComponentManager manager) throws ComponentException { |
| if (this.manager == null && manager != null) { |
| this.manager = manager; |
| this.cache = (GeneratorSelector) this.manager.lookup(GeneratorSelector.ROLE + "Selector"); |
| this.markupSelector = (ComponentSelector)this.manager.lookup(MarkupLanguage.ROLE + "Selector"); |
| this.languageSelector = (ComponentSelector)this.manager.lookup(ProgrammingLanguage.ROLE + "Selector"); |
| this.classManager = (ClassLoaderManager)this.manager.lookup(ClassLoaderManager.ROLE); |
| } |
| } |
| |
| /** |
| * Set the sitemap-provided configuration. This method sets the persistent code repository and the auto-reload option |
| * @param params The configuration information |
| * @exception ParameterException Not thrown here |
| */ |
| public void parameterize(Parameters params) throws ParameterException { |
| this.autoReload = params.getParameterAsBoolean("auto-reload", autoReload); |
| this.rootPackage = params.getParameter("root-package", "org.apache.cocoon.www"); |
| this.preload = params.getParameterAsBoolean("preload", preload); |
| this.watchSource = params.getParameterAsBoolean("watch-source", watchSource); |
| } |
| |
| /** |
| * Generates program source file name in the working directory |
| * from the source SystemID |
| */ |
| private String getNormalizedName(final String systemId) { |
| StringBuffer contextFilename = new StringBuffer(this.rootPackage.replace('.', File.separatorChar)); |
| contextFilename.append(File.separator); |
| if(systemId.startsWith(this.contextDir)) { |
| // VG: File is located under contextDir; using relative file name ... |
| contextFilename.append(systemId.substring(this.contextDir.length())); |
| } else { |
| // VG: File is located outside of contextDir; using systemId ... |
| contextFilename.append(systemId); |
| } |
| return IOUtils.normalizedFilename(contextFilename.toString()); |
| } |
| |
| /** |
| * Load a program built from an XML document written in a <code>MarkupLanguage</code> |
| * |
| * @param fileName The input document's <code>File</code> |
| * @param markupLanguageName The <code>MarkupLanguage</code> in which the input document is written |
| * @param programmingLanguageName The <code>ProgrammingLanguage</code> in which the program must be written |
| * @return The loaded program instance |
| * @exception Exception If an error occurs during generation or loading |
| * @deprecated Pass Source object instead of file name. |
| */ |
| public CompiledComponent load(ComponentManager newManager, |
| String fileName, |
| String markupLanguageName, |
| String programmingLanguageName, |
| SourceResolver resolver) |
| throws Exception { |
| |
| final Source source = resolver.resolveURI(fileName); |
| try { |
| return load(newManager, source, markupLanguageName, programmingLanguageName, resolver); |
| } finally { |
| resolver.release(source); |
| } |
| } |
| |
| /** |
| * Load a program built from an XML document written in a <code>MarkupLanguage</code>. |
| * |
| * This method does not releases passed source object. Caller of the method must release |
| * source when needed. |
| * |
| * @param source The input document's <code>File</code> |
| * @param markupLanguageName The <code>MarkupLanguage</code> in which the input document is written |
| * @param programmingLanguageName The <code>ProgrammingLanguage</code> in which the program must be written |
| * @return The loaded program instance |
| * @exception Exception If an error occurs during generation or loading |
| */ |
| public CompiledComponent load(ComponentManager newManager, |
| Source source, |
| String markupLanguageName, |
| String programmingLanguageName, |
| SourceResolver resolver) |
| throws Exception { |
| |
| final String id = source.getURI(); |
| |
| ProgrammingLanguage programmingLanguage = null; |
| MarkupLanguage markupLanguage = null; |
| try { |
| // Create file name for the program generated from the provided source. |
| final String normalizedName = getNormalizedName(id); |
| |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Loading serverpage systemId=[" + id + "]" + |
| " markupLanguageName=[" + markupLanguageName + "]" + |
| " programmingLanguageName=[" + programmingLanguageName + "]" + |
| " -> normalizedName=[" + normalizedName + "]"); |
| } |
| |
| markupLanguage = (MarkupLanguage) this.markupSelector.select(markupLanguageName); |
| programmingLanguage = (ProgrammingLanguage) this.languageSelector.select(programmingLanguageName); |
| programmingLanguage.setLanguageName(programmingLanguageName); |
| |
| Program program = null; |
| CompiledComponent programInstance = null; |
| |
| // Attempt to load program object from cache |
| try { |
| programInstance = (CompiledComponent) this.cache.select(normalizedName); |
| } catch (Exception e) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("The serverpage [" + id + "] is not in the cache yet"); |
| } |
| } |
| |
| if (programInstance == null && this.preload) { |
| // Preloading: Load program if its source/[object file] is available |
| try { |
| program = programmingLanguage.preload(normalizedName, |
| this.workDir, |
| markupLanguage.getEncoding()); |
| |
| this.cache.addGenerator(newManager, normalizedName, program); |
| programInstance = (CompiledComponent) this.cache.select(normalizedName); |
| |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Successfully preloaded serverpage [" + id + "]"); |
| } |
| } catch (Exception e) { |
| if (getLogger().isInfoEnabled()) { |
| getLogger().info("The serverpage [" + id |
| + "] could not be preloaded, will be re-created (" |
| + e + ")"); |
| } |
| } |
| } |
| |
| if (programInstance == null) { |
| synchronized (this) { |
| // Attempt again to load program object from cache. |
| // This avoids that simultaneous requests recompile |
| // the same XSP over and over again. |
| try { |
| programInstance = (CompiledComponent) this.cache.select(normalizedName); |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("The serverpage [" + id + "] was now in the cache"); |
| } |
| } catch (Exception e) { |
| // no instance found |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Creating new serverpage for [" + id + "]"); |
| } |
| generateSourcecode(source, |
| normalizedName, |
| markupLanguage, |
| programmingLanguage); |
| |
| programInstance = loadProgram(newManager, |
| normalizedName, |
| markupLanguage, |
| programmingLanguage); |
| } |
| } |
| } else { |
| // found an instance |
| if (this.autoReload) { |
| long sourceLastModified = source.getLastModified(); |
| // Has XSP changed? |
| // Note : lastModified can be 0 if source is dynamically generated. |
| // In that case, let the program instance decide if it is modified or not. |
| if (programInstance.modifiedSince(sourceLastModified)) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("ReCreating serverpage for [" + id + "]"); |
| } |
| synchronized (this) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Releasing old serverpage program [" + id + "]"); |
| } |
| release(programInstance); |
| programmingLanguage.unload(program, normalizedName, this.workDir); |
| this.cache.removeGenerator(normalizedName); |
| programInstance = null; |
| program = null; |
| |
| generateSourcecode(source, |
| normalizedName, |
| markupLanguage, |
| programmingLanguage); |
| |
| programInstance = loadProgram(newManager, |
| normalizedName, |
| markupLanguage, |
| programmingLanguage); |
| } |
| } else { |
| // check the repository for changes at all? |
| if (this.watchSource) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Checking sourcecode of [" + id + "] for a change"); |
| } |
| File sourcecodeFile = new File(this.workDir, |
| normalizedName + "." + programmingLanguage.getSourceExtension()); |
| // has sourcecode in repository changed ? |
| if (sourcecodeFile != null && sourcecodeFile.exists()) { |
| long sourcecodeLastModified = sourcecodeFile.lastModified(); |
| if (sourcecodeLastModified > sourceLastModified |
| || sourceLastModified == 0 |
| || sourcecodeLastModified == 0) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Create new serverpage program for [" + id + "] - repository has changed"); |
| } |
| synchronized (this) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Releasing old serverpage program [" + id + "]"); |
| } |
| release(programInstance); |
| //programmingLanguage.unload(program, normalizedName, this.workDir); |
| this.cache.removeGenerator(normalizedName); |
| programInstance = null; |
| program = null; |
| |
| programInstance = loadProgram(newManager, |
| normalizedName, |
| markupLanguage, |
| programmingLanguage); |
| } |
| } else { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Sourcecode of [" + id + "] has not changed - returning program from cache"); |
| } |
| } |
| } else { |
| if (getLogger().isErrorEnabled()) { |
| getLogger().error("Could not find sourcecode for [" + id + "]"); |
| } |
| } |
| } |
| } |
| } else { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Not checking for modifications [autoReload=false] - using current version"); |
| } |
| } |
| } |
| |
| // Recompose with the new manager if program needs it. |
| // This is required to provide XSP with manager from the correct |
| // sitemap so it will be able to find all components declared in |
| // the sitemap. |
| if (programInstance instanceof Recomposable) { |
| ((Recomposable) programInstance).recompose(newManager); |
| } |
| |
| return (programInstance); |
| } finally { |
| this.markupSelector.release(markupLanguage); |
| this.languageSelector.release(programmingLanguage); |
| } |
| } |
| |
| private CompiledComponent loadProgram(ComponentManager newManager, |
| String normalizedName, |
| MarkupLanguage markupLanguage, |
| ProgrammingLanguage programmingLanguage) |
| throws Exception { |
| |
| CompiledComponent programInstance = null; |
| |
| try { |
| return (CompiledComponent) this.cache.select(normalizedName); |
| } catch (Exception e) { |
| } |
| |
| try { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Loading program [" + normalizedName + "]"); |
| } |
| Program program = programmingLanguage.load(normalizedName, this.workDir, markupLanguage.getEncoding()); |
| |
| this.cache.addGenerator(newManager, normalizedName, program); |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Successfully loaded program [" + normalizedName + "]"); |
| } |
| } catch (LanguageException le) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Got Language Exception", le); |
| } |
| throw new ProcessingException("Language Exception", le); |
| } |
| |
| try { |
| programInstance = (CompiledComponent) this.cache.select(normalizedName); |
| } catch (Exception cme) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Can't load ServerPage: got exception", cme); |
| } |
| throw new ProcessingException("Can't load ServerPage", cme); |
| } |
| |
| return (programInstance); |
| } |
| |
| |
| private void generateSourcecode(Source source, |
| String normalizedName, |
| MarkupLanguage markupLanguage, |
| ProgrammingLanguage programmingLanguage) |
| throws Exception { |
| |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Creating sourcecode for [" + source.getURI() + "]"); |
| } |
| |
| // Generate code |
| String code = markupLanguage.generateCode(source, normalizedName, programmingLanguage); |
| if (code == null || code.length() == 0) { |
| // FIXME(VG): Xalan with incremental-processing=true does not propagate exceptions |
| // from working thread to main thread. See |
| // http://nagoya.apache.org/bugzilla/show_bug.cgi?id=8033 |
| throw new ProcessingException("Failed to generate program code (this may happen " + |
| "if you use Xalan in incremental processing mode). " + |
| "Please check log file and/or console for errors."); |
| } |
| |
| String encoding = markupLanguage.getEncoding(); |
| |
| // Format source code if applicable |
| CodeFormatter codeFormatter = programmingLanguage.getCodeFormatter(); |
| if (codeFormatter != null) { |
| code = codeFormatter.format(code, encoding); |
| } |
| |
| // Store generated code |
| final File sourceFile = new File(this.workDir, normalizedName + "." + programmingLanguage.getSourceExtension()); |
| final File sourceDir = sourceFile.getParentFile(); |
| if (sourceDir != null) { |
| sourceDir.mkdirs(); |
| } |
| IOUtils.serializeString(sourceFile, code); |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Successfully created sourcecode for [" + source.getURI() + "]"); |
| } |
| } |
| |
| /** |
| * Releases the program instance. |
| * @param component program instance to be released |
| */ |
| public void release(CompiledComponent component) { |
| this.cache.release(component); |
| } |
| |
| /** |
| * Removes named program from the program generator's cache. |
| * Disposes all created instances of the program. |
| * @param source of the program to be removed |
| */ |
| public void remove(Source source) { |
| final String normalizedName = getNormalizedName(source.getURI()); |
| this.cache.removeGenerator(normalizedName); |
| } |
| |
| /** |
| * dispose |
| */ |
| public void dispose() { |
| this.manager.release(this.cache); |
| this.cache = null; |
| this.manager.release(this.markupSelector); |
| this.markupSelector = null; |
| this.manager.release(this.languageSelector); |
| this.languageSelector = null; |
| this.manager.release(this.classManager); |
| this.classManager = null; |
| |
| this.manager = null; |
| |
| this.workDir = null; |
| this.contextDir = null; |
| } |
| } |