1   package org.daisy.pipeline.tts.config;
2   
3   import java.io.File;
4   import java.io.StringReader;
5   import java.net.URI;
6   import java.net.URL;
7   import java.util.Map;
8   
9   import javax.xml.transform.sax.SAXSource;
10  
11  import net.sf.saxon.s9api.Axis;
12  import net.sf.saxon.s9api.Processor;
13  import net.sf.saxon.s9api.QName;
14  import net.sf.saxon.s9api.XdmNode;
15  import net.sf.saxon.s9api.XdmNodeKind;
16  import net.sf.saxon.s9api.XdmSequenceIterator;
17  
18  import org.daisy.common.file.URLs;
19  import org.daisy.common.properties.Properties;
20  import org.daisy.common.properties.Properties.Property;
21  
22  import org.osgi.service.component.annotations.Component;
23  
24  import org.slf4j.Logger;
25  import org.slf4j.LoggerFactory;
26  
27  import org.xml.sax.InputSource;
28  
29  public class ConfigReader {
30  
31  	private static final Logger logger = LoggerFactory.getLogger(ConfigReader.class);
32  
33  	public interface Extension {
34  		/**
35  		 * @param node has a non-null local name
36  		 * @return false is the parsing can keep going with other extensions
37  		 *         (most likely if @node is not related to the extension)
38  		 */
39  		public boolean parseNode(XdmNode node, URI documentURI, ConfigReader parent);
40  	}
41  
42  	private final Processor saxonproc;
43  
44  	public ConfigReader(Processor saxonproc, Extension... extensions) {
45  		this(saxonproc, null, extensions);
46  	}
47  
48  	public ConfigReader(Processor saxonproc, XdmNode doc, Extension... extensions) {
49  		this(saxonproc, doc, null, extensions);
50  	}
51  
52  	public ConfigReader(Processor saxonproc, XdmNode doc, Map<String,String> properties, Extension... extensions) {
53  		this.saxonproc = saxonproc;
54  		String config = properties != null ? TTSConfigProperty.getValue(properties) : TTSConfigProperty.getValue();
55  		if (config != null && !"".equals(config)) {
56  			XdmNode content = null; {
57  				// check if it is a file path
58  				if (config.startsWith("file:") || new File(config).exists())
59  					content = parseXML(config);
60  				else
61  					// check if it is XML
62  					try {
63  						content = saxonproc.newDocumentBuilder().build(
64  							new SAXSource(new InputSource(new StringReader(config))));
65  					} catch (Exception e) {
66  						logger.debug(
67  							"failed to read TTS configuration: not a file path and not valid XML: "
68  							+ config);
69  					}
70  				if (content != null)
71  					read(content, extensions);
72  			}
73  		}
74  		if (doc != null) {
75  			read(doc, extensions);
76  		}
77  	}
78  
79  	/**
80  	 * Read document from URL within TTS configuration document.
81  	 *
82  	 * @param url  The URL to read from. If relative, it is resolved
83  	 *             against the base URI of the configuration document.
84  	 * @param base The base URI of the configuration document.
85  	 */
86  	protected XdmNode parseXML(String url, URI base) {
87  		try {
88  			return parseXML(resolve(url, base), saxonproc);
89  		} catch (Exception e) {
90  			logger.debug("Malformed URL: " + url);
91  			return null;
92  		}
93  	}
94  
95  	// package private for unit tests
96  	static URL resolve(String url, URI base) {
97  		return URLs.asURL(URLs.resolve(base, URLs.asURI(url)));
98  	}
99  
100 	/**
101 	 * Read document from URL.
102 	 *
103 	 * @param url The URL to read from. If relative, it is resolved
104 	 *            against the current directory.
105 	 */
106 	protected XdmNode parseXML(String url) {
107 		return parseXML(url, URLs.asURI(new File("./")));
108 	}
109 
110 	/**
111 	 * Read document from URL.
112 	 *
113 	 * @param url The absolute URL to read from.
114 	 */
115 	private static XdmNode parseXML(URL url, Processor saxonproc) {
116 		if (url == null)
117 			return null;
118 		try {
119 			SAXSource source = new SAXSource(new InputSource(url.openStream()));
120 			source.setSystemId(url.toString());
121 			return saxonproc.newDocumentBuilder().build(source);
122 		} catch (Exception e) {
123 			logger.debug("error while reading " + url + ": " + e);
124 			return null;
125 		}
126 	}
127 
128 	private void read(XdmNode doc, Extension... extensions) {
129 		URI docURI = doc.getBaseURI();
130 		XdmSequenceIterator it = doc.axisIterator(Axis.CHILD);
131 		XdmNode root = doc;
132 		while (doc.getNodeKind() != XdmNodeKind.ELEMENT && it.hasNext())
133 			root = (XdmNode) it.next();
134 		it = root.axisIterator(Axis.CHILD);
135 		while (it.hasNext()) {
136 			XdmNode node = (XdmNode) it.next();
137 			QName qname = node.getNodeName();
138 			if (qname != null) {
139 				boolean parsed = false;
140 				for (int k = 0; !parsed && k < extensions.length; ++k) {
141 					if ("css".equalsIgnoreCase(qname.getLocalName())) {
142 						logger.warn("Ignoring 'css' element " + node.toString()
143 						            + " inside TTS config file.\nPlease specify CSS style sheets"
144 						            + " through the designated option or attach them to the input"
145 						            + " document.");
146 					} else if ("lexicon".equalsIgnoreCase(qname.getLocalName())) {
147 						logger.warn("Ignoring 'lexicon' element " + node.toString()
148 						            + " inside TTS config file.\nPlease specify PLS lexicons"
149 						            + " through the designated option or attach them to the input"
150 						            + " document.");
151 					} else {
152 						parsed = extensions[k].parseNode(node, docURI, this);
153 					}
154 				}
155 			}
156 		}
157 	}
158 
159 	// this is to make sure that the "org.daisy.pipeline.tts.config" property is
160 	// exposed from the start, before the first ConfigReader object is created
161 	@Component(
162 		name = "tts-config-property",
163 		immediate = true
164 	)
165 	protected static class TTSConfigProperty {
166 
167 		private static final Property PROPERTY = Properties.getProperty("org.daisy.pipeline.tts.config",
168 		                                                                true,
169 		                                                                "TTS configuration (XML or file path)",
170 		                                                                false,
171 		                                                                null);
172 
173 		protected TTSConfigProperty() {}
174 
175 		public static String getValue() {
176 			return PROPERTY.getValue();
177 		}
178 
179 		public static String getValue(Map<String,String> snapshot) {
180 			return PROPERTY.getValue(snapshot);
181 		}
182 
183 	}
184 }