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