| /* |
| * 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.markup; |
| |
| import org.apache.avalon.framework.activity.Disposable; |
| import org.apache.avalon.framework.configuration.Configurable; |
| import org.apache.avalon.framework.configuration.Configuration; |
| import org.apache.avalon.framework.configuration.ConfigurationException; |
| import org.apache.avalon.framework.logger.AbstractLogEnabled; |
| import org.apache.avalon.framework.parameters.Parameters; |
| import org.apache.avalon.framework.service.ServiceException; |
| import org.apache.avalon.framework.service.ServiceManager; |
| import org.apache.avalon.framework.service.Serviceable; |
| |
| import org.apache.avalon.excalibur.pool.Recyclable; |
| import org.apache.excalibur.source.Source; |
| import org.apache.excalibur.source.SourceException; |
| import org.apache.excalibur.source.SourceResolver; |
| |
| import org.apache.cocoon.ProcessingException; |
| import org.apache.cocoon.xml.AbstractXMLPipe; |
| import org.apache.cocoon.components.language.programming.ProgrammingLanguage; |
| import org.apache.cocoon.components.source.SourceUtil; |
| import org.apache.excalibur.store.Store; |
| |
| import org.xml.sax.Attributes; |
| import org.xml.sax.SAXException; |
| |
| import java.io.IOException; |
| import java.net.MalformedURLException; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Base implementation of <code>MarkupLanguage</code>. This class uses |
| * logicsheets as the only means of code generation. Code generation |
| * should be decoupled from this context!!! |
| * |
| * @author <a href="mailto:ricardo@apache.org">Ricardo Rocha</a> |
| * @author <a href="mailto:dims@yahoo.com">Davanum Srinivas</a> |
| * @author <a href="mailto:ovidiu@cup.hp.com">Ovidiu Predescu</a> |
| * @version CVS $Id$ |
| */ |
| public abstract class AbstractMarkupLanguage |
| extends AbstractLogEnabled |
| implements MarkupLanguage, Serviceable, Configurable, Recyclable, Disposable |
| { |
| /** |
| * Name "attr-interpolation" of boolean attribute to enable |
| * expression interpolation in attribute values. |
| */ |
| public static final String ATTR_INTERPOLATION = "attr-interpolation"; |
| |
| /** |
| * Name "text-interpolation" of boolean attribute to enable |
| * expression interpolation inside text nodes. |
| */ |
| public static final String TEXT_INTERPOLATION = "text-interpolation"; |
| |
| /** The 'file' URL protocol. */ |
| private static final String FILE = "file:"; |
| |
| /** Prefix for cache keys to avoid name clash with the XSLTProcessor */ |
| private static final String CACHE_PREFIX = "logicsheet:"; |
| |
| /** This language name */ |
| protected String name; |
| |
| /** The supported language table */ |
| protected Map languages; |
| |
| /** The code-generation logicsheet cache */ |
| protected Store logicsheetCache; |
| |
| /** The markup language's namespace uri */ |
| private String uri; |
| |
| /** The markup language's namespace prefix */ |
| private String prefix; |
| |
| /** Are attribute expressions to be expanded? */ |
| private boolean attrInterpolation; |
| |
| /** Are text expressions to be expanded? */ |
| private boolean textInterpolation; |
| |
| /** The service manager */ |
| protected ServiceManager manager; |
| |
| /** The URL factory source resolver used to resolve URIs */ |
| private SourceResolver resolver; |
| |
| |
| /** |
| * Stores the list of logicsheets required by the currently |
| * loaded program. |
| */ |
| private final LinkedList logicSheetList = new LinkedList(); |
| |
| |
| /** The default constructor. */ |
| public AbstractMarkupLanguage() { |
| // Initialize language table |
| this.languages = new HashMap(); |
| } |
| |
| /** |
| * Process additional configuration. Load supported programming |
| * language definitions |
| * |
| * @param conf The language configuration |
| * @exception ConfigurationException If an error occurs loading logichseets |
| */ |
| public void configure(Configuration conf) throws ConfigurationException { |
| try { |
| this.name = conf.getAttribute("name"); |
| |
| // Cannot use Parameterizable because parameterize() is called |
| // after configure(), and <xsp-language> param's are already |
| // needed for processing logicsheet definitions. |
| Parameters params = Parameters.fromConfiguration(conf); |
| this.uri = params.getParameter("uri"); |
| this.prefix = params.getParameter("prefix", null); |
| this.attrInterpolation = |
| params.getParameterAsBoolean(ATTR_INTERPOLATION, false); |
| this.textInterpolation = |
| params.getParameterAsBoolean(TEXT_INTERPOLATION, false); |
| |
| // Set up each target-language |
| Configuration[] l = conf.getChildren("target-language"); |
| for (int i = 0; i < l.length; i++) { |
| LanguageDescriptor language = new LanguageDescriptor(); |
| language.setName(l[i].getAttribute("name")); |
| |
| // Create & Store the core logicsheet |
| Logicsheet logicsheet = createLogicsheet(l[i], false); |
| language.setLogicsheet(logicsheet.getSystemId()); |
| |
| // Set up each built-in logicsheet |
| Configuration[] n = l[i].getChildren("builtin-logicsheet"); |
| for (int j = 0; j < n.length; j++) { |
| // Create & Store the named logicsheets |
| NamedLogicsheet namedLogicsheet = |
| (NamedLogicsheet) createLogicsheet(n[j], true); |
| |
| language.addNamedLogicsheet( |
| namedLogicsheet.getURI(), |
| namedLogicsheet.getPrefix(), |
| namedLogicsheet.getSystemId()); |
| } |
| |
| this.languages.put(language.getName(), language); |
| } |
| } catch (Exception e) { |
| getLogger().warn("Configuration Error: " + e.getMessage(), e); |
| throw new ConfigurationException("AbstractMarkupLanguage: " |
| + e.getMessage(), e); |
| } |
| } |
| |
| /** |
| * Abstract out the Logicsheet creation. Handles both Named and regular logicsheets. |
| */ |
| private Logicsheet createLogicsheet(Configuration configuration, boolean named) |
| throws Exception |
| { |
| Parameters params = Parameters.fromConfiguration(configuration); |
| |
| Logicsheet logicsheet; |
| if (named) { |
| String location = params.getParameter("href", null); |
| String uri = params.getParameter("uri", null); |
| String prefix = params.getParameter("prefix", null); |
| |
| NamedLogicsheet namedLogicsheet |
| = new NamedLogicsheet(location, manager, |
| resolver, getLogicsheetFilter()); |
| namedLogicsheet.setURI(uri); |
| namedLogicsheet.setPrefix(prefix); |
| logicsheet = namedLogicsheet; |
| } else { |
| String location = params.getParameter("core-logicsheet", null); |
| logicsheet = new Logicsheet(location, manager, |
| resolver, getLogicsheetFilter()); |
| } |
| |
| logicsheet.enableLogging(getLogger()); |
| |
| String logicsheetName = logicsheet.getSystemId(); |
| logicsheetCache.store(CACHE_PREFIX + logicsheetName, logicsheet); |
| |
| return logicsheet; |
| } |
| |
| /** |
| * Set the global service manager. |
| * @param manager The sitemap-specified service manager |
| */ |
| public void service(ServiceManager manager) throws ServiceException { |
| this.manager = manager; |
| |
| // Initialize logicsheet cache |
| this.logicsheetCache = (Store) manager.lookup(Store.TRANSIENT_STORE); |
| |
| // Initialize the source resolver |
| this.resolver = (SourceResolver)this.manager.lookup(SourceResolver.ROLE); |
| } |
| |
| /** |
| * Recycle this component: clear logic sheet list and dependencies. |
| */ |
| public void recycle() { |
| this.logicSheetList.clear(); |
| } |
| |
| /** |
| * Release all resources. |
| */ |
| public void dispose() { |
| this.manager.release(this.logicsheetCache); |
| this.logicsheetCache = null; |
| |
| this.manager.release(this.resolver); |
| this.resolver = null; |
| this.manager = null; |
| this.languages.clear(); |
| } |
| |
| /** |
| * Return the markup language name. Two markup languages are |
| * well-know at the moment: sitemap and xsp. |
| * |
| * @return The language name. |
| */ |
| public String getName() { |
| return this.name; |
| } |
| |
| /** |
| * Returns the namespace URI for this language. |
| */ |
| public String getURI() { |
| return this.uri; |
| } |
| |
| /** |
| * Returns the namespace prefix for this language. |
| */ |
| public String getPrefix() { |
| return this.prefix; |
| } |
| |
| /** |
| * Returns true if expansion of attribute expressions is enabled |
| * for this language. |
| */ |
| public boolean hasAttrInterpolation() { |
| return this.attrInterpolation; |
| } |
| |
| /** |
| * Returns true if expansion of expressions inside text nodes is enabled |
| * for this language. |
| */ |
| public boolean hasTextInterpolation() { |
| return this.textInterpolation; |
| } |
| |
| /** |
| * Return the source document's encoding. This can be <code>null</code> for |
| * the platform's default encoding. The default implementation returns |
| * <code>null</code>, but derived classes may override it if encoding applies to |
| * their concrete languages. |
| * |
| * FIXME: There should be a way to get the |
| * XML document's encoding as seen by the parser; unfortunately, this |
| * information is not returned by current DOM or SAX parsers... |
| * |
| * @return The document-specified encoding |
| */ |
| public String getEncoding() { |
| return null; |
| } |
| |
| /** |
| * Returns a filter that chains on the fly the requested |
| * transformers for source code generation. This method scans the |
| * input SAX events for built-in logicsheet declared as namespace |
| * attribute on the root element. Derived class should overide |
| * this method and the public inner class in order to add more |
| * specif action and to build a more specific transformer chain. |
| * |
| * @param logicsheetMarkupGenerator the logicsheet markup generator |
| * @return XMLFilter the filter that build on the fly the transformer chain |
| */ |
| protected TransformerChainBuilderFilter getTransformerChainBuilder( |
| LogicsheetCodeGenerator logicsheetMarkupGenerator) |
| { |
| return new TransformerChainBuilderFilter(logicsheetMarkupGenerator); |
| } |
| |
| /** |
| * Prepare the input source for logicsheet processing and code |
| * generation with a preprocess filter. The return |
| * <code>XMLFilter</code> object is the first filter on the |
| * transformer chain. The default implementation does nothing by |
| * returning a identity filter, but derived classes should (at |
| * least) use the passed programming language to quote |
| * <code>Strings</code> |
| * |
| * @param filename The source filename |
| * @param language The target programming language |
| * @return The preprocess filter |
| */ |
| protected AbstractXMLPipe getPreprocessFilter(String filename, |
| AbstractXMLPipe filter, |
| ProgrammingLanguage language) |
| { |
| // No-op |
| return filter; |
| } |
| |
| /** |
| * Add a dependency on an external file to the document for inclusion in |
| * generated code. This is used to populate a list of <code>File</code>'s |
| * tested for change on each invocation; this information is used to assert whether regeneration is necessary. |
| * |
| * @param location The file path of the dependent file |
| * @see AbstractMarkupLanguage |
| * @see org.apache.cocoon.generation.ServerPagesGenerator |
| * @see org.apache.cocoon.generation.AbstractServerPage |
| */ |
| protected abstract void addDependency(String location); |
| |
| /** |
| * Generate source code from the input document for the target |
| * <code>ProgrammingLanguage</code>. After preprocessing the input |
| * document, this method applies logicsheets in the following |
| * order: |
| * |
| * <ul> |
| * <li>User-defined logicsheets</li> |
| * <li>Namespace-mapped logicsheets</li> |
| * <li>Language-specific logicsheet</li> |
| * </ul> |
| * |
| * @param source The input source |
| * @param filename The input document's original filename |
| * @param programmingLanguage The target programming language |
| * @return The generated source code |
| * @exception Exception If an error occurs during code generation |
| */ |
| public String generateCode(Source source, |
| String filename, |
| ProgrammingLanguage programmingLanguage) |
| throws Exception { |
| |
| String languageName = programmingLanguage.getLanguageName(); |
| LanguageDescriptor language = (LanguageDescriptor)this.languages.get(languageName); |
| if (language == null) { |
| throw new IllegalArgumentException("Unsupported programming language: " + languageName); |
| } |
| |
| // Create code generator |
| LogicsheetCodeGenerator codeGenerator = new LogicsheetCodeGenerator(); |
| codeGenerator.enableLogging(getLogger()); |
| codeGenerator.initialize(); |
| // Set the transformer chain builder filter |
| TransformerChainBuilderFilter tranBuilder = |
| getTransformerChainBuilder(codeGenerator); |
| tranBuilder.setLanguageDescriptor(language); |
| |
| // Get the needed preprocess filter |
| AbstractXMLPipe preprocessor = getPreprocessFilter(filename, tranBuilder, programmingLanguage); |
| return codeGenerator.generateCode(source, preprocessor); |
| } |
| |
| /** |
| * Add logicsheet list to the code generator. |
| * @param codeGenerator The code generator |
| */ |
| protected void addLogicsheetsToGenerator(LogicsheetCodeGenerator codeGenerator) |
| throws MalformedURLException, IOException, SAXException, ProcessingException { |
| |
| if (codeGenerator == null) { |
| getLogger().debug("This should never happen: codeGenerator is null"); |
| throw new SAXException("codeGenerator must never be null."); |
| } |
| |
| // Walk backwards and remove duplicates. |
| LinkedList newLogicSheetList = new LinkedList(); |
| for(int i = logicSheetList.size()-1; i>=0; i--) { |
| Logicsheet logicsheet = (Logicsheet) logicSheetList.get(i); |
| if(newLogicSheetList.indexOf(logicsheet) == -1) |
| newLogicSheetList.addFirst(logicsheet); |
| } |
| |
| // Add the list of logicsheets now. |
| Iterator iterator = newLogicSheetList.iterator(); |
| while(iterator.hasNext()) { |
| Logicsheet logicsheet = (Logicsheet) iterator.next(); |
| codeGenerator.addLogicsheet(logicsheet); |
| } |
| } |
| |
| /** |
| * Add a logicsheet to the code generator. |
| * @param language Target programming language of the logicsheet |
| * @param logicsheetLocation Location of the logicsheet to be added |
| * @exception MalformedURLException If location is invalid |
| * @exception IOException IO Error |
| * @exception SAXException Logicsheet parse error |
| */ |
| protected void addLogicsheetToList(LanguageDescriptor language, |
| String logicsheetLocation) |
| throws IOException, SAXException, ProcessingException |
| { |
| Source inputSource = null; |
| String logicsheetName; |
| try { |
| // Logicsheet is reusable (across multiple XSPs) object, |
| // and it is resolved via urlResolver, and not via per-request |
| // temporary resolver. |
| // Resolve logicsheet location relative to sitemap from where it is used. |
| inputSource = this.resolver.resolveURI(logicsheetLocation); |
| logicsheetName = inputSource.getURI(); |
| } catch (SourceException se) { |
| throw SourceUtil.handle(se); |
| } finally { |
| this.resolver.release( inputSource ); |
| } |
| |
| // Logicsheets are chained by looking at the namespaces on the xsl:stylesheet |
| // root node. To get at these namespaces, the stylesheet must be parsed. |
| // Stylesheets are cached that we have only one chance to fill the namespaces. |
| // To avoid a race condition, we have to lock the critical section. |
| // For maximum concurrency we lock the cache, store if necessary the new, |
| // unparsed logicsheet, and then lock the logicsheet for the long-running |
| // parse operation. |
| |
| Logicsheet logicsheet; |
| synchronized (logicsheetCache) { |
| String cacheKey = CACHE_PREFIX + logicsheetName; |
| logicsheet = (Logicsheet)logicsheetCache.get(cacheKey); |
| if (logicsheet == null) { |
| // Resolver (local) could not be used as it is temporary |
| // (per-request) object, yet Logicsheet is being cached and reused |
| // across multiple requests. "Global" url-factory-based resolver |
| // passed to the Logicsheet. |
| logicsheet = new Logicsheet(logicsheetName, manager, |
| resolver, getLogicsheetFilter()); |
| logicsheetCache.store(cacheKey, logicsheet); |
| } |
| } |
| |
| synchronized (logicsheet) { |
| Map namespaces = logicsheet.getNamespaceURIs(); |
| if (namespaces == null) |
| logicsheet.fillNamespaceURIs(); |
| } |
| |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("addLogicsheetToList: " |
| + "name: " + logicsheetName |
| + ", location: " + logicsheetLocation |
| + ", instance: " + logicsheet); |
| } |
| |
| if (logicsheetName.startsWith(FILE)) { |
| String filename = logicsheetName.substring(FILE.length()); |
| addDependency(filename); |
| getLogger().debug("addLogicsheetToList: " |
| + "adding dependency on file " + filename); |
| } |
| |
| logicSheetList.add(logicsheet); |
| |
| Map namespaces = logicsheet.getNamespaceURIs(); |
| if(!logicsheetLocation.equals(language.getLogicsheet())) { |
| if(namespaces.size() > 0) { |
| Iterator iter = namespaces.keySet().iterator(); |
| while(iter.hasNext()) { |
| String namespace = (String) iter.next(); |
| String namedLogicsheetName = language.getNamedLogicsheetByURI(namespace); |
| if(namedLogicsheetName!= null |
| && !logicsheetLocation.equals(namedLogicsheetName)) { |
| getLogger().debug("Adding embedded logic sheet for " |
| + namespace + ": " + namedLogicsheetName); |
| // Add embedded logic sheets too. |
| addLogicsheetToList(language, namedLogicsheetName); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Return the optional filter to prepocess logicsheets. |
| */ |
| protected LogicsheetFilter getLogicsheetFilter() { |
| return new LogicsheetFilter(); |
| } |
| |
| // |
| // Inner classes |
| // |
| |
| /** This class holds transient information about a target programming language. */ |
| protected static class LanguageDescriptor { |
| /** The progamming language name */ |
| protected String name; |
| |
| /** The progamming language core logicsheet */ |
| protected String logicsheet; |
| |
| /** The list of built-in logicsheets defined for this target language */ |
| protected HashMap namedLogicsheets; |
| |
| /** The default constructor */ |
| protected LanguageDescriptor() { |
| this.namedLogicsheets = new HashMap(); |
| } |
| |
| /** |
| * Set the programming language's name |
| * @param name The programming language's name |
| */ |
| protected void setName(String name) { |
| this.name = name; |
| } |
| |
| /** |
| * Return the programming language's name |
| * @return The programming language's name |
| */ |
| protected String getName() { |
| return this.name; |
| } |
| |
| /** |
| * Set the programming language's core logichseet location |
| * @param logicsheet The programming language's core logichseet location |
| */ |
| protected void setLogicsheet(String logicsheet) { |
| this.logicsheet = logicsheet; |
| } |
| |
| /** |
| * Return the programming language's core logichseet location |
| * @return The programming language's core logichseet location |
| */ |
| protected String getLogicsheet() { |
| return this.logicsheet; |
| } |
| |
| /** |
| * Add a namespace-mapped logicsheet to this language |
| * @param prefix The logichseet's namespace prefix |
| * @param uri The logichseet's namespace uri |
| * @param namedLogicsheet The logichseet's location |
| */ |
| protected void addNamedLogicsheet(String uri, String prefix, String namedLogicsheet) { |
| this.namedLogicsheets.put(uri, namedLogicsheet); |
| } |
| |
| /** |
| * Return a namespace-mapped logicsheet given its uri |
| * @return The namespace-mapped logicsheet |
| */ |
| protected String getNamedLogicsheetByURI(String uri) { |
| return (String)this.namedLogicsheets.get(uri); |
| } |
| } |
| |
| |
| /** |
| * An XMLFilter that build the chain of transformers on the fly. |
| * Each time a stylesheet is found, a call to the code generator is done |
| * to add the new transformer at the end of the current transformer chain. |
| */ |
| public class TransformerChainBuilderFilter extends AbstractXMLPipe { |
| /** The markup generator */ |
| protected LogicsheetCodeGenerator logicsheetMarkupGenerator; |
| |
| /** the language description */ |
| protected LanguageDescriptor language; |
| |
| private boolean isRootElem; |
| private List startPrefixes; |
| |
| /** |
| * the constructor depends on the code generator |
| * @param logicsheetMarkupGenerator The code generator |
| */ |
| protected TransformerChainBuilderFilter(LogicsheetCodeGenerator logicsheetMarkupGenerator) { |
| this.logicsheetMarkupGenerator = logicsheetMarkupGenerator; |
| } |
| |
| /** |
| * This method should be called prior to receiving any SAX event. |
| * Indeed the language information is needed to get the core stylesheet. |
| * @param language the language in used |
| */ |
| protected void setLanguageDescriptor(LanguageDescriptor language) { |
| this.language = language; |
| } |
| |
| /** @see org.xml.sax.ContentHandler */ |
| public void startDocument() throws SAXException { |
| isRootElem = true; |
| startPrefixes = new ArrayList(); |
| } |
| |
| /** @see org.xml.sax.ContentHandler */ |
| public void startPrefixMapping(String prefix, String uri) throws SAXException { |
| if (!isRootElem) { |
| super.startPrefixMapping(prefix, uri); |
| } else { |
| // Cache the prefix mapping |
| String[] prefixNamingArray = new String[2]; |
| prefixNamingArray[0] = prefix; |
| prefixNamingArray[1] = uri; |
| this.startPrefixes.add(prefixNamingArray); |
| } |
| } |
| |
| /** @see org.xml.sax.ContentHandler */ |
| public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { |
| if (isRootElem) { |
| isRootElem = false; |
| try { |
| // Add namespace-mapped logicsheets |
| int prefixesCount = this.startPrefixes.size(); |
| for (int i = 0; i < prefixesCount; i++) { |
| String[] prefixNamingArray = (String[]) this.startPrefixes.get(i); |
| String namedLogicsheetName = this.language.getNamedLogicsheetByURI(prefixNamingArray[1]); |
| if (namedLogicsheetName != null) { |
| AbstractMarkupLanguage.this.addLogicsheetToList(language, namedLogicsheetName); |
| } |
| } |
| |
| // Add the language stylesheet (Always the last one) |
| AbstractMarkupLanguage.this.addLogicsheetToList(language, this.language.getLogicsheet()); |
| AbstractMarkupLanguage.this.addLogicsheetsToGenerator(this.logicsheetMarkupGenerator); |
| } catch (ProcessingException pe) { |
| throw new SAXException (pe); |
| } catch (IOException ioe) { |
| throw new SAXException(ioe); |
| } |
| |
| // All stylesheet have been configured and correctly setup. |
| // Starts firing SAX events, especially the startDocument event, |
| // and the cached prefixNaming. |
| super.startDocument(); |
| int prefixesCount = this.startPrefixes.size(); |
| for (int i = 0; i < prefixesCount; i++) { |
| String[] prefixNamingArray = (String[]) this.startPrefixes.get(i); |
| super.startPrefixMapping(prefixNamingArray[0], prefixNamingArray[1]); |
| } |
| } |
| // Call super method |
| super.startElement(namespaceURI, localName, qName, atts); |
| } |
| } |
| } |