001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.i18n;
018:
019: import org.apache.avalon.framework.logger.AbstractLogEnabled;
020: import org.apache.excalibur.source.Source;
021: import org.apache.excalibur.source.SourceNotFoundException;
022: import org.apache.excalibur.source.SourceResolver;
023: import org.apache.excalibur.source.SourceValidity;
024: import org.apache.excalibur.source.impl.validity.ExpiresValidity;
025:
026: import org.apache.cocoon.ResourceNotFoundException;
027: import org.apache.cocoon.components.source.SourceUtil;
028: import org.apache.cocoon.components.source.impl.validity.DelayedValidity;
029: import org.apache.cocoon.xml.ParamSaxBuffer;
030:
031: import org.xml.sax.Attributes;
032: import org.xml.sax.ContentHandler;
033: import org.xml.sax.Locator;
034: import org.xml.sax.SAXException;
035:
036: import java.net.MalformedURLException;
037: import java.util.Collections;
038: import java.util.HashMap;
039: import java.util.Locale;
040: import java.util.Map;
041:
042: /**
043: * Implementation of <code>Bundle</code> interface for XML resources. Represents a
044: * single XML message bundle.
045: *
046: * <p>
047: * XML format for this resource bundle implementation is the following:
048: * <pre>
049: * <catalogue xml:lang="en">
050: * <message key="key1">Message <br/> Value 1</message>
051: * <message key="key2">Message <br/> Value 1</message>
052: * ...
053: * </catalogue>
054: * </pre>
055: *
056: * <p>
057: * Value can be any well formed XML snippet and it will be cached by the key specified
058: * in the attribute <code>key</code>. Objects returned by this {@link Bundle} implementation
059: * are instances of the {@link ParamSaxBuffer} class.
060: *
061: * <p>
062: * If value for a key is not present in this bundle, parent bundle will be queried.
063: *
064: * @author <a href="mailto:dev@cocoon.apache.org">Apache Cocoon Team</a>
065: * @version $Id: XMLResourceBundle.java 433543 2006-08-22 06:22:54Z crossley $
066: */
067: public class XMLResourceBundle extends AbstractLogEnabled implements
068: Bundle {
069:
070: /**
071: * XML bundle root element name
072: */
073: public static final String EL_CATALOGUE = "catalogue";
074:
075: /**
076: * XML bundle message element name
077: */
078: public static final String EL_MESSAGE = "message";
079:
080: /**
081: * XML bundle message element's key attribute name
082: */
083: public static final String AT_KEY = "key";
084:
085: /**
086: * Source URI of the bundle
087: */
088: private String sourceURI;
089:
090: /**
091: * Bundle validity
092: */
093: private SourceValidity validity;
094:
095: /**
096: * Locale of the bundle
097: */
098: private Locale locale;
099:
100: /**
101: * Parent of the current bundle
102: */
103: protected Bundle parent;
104:
105: /**
106: * Objects stored in the bundle
107: */
108: protected Map values;
109:
110: /**
111: * Processes XML bundle file and creates map of values
112: */
113: private static class SAXContentHandler implements ContentHandler {
114: private Map values;
115: private int state;
116: private String namespace;
117: private ParamSaxBuffer buffer;
118:
119: public SAXContentHandler(Map values) {
120: this .values = values;
121: }
122:
123: public void setDocumentLocator(Locator arg0) {
124: // Ignore
125: }
126:
127: public void startDocument() throws SAXException {
128: // Ignore
129: }
130:
131: public void endDocument() throws SAXException {
132: // Ignore
133: }
134:
135: public void processingInstruction(String arg0, String arg1)
136: throws SAXException {
137: // Ignore
138: }
139:
140: public void skippedEntity(String arg0) throws SAXException {
141: // Ignore
142: }
143:
144: public void startElement(String ns, String localName,
145: String qName, Attributes atts) throws SAXException {
146: switch (this .state) {
147: case 0:
148: // <i18n:catalogue>
149: if (!"".equals(ns)
150: && !I18nUtils.matchesI18nNamespace(ns)) {
151: throw new SAXException(
152: "Root element <"
153: + EL_CATALOGUE
154: + "> must be non-namespaced or in i18n namespace.");
155: }
156: if (!EL_CATALOGUE.equals(localName)) {
157: throw new SAXException("Root element must be <"
158: + EL_CATALOGUE + ">.");
159: }
160: this .namespace = ns;
161: this .state++;
162: break;
163:
164: case 1:
165: // <i18n:message>
166: if (!EL_MESSAGE.equals(localName)) {
167: throw new SAXException("<" + EL_CATALOGUE
168: + "> must contain <" + EL_MESSAGE
169: + "> elements only.");
170: }
171: if (!this .namespace.equals(ns)) {
172: throw new SAXException("<" + EL_MESSAGE
173: + "> element must be in '" + this .namespace
174: + "' namespace.");
175: }
176: String key = atts.getValue(AT_KEY);
177: if (key == null) {
178: throw new SAXException("<" + EL_MESSAGE
179: + "> must have '" + AT_KEY + "' attribute.");
180: }
181: this .buffer = new ParamSaxBuffer();
182: this .values.put(key, this .buffer);
183: this .state++;
184: break;
185:
186: case 2:
187: this .buffer.startElement(ns, localName, qName, atts);
188: break;
189:
190: default:
191: throw new SAXException("Internal error: Invalid state");
192: }
193: }
194:
195: public void endElement(String ns, String localName, String qName)
196: throws SAXException {
197: switch (this .state) {
198: case 0:
199: break;
200:
201: case 1:
202: // </i18n:catalogue>
203: this .state--;
204: break;
205:
206: case 2:
207: if (this .namespace.equals(ns)
208: && EL_MESSAGE.equals(localName)) {
209: // </i18n:message>
210: this .buffer = null;
211: this .state--;
212: } else {
213: this .buffer.endElement(ns, localName, qName);
214: }
215: break;
216:
217: default:
218: throw new SAXException("Internal error: Invalid state");
219: }
220: }
221:
222: public void startPrefixMapping(String prefix, String uri)
223: throws SAXException {
224: if (this .buffer != null) {
225: this .buffer.startPrefixMapping(prefix, uri);
226: }
227: }
228:
229: public void endPrefixMapping(String prefix) throws SAXException {
230: if (this .buffer != null) {
231: this .buffer.endPrefixMapping(prefix);
232: }
233: }
234:
235: public void ignorableWhitespace(char[] ch, int start, int length)
236: throws SAXException {
237: if (this .buffer != null) {
238: this .buffer.ignorableWhitespace(ch, start, length);
239: }
240: }
241:
242: public void characters(char[] ch, int start, int length)
243: throws SAXException {
244: if (this .buffer != null) {
245: this .buffer.characters(ch, start, length);
246: }
247: }
248: }
249:
250: /**
251: * Construct a bundle.
252: * @param sourceURI source URI of the XML bundle
253: * @param locale locale
254: * @param parent parent bundle of this bundle
255: */
256: public XMLResourceBundle(String sourceURI, Locale locale,
257: Bundle parent) {
258: this .sourceURI = sourceURI;
259: this .locale = locale;
260: this .parent = parent;
261: this .values = Collections.EMPTY_MAP;
262: }
263:
264: /**
265: * (Re)Loads the XML bundle if necessary, based on the source URI.
266: * @return true if reloaded successfully
267: */
268: protected boolean reload(SourceResolver resolver, long interval) {
269: Source newSource = null;
270: Map newValues;
271:
272: try {
273: int valid = this .validity == null ? SourceValidity.INVALID
274: : this .validity.isValid();
275: if (valid != SourceValidity.VALID) {
276: // Saved validity is not valid, get new source and validity
277: newSource = resolver.resolveURI(this .sourceURI);
278: SourceValidity newValidity = newSource.getValidity();
279:
280: if (valid == SourceValidity.INVALID
281: || this .validity.isValid(newValidity) != SourceValidity.VALID) {
282: newValues = new HashMap();
283: SourceUtil.toSAX(newSource, new SAXContentHandler(
284: newValues));
285: synchronized (this ) {
286: // Update source validity and values
287: if (interval > 0 && newValidity != null) {
288: this .validity = new DelayedValidity(
289: interval, newValidity);
290: } else {
291: this .validity = newValidity;
292: }
293: this .values = newValues;
294: }
295: }
296: }
297:
298: // Success
299: return true;
300:
301: } catch (MalformedURLException e) {
302: getLogger().error(
303: "Bundle <" + this .sourceURI
304: + "> not loaded: Invalid URI", e);
305: newValues = Collections.EMPTY_MAP;
306:
307: } catch (ResourceNotFoundException e) {
308: // FIXME: this damn SourceUtil converts SNFE to RNFE!!!
309: if (getLogger().isInfoEnabled()) {
310: if (newSource != null && !newSource.exists()) {
311: // Nominal case where a bundle doesn't exist: log the message but not the exception
312: getLogger()
313: .info(
314: "Bundle <"
315: + sourceURI
316: + "> not loaded: Source URI not found");
317: } else {
318: // Log the exception
319: getLogger()
320: .info(
321: "Bundle <"
322: + sourceURI
323: + "> not loaded: Source URI not found",
324: e);
325: }
326: }
327: newValues = Collections.EMPTY_MAP;
328:
329: } catch (SourceNotFoundException e) {
330: if (getLogger().isInfoEnabled()) {
331: if (newSource != null && !newSource.exists()) {
332: // Nominal case where a bundle doesn't exist: log the message but not the exception
333: getLogger()
334: .info(
335: "Bundle <"
336: + sourceURI
337: + "> not loaded: Source URI not found");
338: } else {
339: // Log the exception
340: getLogger()
341: .info(
342: "Bundle <"
343: + sourceURI
344: + "> not loaded: Source URI not found",
345: e);
346: }
347: }
348: newValues = Collections.EMPTY_MAP;
349:
350: } catch (SAXException e) {
351: getLogger().error(
352: "Bundle <" + sourceURI
353: + "> not loaded: Invalid XML", e);
354: // Keep existing loaded values
355: newValues = this .values;
356:
357: } catch (Exception e) {
358: getLogger().error(
359: "Bundle <" + sourceURI + "> not loaded: Exception",
360: e);
361: // Keep existing loaded values
362: newValues = this .values;
363:
364: } finally {
365: if (newSource != null) {
366: resolver.release(newSource);
367: }
368: }
369:
370: synchronized (this ) {
371: // Use expires validity to delay next reloading.
372: if (interval > 0) {
373: this .validity = new ExpiresValidity(interval);
374: } else {
375: this .validity = null;
376: }
377: this .values = newValues;
378: }
379:
380: // Failure
381: return false;
382: }
383:
384: /**
385: * Gets the locale of the bundle.
386: *
387: * @return the locale
388: */
389: public Locale getLocale() {
390: return this .locale;
391: }
392:
393: /**
394: * Gets the source URI of the bundle.
395: *
396: * @return the source URI
397: */
398: public String getSourceURI() {
399: return this .sourceURI;
400: }
401:
402: /**
403: * Gets the validity of the bundle.
404: *
405: * @return the validity
406: */
407: public SourceValidity getValidity() {
408: return this .validity;
409: }
410:
411: /**
412: * Get an instance of the {@link ParamSaxBuffer} associated with the key.
413: *
414: * @param key the key
415: * @return the value, or null if no value associated with the key.
416: */
417: public Object getObject(String key) {
418: if (key == null) {
419: return null;
420: }
421:
422: Object value = this .values.get(key);
423: if (value != null) {
424: return value;
425: }
426:
427: if (this .parent != null) {
428: return this .parent.getObject(key);
429: }
430:
431: return null;
432: }
433:
434: /**
435: * Get a string representation of the value object by key.
436: *
437: * @param key the key
438: * @return the string value, or null if no value associated with the key.
439: */
440: public String getString(String key) {
441: if (key == null) {
442: return null;
443: }
444:
445: Object value = this.values.get(key);
446: if (value != null) {
447: return value.toString();
448: }
449:
450: if (this.parent != null) {
451: return this.parent.getString(key);
452: }
453:
454: return null;
455: }
456: }
|