/* * 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. */ /* $Id$ */ package org.apache.fop.util; import java.io.IOException; import java.io.InputStream; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Enumeration; import java.util.Hashtable; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.Properties; import java.util.ResourceBundle; import java.util.Stack; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.sax.SAXResult; import javax.xml.transform.sax.SAXTransformerFactory; import javax.xml.transform.stream.StreamSource; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import org.apache.xmlgraphics.util.QName; /** * This class is a ResourceBundle that loads its contents from XML files instead of properties * files (like PropertiesResourceBundle). * <p> * The XML format for this resource bundle implementation is the following * (the same as Apache Cocoon's XMLResourceBundle): * <pre> * <catalogue xml:lang="en"> * <message key="key1">Message <br/> Value 1</message> * <message key="key2">Message <br/> Value 1</message> * ... * </catalogue> * </pre> */ public class XMLResourceBundle extends ResourceBundle { //Note: Some code here has been copied and adapted from Apache Harmony! private Properties resources = new Properties(); private Locale locale; private static SAXTransformerFactory tFactory = (SAXTransformerFactory)SAXTransformerFactory.newInstance(); /** * Creates a resource bundle from an InputStream. * @param in the stream to read from * @throws IOException if an I/O error occurs */ public XMLResourceBundle(InputStream in) throws IOException { try { Transformer transformer = tFactory.newTransformer(); StreamSource src = new StreamSource(in); SAXResult res = new SAXResult(new CatalogueHandler()); transformer.transform(src, res); } catch (TransformerException e) { throw new IOException("Error while parsing XML resource bundle: " + e.getMessage()); } } /** * Gets a resource bundle using the specified base name, default locale, and class loader. * @param baseName the base name of the resource bundle, a fully qualified class name * @param loader the class loader from which to load the resource bundle * @return a resource bundle for the given base name and the default locale * @throws MissingResourceException if no resource bundle for the specified base name can be * found * @see java.util.ResourceBundle#getBundle(String) */ public static ResourceBundle getXMLBundle(String baseName, ClassLoader loader) throws MissingResourceException { return getXMLBundle(baseName, Locale.getDefault(), loader); } /** * Gets a resource bundle using the specified base name, locale, and class loader. * @param baseName the base name of the resource bundle, a fully qualified class name * @param locale the locale for which a resource bundle is desired * @param loader the class loader from which to load the resource bundle * @return a resource bundle for the given base name and locale * @throws MissingResourceException if no resource bundle for the specified base name can be * found * @see java.util.ResourceBundle#getBundle(String, Locale, ClassLoader) */ public static ResourceBundle getXMLBundle(String baseName, Locale locale, ClassLoader loader) throws MissingResourceException { if (loader == null) { throw new NullPointerException("loader must not be null"); } if (baseName == null) { throw new NullPointerException("baseName must not be null"); } assert locale != null; ResourceBundle bundle; if (!locale.equals(Locale.getDefault())) { bundle = handleGetXMLBundle(baseName, "_" + locale, false, loader); if (bundle != null) { return bundle; } } bundle = handleGetXMLBundle(baseName, "_" + Locale.getDefault(), true, loader); if (bundle != null) { return bundle; } throw new MissingResourceException( baseName + " (" + locale + ")", baseName + '_' + locale, null); } static class MissingBundle extends ResourceBundle { public Enumeration getKeys() { return null; } public Object handleGetObject(String name) { return null; } } private static final ResourceBundle MISSING = new MissingBundle(); private static final ResourceBundle MISSINGBASE = new MissingBundle(); private static Map cache = new java.util.WeakHashMap(); //<Object, Hashtable<String, ResourceBundle>> private static ResourceBundle handleGetXMLBundle(String base, String locale, boolean loadBase, final ClassLoader loader) { XMLResourceBundle bundle = null; String bundleName = base + locale; Object cacheKey = loader != null ? (Object) loader : (Object) "null"; Hashtable loaderCache; //<String, ResourceBundle> synchronized (cache) { loaderCache = (Hashtable)cache.get(cacheKey); if (loaderCache == null) { loaderCache = new Hashtable(); cache.put(cacheKey, loaderCache); } } ResourceBundle result = (ResourceBundle)loaderCache.get(bundleName); if (result != null) { if (result == MISSINGBASE) { return null; } if (result == MISSING) { if (!loadBase) { return null; } String extension = strip(locale); if (extension == null) { return null; } return handleGetXMLBundle(base, extension, loadBase, loader); } return result; } final String fileName = bundleName.replace('.', '/') + ".xml"; InputStream stream = (InputStream)AccessController .doPrivileged(new PrivilegedAction() { public Object run() { return loader == null ? ClassLoader.getSystemResourceAsStream(fileName) : loader.getResourceAsStream(fileName); } }); if (stream != null) { try { try { bundle = new XMLResourceBundle(stream); } finally { stream.close(); } bundle.setLocale(locale); } catch (IOException e) { throw new MissingResourceException(e.getMessage(), base, null); } } String extension = strip(locale); if (bundle != null) { if (extension != null) { ResourceBundle parent = handleGetXMLBundle(base, extension, true, loader); if (parent != null) { bundle.setParent(parent); } } loaderCache.put(bundleName, bundle); return bundle; } if (extension != null) { ResourceBundle fallback = handleGetXMLBundle(base, extension, loadBase, loader); if (fallback != null) { loaderCache.put(bundleName, fallback); return fallback; } } loaderCache.put(bundleName, loadBase ? MISSINGBASE : MISSING); return null; } private void setLocale(String name) { String language = ""; String country = ""; String variant = ""; if (name.length() > 1) { int nextIndex = name.indexOf('_', 1); if (nextIndex == -1) { nextIndex = name.length(); } language = name.substring(1, nextIndex); if (nextIndex + 1 < name.length()) { int index = nextIndex; nextIndex = name.indexOf('_', nextIndex + 1); if (nextIndex == -1) { nextIndex = name.length(); } country = name.substring(index + 1, nextIndex); if (nextIndex + 1 < name.length()) { variant = name.substring(nextIndex + 1, name.length()); } } } this.locale = new Locale(language, country, variant); } private static String strip(String name) { int index = name.lastIndexOf('_'); if (index != -1) { return name.substring(0, index); } return null; } private Enumeration getLocalKeys() { return (Enumeration)resources.propertyNames(); } /** {@inheritDoc} */ public Locale getLocale() { return this.locale; } /** {@inheritDoc} */ public Enumeration getKeys() { if (parent == null) { return getLocalKeys(); } return new Enumeration() { private Enumeration local = getLocalKeys(); private Enumeration pEnum = parent.getKeys(); private Object nextElement; private boolean findNext() { if (nextElement != null) { return true; } while (pEnum.hasMoreElements()) { Object next = pEnum.nextElement(); if (!resources.containsKey(next)) { nextElement = next; return true; } } return false; } public boolean hasMoreElements() { if (local.hasMoreElements()) { return true; } return findNext(); } public Object nextElement() { if (local.hasMoreElements()) { return local.nextElement(); } if (findNext()) { Object result = nextElement; nextElement = null; return result; } // Cause an exception return pEnum.nextElement(); } }; } /** {@inheritDoc} */ protected Object handleGetObject(String key) { if (key == null) { throw new NullPointerException("key must not be null"); } return resources.get(key); } /** {@inheritDoc} */ public String toString() { return "XMLResourceBundle: " + getLocale(); } private class CatalogueHandler extends DefaultHandler { private static final String CATALOGUE = "catalogue"; private static final String MESSAGE = "message"; private StringBuffer valueBuffer = new StringBuffer(); private Stack elementStack = new Stack(); private String currentKey; private boolean isOwnNamespace(String uri) { return ("".equals(uri)); } private QName getParentElementName() { return (QName)elementStack.peek(); } /** {@inheritDoc} */ public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { super.startElement(uri, localName, qName, atts); QName elementName = new QName(uri, qName); if (isOwnNamespace(uri)) { if (CATALOGUE.equals(localName)) { //nop } else if (MESSAGE.equals(localName)) { if (!CATALOGUE.equals(getParentElementName().getLocalName())) { throw new SAXException(MESSAGE + " must be a child of " + CATALOGUE); } this.currentKey = atts.getValue("key"); } else { throw new SAXException("Invalid element name: " + elementName); } } else { //ignore } this.valueBuffer.setLength(0); elementStack.push(elementName); } /** {@inheritDoc} */ public void endElement(String uri, String localName, String qName) throws SAXException { super.endElement(uri, localName, qName); elementStack.pop(); if (isOwnNamespace(uri)) { if (CATALOGUE.equals(localName)) { //nop } else if (MESSAGE.equals(localName)) { if (this.currentKey == null) { throw new SAXException( "current key is null (attribute 'key' might be mistyped)"); } resources.put(this.currentKey, this.valueBuffer.toString()); this.currentKey = null; } } else { //ignore } this.valueBuffer.setLength(0); } /** {@inheritDoc} */ public void characters(char[] ch, int start, int length) throws SAXException { super.characters(ch, start, length); valueBuffer.append(ch, start, length); } } }