/* * 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.i18n; import org.apache.avalon.framework.logger.AbstractLogEnabled; import org.apache.avalon.framework.service.ServiceManager; import org.apache.excalibur.source.Source; import org.apache.excalibur.source.SourceNotFoundException; import org.apache.excalibur.source.SourceResolver; import org.apache.excalibur.source.SourceValidity; import org.apache.excalibur.source.impl.validity.ExpiresValidity; import org.apache.cocoon.ResourceNotFoundException; import org.apache.cocoon.components.source.SourceUtil; import org.apache.cocoon.components.source.impl.validity.DelayedValidity; import org.apache.cocoon.xml.ParamSaxBuffer; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.Locator; import org.xml.sax.SAXException; import java.net.MalformedURLException; import java.util.Collections; import java.util.HashMap; import java.util.Locale; import java.util.Map; /** * Implementation of <code>Bundle</code> interface for XML resources. Represents a * single XML message bundle. * * <p> * XML format for this resource bundle implementation is the following: * <pre> * <catalogue xml:lang="en"> * <message key="key1">Message <br/> Value 1</message> * <message key="key2">Message <br/> Value 1</message> * ... * </catalogue> * </pre> * * <p> * Value can be any well formed XML snippet and it will be cached by the key specified * in the attribute <code>key</code>. Objects returned by this {@link Bundle} implementation * are instances of the {@link ParamSaxBuffer} class. * * <p> * If value for a key is not present in this bundle, parent bundle will be queried. * * @author <a href="mailto:dev@cocoon.apache.org">Apache Cocoon Team</a> * @version $Id$ */ public class XMLResourceBundle extends AbstractLogEnabled implements Bundle { /** * XML bundle root element name */ public static final String EL_CATALOGUE = "catalogue"; /** * XML bundle message element name */ public static final String EL_MESSAGE = "message"; /** * XML bundle message element's key attribute name */ public static final String AT_KEY = "key"; /** * Source URI of the bundle */ private String sourceURI; /** * Bundle validity */ private SourceValidity validity; /** * Locale of the bundle */ private Locale locale; /** * Parent of the current bundle */ protected Bundle parent; /** * Objects stored in the bundle */ protected Map values; /** * Processes XML bundle file and creates map of values */ private static class SAXContentHandler implements ContentHandler { private Map values; private int state; private String namespace; private ParamSaxBuffer buffer; public SAXContentHandler(Map values) { this.values = values; } public void setDocumentLocator(Locator arg0) { // Ignore } public void startDocument() throws SAXException { // Ignore } public void endDocument() throws SAXException { // Ignore } public void processingInstruction(String arg0, String arg1) throws SAXException { // Ignore } public void skippedEntity(String arg0) throws SAXException { // Ignore } public void startElement(String ns, String localName, String qName, Attributes atts) throws SAXException { switch (this.state) { case 0: // <i18n:catalogue> if (!"".equals(ns) && !I18nUtils.matchesI18nNamespace(ns)) { throw new SAXException("Root element <" + EL_CATALOGUE + "> must be non-namespaced or in i18n namespace."); } if (!EL_CATALOGUE.equals(localName)) { throw new SAXException("Root element must be <" + EL_CATALOGUE + ">."); } this.namespace = ns; this.state++; break; case 1: // <i18n:message> if (!EL_MESSAGE.equals(localName)) { throw new SAXException("<" + EL_CATALOGUE + "> must contain <" + EL_MESSAGE + "> elements only."); } if (!this.namespace.equals(ns)) { throw new SAXException("<" + EL_MESSAGE + "> element must be in '" + this.namespace + "' namespace."); } String key = atts.getValue(AT_KEY); if (key == null) { throw new SAXException("<" + EL_MESSAGE + "> must have '" + AT_KEY + "' attribute."); } this.buffer = new ParamSaxBuffer(); this.values.put(key, this.buffer); this.state++; break; case 2: this.buffer.startElement(ns, localName, qName, atts); break; default: throw new SAXException("Internal error: Invalid state"); } } public void endElement(String ns, String localName, String qName) throws SAXException { switch (this.state) { case 0: break; case 1: // </i18n:catalogue> this.state--; break; case 2: if (this.namespace.equals(ns) && EL_MESSAGE.equals(localName)) { // </i18n:message> this.buffer = null; this.state--; } else { this.buffer.endElement(ns, localName, qName); } break; default: throw new SAXException("Internal error: Invalid state"); } } public void startPrefixMapping(String prefix, String uri) throws SAXException { if (this.buffer != null) { this.buffer.startPrefixMapping(prefix, uri); } } public void endPrefixMapping(String prefix) throws SAXException { if (this.buffer != null) { this.buffer.endPrefixMapping(prefix); } } public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { if (this.buffer != null) { this.buffer.ignorableWhitespace(ch, start, length); } } public void characters(char[] ch, int start, int length) throws SAXException { if (this.buffer != null) { this.buffer.characters(ch, start, length); } } } /** * Construct a bundle. * @param sourceURI source URI of the XML bundle * @param locale locale * @param parent parent bundle of this bundle */ public XMLResourceBundle(String sourceURI, Locale locale, Bundle parent) { this.sourceURI = sourceURI; this.locale = locale; this.parent = parent; this.values = Collections.EMPTY_MAP; } /** * (Re)Loads the XML bundle if necessary, based on the source URI. * @return true if reloaded successfully */ protected boolean reload(SourceResolver resolver, long interval) { return reload(null, resolver, interval); } /** * (Re)Loads the XML bundle if necessary, based on the source URI. * @return true if reloaded successfully */ protected boolean reload(ServiceManager manager, SourceResolver resolver, long interval) { Source newSource = null; Map newValues; try { int valid = this.validity == null ? SourceValidity.INVALID : this.validity.isValid(); if (valid != SourceValidity.VALID) { // Saved validity is not valid, get new source and validity newSource = resolver.resolveURI(this.sourceURI); SourceValidity newValidity = newSource.getValidity(); if (valid == SourceValidity.INVALID || this.validity.isValid(newValidity) != SourceValidity.VALID) { newValues = new HashMap(); if (manager != null) { SourceUtil.toSAX(manager, newSource, null, new SAXContentHandler(newValues)); } else { SourceUtil.toSAX(newSource, null, new SAXContentHandler(newValues)); } synchronized (this) { // Update source validity and values if (interval > 0 && newValidity != null) { this.validity = new DelayedValidity(interval, newValidity); } else { this.validity = newValidity; } this.values = newValues; } } } // Success return true; } catch (MalformedURLException e) { getLogger().error("Bundle <" + this.sourceURI + "> not loaded: Invalid URI", e); newValues = Collections.EMPTY_MAP; } catch (ResourceNotFoundException e) { // FIXME: this damn SourceUtil converts SNFE to RNFE!!! if (getLogger().isInfoEnabled()) { if (newSource != null && !newSource.exists()) { // Nominal case where a bundle doesn't exist: log the message but not the exception getLogger().info("Bundle <" + sourceURI + "> not loaded: Source URI not found"); } else { // Log the exception getLogger().info("Bundle <" + sourceURI + "> not loaded: Source URI not found", e); } } newValues = Collections.EMPTY_MAP; } catch (SourceNotFoundException e) { if (getLogger().isInfoEnabled()) { if (newSource != null && !newSource.exists()) { // Nominal case where a bundle doesn't exist: log the message but not the exception getLogger().info("Bundle <" + sourceURI + "> not loaded: Source URI not found"); } else { // Log the exception getLogger().info("Bundle <" + sourceURI + "> not loaded: Source URI not found", e); } } newValues = Collections.EMPTY_MAP; } catch (SAXException e) { getLogger().error("Bundle <" + sourceURI + "> not loaded: Invalid XML", e); // Keep existing loaded values newValues = this.values; } catch (Exception e) { getLogger().error("Bundle <" + sourceURI + "> not loaded: Exception", e); // Keep existing loaded values newValues = this.values; } finally { if (newSource != null) { resolver.release(newSource); } } synchronized (this) { // Use expires validity to delay next reloading. if (interval > 0) { this.validity = new ExpiresValidity(interval); } else { this.validity = null; } this.values = newValues; } // Failure return false; } /** * Gets the locale of the bundle. * * @return the locale */ public Locale getLocale() { return this.locale; } /** * Gets the source URI of the bundle. * * @return the source URI */ public String getSourceURI() { return this.sourceURI; } /** * Gets the validity of the bundle. * * @return the validity */ public SourceValidity getValidity() { return this.validity; } /** * Sets the validity of the bundle. */ public void setValidity(SourceValidity validity) { this.validity = validity; } /** * Get an instance of the {@link ParamSaxBuffer} associated with the key. * * @param key the key * @return the value, or null if no value associated with the key. */ public Object getObject(String key) { if (key == null) { return null; } Object value = this.values.get(key); if (value != null) { return value; } if (this.parent != null) { return this.parent.getObject(key); } return null; } /** * Get a string representation of the value object by key. * * @param key the key * @return the string value, or null if no value associated with the key. */ public String getString(String key) { if (key == null) { return null; } Object value = this.values.get(key); if (value != null) { return value.toString(); } if (this.parent != null) { return this.parent.getString(key); } return null; } }