1   package org.daisy.pipeline.tts.impl;
2   
3   import java.io.IOException;
4   import java.io.StringReader;
5   import java.util.ArrayList;
6   import java.util.Collection;
7   import java.util.HashMap;
8   import java.util.List;
9   import java.util.Map;
10  
11  import javax.xml.transform.sax.SAXSource;
12  
13  import net.sf.saxon.s9api.Processor;
14  import net.sf.saxon.s9api.SaxonApiException;
15  import net.sf.saxon.s9api.XdmNode;
16  
17  import org.daisy.common.properties.Properties;
18  import org.daisy.pipeline.tts.config.ConfigReader;
19  import org.daisy.pipeline.tts.config.DynamicPropertiesExtension;
20  import org.daisy.pipeline.tts.TTSEngine;
21  import org.daisy.pipeline.tts.TTSRegistry;
22  import org.daisy.pipeline.tts.TTSService;
23  import org.daisy.pipeline.tts.TTSService.ServiceDisabledException;
24  import org.daisy.pipeline.webservice.restlet.AuthenticatedResource;
25  import org.daisy.pipeline.webservice.xml.XmlUtils;
26  
27  import org.restlet.data.MediaType;
28  import org.restlet.data.Status;
29  import org.restlet.ext.xml.DomRepresentation;
30  import org.restlet.representation.Representation;
31  import org.restlet.resource.Get;
32  import org.restlet.resource.Post;
33  
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  
37  import org.w3c.dom.Document;
38  import org.w3c.dom.Element;
39  
40  import org.xml.sax.InputSource;
41  
42  public class TTSEnginesResource extends AuthenticatedResource {
43  
44  	static final String TTS_REGISTRY_KEY = "tts-registry";
45  
46  	private static final Logger logger = LoggerFactory.getLogger(TTSEnginesResource.class.getName());
47  	private static final Processor saxonProcessor = new Processor(false);
48  
49  	private TTSRegistry ttsRegistry;
50  
51  	@Override
52  	public void doInit() {
53  		super.doInit();
54  		if (!isAuthenticated()) {
55  			return;
56  		}
57  		ttsRegistry = (TTSRegistry)getContext().getAttributes().get(TTS_REGISTRY_KEY);
58  	}
59  
60  	/**
61  	 * Gets the resource.
62  	 *
63  	 * @return the resource
64  	 */
65  	@Get("xml")
66  	public Representation getResource() {
67  		return getResource(null);
68  	}
69  
70  	/**
71  	 * @param ttsConfig
72  	 */
73  	@Post("xml")
74  	public Representation getResource(Representation ttsConfig) {
75  		logRequest();
76  		maybeEnableCORS();
77  		if (!isAuthenticated()) {
78  			setStatus(Status.CLIENT_ERROR_UNAUTHORIZED);
79  			return null;
80  		}
81  		Collection<TTSService> ttsServices = ttsRegistry.getServices();
82  		Map<String,String> properties; {
83  			DynamicPropertiesExtension propsExt = new DynamicPropertiesExtension();
84  			XdmNode configXML = null; {
85  				if (ttsConfig != null) {
86  					try {
87  						configXML = saxonProcessor.newDocumentBuilder().build(
88  							new SAXSource(new InputSource(new StringReader(ttsConfig.getText()))));
89  					} catch (IOException|SaxonApiException e) {
90  						logger.error("bad request:", e);
91  						setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
92  						return getErrorRepresentation(e.getMessage());
93  					}
94  				}
95  			}
96  			properties = Properties.getSnapshot();
97  			new ConfigReader(saxonProcessor, configXML, properties, propsExt);
98  			if (configXML != null)
99  				logger.debug("TTS configuration XML:\n" + configXML);
100 			Map<String,String> dynProperties = propsExt.getDynamicProperties();
101 			if (dynProperties != null && !dynProperties.isEmpty()) {
102 				properties = new HashMap<>(properties);
103 				properties.putAll(dynProperties);
104 			}
105 		}
106 		Document enginesDoc; {
107 			enginesDoc = XmlUtils.createDom("tts-engines");
108 			Element enginesElem = enginesDoc.getDocumentElement();
109 			String baseURL = getRequest().getRootRef().toString();
110 			enginesElem.setAttribute("href", baseURL + TTSEnginesWebServiceExtension.TTS_ENGINES_ROUTE);
111 			for (TTSService s : ttsServices) {
112 				Element engineElem = enginesDoc.createElementNS(XmlUtils.NS_PIPELINE_DATA, "tts-engine");
113 				String name = s.getName();
114 				engineElem.setAttribute("name", name);
115 				engineElem.setAttribute("nicename", s.getDisplayName());
116 				TTSEngine e = null;
117 				Throwable error = null;
118 				try {
119 				    e = s.newEngine(properties);
120 					engineElem.setAttribute("status", "available");
121 				} catch (ServiceDisabledException ex) {
122 					logger.debug(name + " is disabled", ex);
123 					error = ex;
124 					engineElem.setAttribute("status", "disabled");
125 				} catch (Throwable ex) {
126 					logger.debug(name + " could not be activated", ex);
127 					error = ex;
128 					engineElem.setAttribute("status", "error");
129 				}
130 				if (e != null) {
131 					engineElem.setAttribute("voices", baseURL + VoicesWebServiceExtension.VOICES_ROUTE + "?engine=" + name);
132 					List<String> features = new ArrayList<>();
133 					if (e.handlesSpeakingRate())
134 						features.add("speech-rate");
135 					if (e.handlesPronunciation())
136 						features.add("phoneme");
137 					if (!features.isEmpty())
138 						engineElem.setAttribute("features", String.join(" ", features));
139 				} else if (error != null) {
140 					// Clients should use first line as the short message. The short message is
141 					// followed by the full message after a blank line.
142 					String shortMessage = error.getMessage();
143 					error = error.getCause();
144 					String detailedMessage = error != null ? error.getMessage() : null;
145 					if (shortMessage.length() > 80) {
146 						// Use the heuristic that if a message is longer than 80 characters, it is possible
147 						// that it is too technical for the average user. It also becomes difficult to fit it
148 						// in the UI. So provide this backup:
149 						if (detailedMessage == null)
150 							detailedMessage = shortMessage;
151 						else {
152 							detailedMessage = detailedMessage.trim();
153 							shortMessage = shortMessage.trim();
154 							if (detailedMessage.startsWith(shortMessage))
155 								detailedMessage = detailedMessage.substring(shortMessage.length());
156 							if (detailedMessage.startsWith(":")) {
157 								detailedMessage = detailedMessage.substring(1);
158 								detailedMessage = detailedMessage.trim();
159 							}
160 							if (detailedMessage.length() > 0) {
161 								if (!shortMessage.matches(".*\\p{Punct}$"))
162 									shortMessage += ":";
163 								detailedMessage = shortMessage + " " + detailedMessage;
164 							} else
165 								detailedMessage = shortMessage;
166 						}
167 						shortMessage = "Could not connect to the service";
168 					}
169 					String message = shortMessage;
170 					if (detailedMessage != null)
171 						message += ("\n\n" + detailedMessage);
172 					engineElem.setAttribute("message", message);
173 				}
174 				enginesElem.appendChild(engineElem);
175 			}
176 		}
177 		DomRepresentation dom; {
178 			try {
179 				dom = new DomRepresentation(MediaType.APPLICATION_XML, enginesDoc);
180 				setStatus(Status.SUCCESS_OK);
181 			} catch (Exception e) {
182 				setStatus(Status.SERVER_ERROR_INTERNAL);
183 				return getErrorRepresentation(e.getMessage());
184 			}
185 		}
186 		logResponse(dom);
187 		return dom;
188 	}
189 }