/* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.commons.conf; import org.xml.sax.*; import org.xml.sax.helpers.DefaultHandler; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; import java.io.IOException; import java.io.Reader; /** * Implementation of {@link ConfigurationReader} used to read XML configuration streams. * <p> * The format of XML files parsed by instances of <code>XmlConfigurationReader</code> is fairly simple: * <ul> * <li> * Any element that doesn't contain other elements is considered to be a variable. Its value will be * the CDATA contained by the element. * </li> * <li> * Any element that contains other elements is considered to be a section. Any CDATA it might contain * will be ignored. * </li> * <li> * The XML file's first element is traditionally called <code>prefs</code>, but this isn't enforced. It * will be excluded from section names. * </li> * </ul> * </p> * <p> * For example: * <pre> * <prefs> * <some> * Random CDATA * <section> * <var1>value1</var1> * <var2>value2</var2> * </section> * </some> * </prefs> * </pre> * This will be interpreted as follows: * <ul> * <li><code>Random CDATA</code> will be ignored.</li> * <li>A variable called <code>some.section.var1</code> will be created with a value of <code>value1</code>.</li> * <li>A variable called <code>some.section.var2</code> will be created with a value of <code>value2</code>.</li> * </ul> * </p> * @author Nicolas Rinaudo * @see XmlConfigurationWriter */ public class XmlConfigurationReader extends DefaultHandler implements ConfigurationReader { // - Class fields -------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** Factory used to create {@link XmlConfigurationReader} instances. */ public static final ConfigurationReaderFactory<XmlConfigurationReader> FACTORY; // - Instance variables -------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** Current depth in the configuration tree. */ private int depth; /** Buffer for each element's CDATA. */ private final StringBuilder buffer; /** Name of the item being parsed. */ private String itemName; /** Class notified whenever a new configuration item is found. */ protected ConfigurationBuilder builder; /** Whether the current element is a variable. */ private boolean isVariable; /** Used to track the parser's position in the XML file. */ private Locator locator; // - Initialisation ------------------------------------------------------------------------------------------------ // ----------------------------------------------------------------------------------------------------------------- static { FACTORY = new ConfigurationReaderFactory<XmlConfigurationReader>() { public XmlConfigurationReader getReaderInstance() throws ReaderConfigurationException { return new XmlConfigurationReader(); } }; } /** * Creates a new instance of XML configuration reader. */ public XmlConfigurationReader() { buffer = new StringBuilder(); } // - Reader methods ------------------------------------------------------------------------------------------------ // ----------------------------------------------------------------------------------------------------------------- /** * Reads the content of <code>in</code> an passes build messages to <code>builder</code>. * @param in input stream from which to read the configuration data. * @param builder object to notify of build events. * @throws IOException if an I/O error occurs. * @throws ConfigurationFormatException if a configuration file format occurs. * @throws ConfigurationException if a non-specific error occurs. */ public void read(Reader in, ConfigurationBuilder builder) throws IOException, ConfigurationException, ConfigurationFormatException { this.builder = builder; locator = null; try {SAXParserFactory.newInstance().newSAXParser().parse(new InputSource(in), this);} catch(ParserConfigurationException e) {throw new ConfigurationException("Failed to create a SAX parser", e);} catch(SAXParseException e) {throw new ConfigurationFormatException(e.getMessage(), e.getLineNumber(), e.getColumnNumber());} catch(SAXException e) {throw new ConfigurationFormatException(e.getException() == null ? e : e.getException());} } // - XML handling -------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** * This method is public as an implementation side effect and should never be called directly. */ @Override public void characters(char[] ch, int start, int length) {buffer.append(ch, start, length);} /** * This method is public as an implementation side effect and should never be called directly. */ @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { depth++; if(depth == 1) return; if(itemName != null) { try {builder.startSection(itemName);} catch(Exception e) {throw new SAXParseException(e.getMessage(), locator, e);} } buffer.setLength(0); itemName = qName; isVariable = true; } /** * This method is public as an implementation side effect and should never be called directly. */ @Override public void endElement(String uri, String localName, String qName) throws SAXException { depth--; if(depth == 0) return; // If the current element doesn't have subsections, considers it to be a variable. if(isVariable) { String value; value = buffer.toString().trim(); // Ignores empty values, otherwise notifies the builder of a new variable. if(!value.isEmpty()) { try {builder.addVariable(qName, value);} catch(Exception e) {throw new SAXParseException(e.getMessage(), locator, e);} } } // The current element is a container, closes it. else { try {builder.endSection(qName);} catch(Exception e) {throw new SAXParseException(e.getMessage(), locator, e);} } isVariable = false; itemName = null; } /** * This method is public as an implementation side effect and should never be called directly. */ @Override public void startDocument() throws SAXException { try {builder.startConfiguration();} catch(Exception e) {throw new SAXParseException(e.getMessage(), locator, e);} } /** * This method is public as an implementation side effect and should never be called directly. */ @Override public void endDocument() throws SAXException { try {builder.endConfiguration();} catch(Exception e) {throw new SAXParseException(e.getMessage(), locator, e);} } /** * This method is public as an implementation side effect and should never be called directly. */ @Override public void setDocumentLocator(Locator locator) {this.locator = locator;} }