blob: 5accbda683224f5318a608fdc50cde24b1c0200e [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.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;
}
}