Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
ConfigurationParser |
|
| 1.9090909090909092;1.909 | ||||
ConfigurationParser$AddDialogMetadataRule |
|
| 1.9090909090909092;1.909 |
1 | /* | |
2 | * Licensed to the Apache Software Foundation (ASF) under one or more | |
3 | * contributor license agreements. See the NOTICE file distributed with | |
4 | * this work for additional information regarding copyright ownership. | |
5 | * The ASF licenses this file to you under the Apache License, Version 2.0 | |
6 | * (the "License"); you may not use this file except in compliance with | |
7 | * the License. You may obtain a copy of the License at | |
8 | * | |
9 | * http://www.apache.org/licenses/LICENSE-2.0 | |
10 | * | |
11 | * Unless required by applicable law or agreed to in writing, software | |
12 | * distributed under the License is distributed on an "AS IS" BASIS, | |
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | * See the License for the specific language governing permissions and | |
15 | * limitations under the License. | |
16 | */ | |
17 | ||
18 | package org.apache.shale.dialog.scxml.config; | |
19 | ||
20 | import java.io.IOException; | |
21 | import java.net.URL; | |
22 | import java.util.ArrayList; | |
23 | import java.util.HashMap; | |
24 | import java.util.Iterator; | |
25 | import java.util.List; | |
26 | import java.util.Map; | |
27 | ||
28 | import org.apache.commons.digester.Digester; | |
29 | import org.apache.commons.digester.Rule; | |
30 | import org.apache.commons.scxml.env.SimpleErrorHandler; | |
31 | import org.apache.commons.scxml.io.SCXMLDigester; | |
32 | import org.apache.commons.scxml.model.CustomAction; | |
33 | import org.apache.commons.scxml.model.ModelException; | |
34 | import org.apache.commons.scxml.model.SCXML; | |
35 | import org.apache.shale.dialog.scxml.Globals; | |
36 | import org.apache.shale.dialog.scxml.action.RedirectAction; | |
37 | import org.apache.shale.dialog.scxml.action.ViewAction; | |
38 | import org.apache.shale.dialog.scxml.config.DialogMetadata.SCXMLAction; | |
39 | import org.xml.sax.SAXException; | |
40 | ||
41 | /** | |
42 | * <p>Configuration utility for parsing SCXML documents as resources for | |
43 | * defining dialogs. This class has no dependencies on web tier APIs, | |
44 | * only on the Commons SCXML state machine engine library, and | |
45 | * on the parsing technology (Commons Digester) being used.</p> | |
46 | * | |
47 | * <p>The configuration for each Shale dialog exists as a standalone | |
48 | * SCXML document, with additional dialog "metadata" file(s) | |
49 | * that serve as the entry point for the Shale Dialog Manager.</p> | |
50 | * | |
51 | * <p>These dialog-config.xml file(s) look like this: | |
52 | * <pre> | |
53 | * <dialogs> | |
54 | * <dialog name="Foo" scxmlconfig="foo.scxml" /> | |
55 | * <dialog name="Bar" scxmlconfig="bar.scxml" | |
56 | * dataclassname="org.apache.shale.examples.Bar" /> | |
57 | * <dialog name="Baz" scxmlconfig="baz.scxml" /> | |
58 | * <!-- etc. --> | |
59 | * </dialogs> | |
60 | * </pre> | |
61 | * </p> | |
62 | * | |
63 | * <p>To use this utility, instantiate a new instance and set the | |
64 | * <code>dialogs</code>, <code>resource</code>, and <code>validating</code> | |
65 | * properties. Then, call the <code>parse()</code> method. You can parse | |
66 | * more than one resource by resetting the <code>resource</code> | |
67 | * property and calling <code>parse()</code> again.</p> | |
68 | * | |
69 | * @since 1.0.4 | |
70 | */ | |
71 | ||
72 | 0 | public final class ConfigurationParser { |
73 | ||
74 | ||
75 | // -------------------------------------------------------------- Properties | |
76 | ||
77 | /** | |
78 | * <p>Registration information for the DTD we will use to validate.</p> | |
79 | */ | |
80 | 0 | private static final String[] REGISTRATIONS = |
81 | { "-//Apache Software Foundation//DTD Shale SCXML Dialog Configuration 1.0//EN", | |
82 | "/org/apache/shale/dialog/scxml/dialog-scxml-config_1_0.dtd" }; | |
83 | ||
84 | ||
85 | /** | |
86 | * <p><code>Map</code> of <code>Dialog</code> instances resulting | |
87 | * from parsing, keyed by dialog name.</p> | |
88 | */ | |
89 | 0 | private Map dialogs = null; |
90 | ||
91 | ||
92 | /** | |
93 | * <p>Return the <code>Map</code> of <code>Dialog</code> instances | |
94 | * into which parsed information will be stored, keyed by dialog | |
95 | * name.</p> | |
96 | * | |
97 | * @return Map of SCXML instances, keyed by logical dialog name | |
98 | */ | |
99 | public Map getDialogs() { | |
100 | 0 | return this.dialogs; |
101 | } | |
102 | ||
103 | ||
104 | /** | |
105 | * <p>Set the <code>Map</code> of <code>Dialog</code> instances | |
106 | * into which parsed information will be stored, keyed by dialog | |
107 | * name.</p> | |
108 | * | |
109 | * @param dialogs The new map | |
110 | */ | |
111 | public void setDialogs(Map dialogs) { | |
112 | 0 | this.dialogs = dialogs; |
113 | 0 | } |
114 | ||
115 | ||
116 | /** | |
117 | * <p>The URL of the configuration resource to be parsed.</p> | |
118 | */ | |
119 | 0 | private URL resource = null; |
120 | ||
121 | ||
122 | /** | |
123 | * <p>Return the URL of the configuration resource to be parsed.</p> | |
124 | * | |
125 | * @return The resource URL | |
126 | */ | |
127 | public URL getResource() { | |
128 | 0 | return this.resource; |
129 | } | |
130 | ||
131 | ||
132 | /** | |
133 | * <p>Set the URL of the configuration resource to be parsed.</p> | |
134 | * | |
135 | * @param resource The new resource URL | |
136 | */ | |
137 | public void setResource(URL resource) { | |
138 | 0 | this.resource = resource; |
139 | 0 | } |
140 | ||
141 | ||
142 | /** | |
143 | * <p>Flag indicating whether we should do a validating parse or not.</p> | |
144 | */ | |
145 | 0 | private boolean validating = true; |
146 | ||
147 | ||
148 | /** | |
149 | * <p>Return a flag indicating whether we will be doing a validating parse | |
150 | * or not. Default value is <code>false</code>.</p> | |
151 | * | |
152 | * @return Whether the parse is validating | |
153 | */ | |
154 | public boolean isValidating() { | |
155 | 0 | return this.validating; |
156 | } | |
157 | ||
158 | ||
159 | /** | |
160 | * <p>Set a flag indicating whether we will be doing a validating parse | |
161 | * or not.</p> | |
162 | * | |
163 | * @param validating New flag value | |
164 | */ | |
165 | public void setValidating(boolean validating) { | |
166 | 0 | this.validating = validating; |
167 | 0 | } |
168 | ||
169 | ||
170 | // ---------------------------------------------------------- Public Methods | |
171 | ||
172 | ||
173 | /** | |
174 | * <p>Parse the configuration resource identified by the <code>resource</code> | |
175 | * property, storing resulting information in the <code>Map</code> specified | |
176 | * by the <code>dialogs</code> property.</p> | |
177 | * | |
178 | * @exception IOException if an input/output error occurs | |
179 | * @exception SAXException if an XML parsing error occurs | |
180 | */ | |
181 | public void parse() throws IOException, SAXException { | |
182 | ||
183 | 0 | Map metadata = new HashMap(); |
184 | 0 | Digester digester = digester(); |
185 | 0 | digester.clear(); |
186 | 0 | digester.push(metadata); |
187 | 0 | digester.parse(getResource()); |
188 | ||
189 | 0 | parseDialogs(metadata); |
190 | 0 | } |
191 | ||
192 | ||
193 | // --------------------------------------------------------- Private Methods | |
194 | ||
195 | ||
196 | /** | |
197 | * <p>Return a fully configured <code>Digester</code> instance.</p> | |
198 | * | |
199 | * @return The configuration parser Digester instance | |
200 | */ | |
201 | private Digester digester() { | |
202 | ||
203 | 0 | Digester digester = new Digester(); |
204 | ||
205 | // Configure global characteristics | |
206 | 0 | digester.setNamespaceAware(false); |
207 | 0 | digester.setUseContextClassLoader(true); |
208 | 0 | digester.setValidating(isValidating()); |
209 | ||
210 | // Register local copy of our DTDs | |
211 | 0 | for (int i = 0; i < REGISTRATIONS.length; i += 2) { |
212 | 0 | URL url = this.getClass().getResource(REGISTRATIONS[i + 1]); |
213 | 0 | digester.register(REGISTRATIONS[i], url); |
214 | } | |
215 | ||
216 | // Configure processing rules | |
217 | ||
218 | // dialogs/dialog | |
219 | 0 | digester.addObjectCreate("dialogs/dialog", DialogMetadata.class); |
220 | 0 | digester.addSetProperties("dialogs/dialog"); |
221 | 0 | digester.addRule("dialogs/dialog", new AddDialogMetadataRule()); |
222 | ||
223 | 0 | digester.addObjectCreate("dialogs/dialog/scxmlaction", SCXMLAction.class); |
224 | 0 | digester.addSetProperties("dialogs/dialog/scxmlaction"); |
225 | 0 | digester.addSetNext("dialogs/dialog/scxmlaction", "addDialogAction"); |
226 | ||
227 | 0 | return digester; |
228 | ||
229 | } | |
230 | ||
231 | ||
232 | /** | |
233 | * <p>Parse the SCXML documents in the dialog metadata, storing resulting | |
234 | * information as an entry in the <code>Map</code> specified by the | |
235 | * <code>dialogs</code> property.</p> | |
236 | * | |
237 | * @param metadata The metadata map | |
238 | * @throws IOException if an input/output error occurs | |
239 | * @throws SAXException if an XML parsing error occurs | |
240 | */ | |
241 | private void parseDialogs(Map metadata) throws IOException, SAXException { | |
242 | ||
243 | 0 | Iterator iterator = metadata.entrySet().iterator(); |
244 | ||
245 | // Create a list of the custom Commons SCXML actions defined by the | |
246 | // Shale dialog Commons SCXML implementation | |
247 | 0 | List shaleDialogActions = new ArrayList(); |
248 | ||
249 | // <shale:redirect> | |
250 | 0 | CustomAction redirectAction = |
251 | new CustomAction(Globals.CUSTOM_SCXML_ACTIONS_URI, | |
252 | "redirect", RedirectAction.class); | |
253 | 0 | shaleDialogActions.add(redirectAction); |
254 | ||
255 | // <shale:view> | |
256 | 0 | CustomAction viewAction = |
257 | new CustomAction(Globals.CUSTOM_SCXML_ACTIONS_URI, | |
258 | "view", ViewAction.class); | |
259 | 0 | shaleDialogActions.add(viewAction); |
260 | ||
261 | // Class loader for app developer defined custom Commons SCXML actions | |
262 | 0 | ClassLoader loader = Thread.currentThread().getContextClassLoader(); |
263 | 0 | if (loader == null) { |
264 | 0 | loader = ConfigurationParser.class.getClassLoader(); |
265 | } | |
266 | ||
267 | 0 | while (iterator.hasNext()) { |
268 | ||
269 | 0 | Map.Entry entry = (Map.Entry) iterator.next(); |
270 | 0 | String name = (String) entry.getKey(); |
271 | 0 | DialogMetadata dMetadata = (DialogMetadata) entry.getValue(); |
272 | 0 | String scxmlconfig = dMetadata.getScxmlconfig(); |
273 | ||
274 | // The custom actions available to this dialog is the | |
275 | // summation of the ones defined by this Shale dialog module | |
276 | // and those defined by the app developer using the dialog | |
277 | // configuration file for this dialog | |
278 | 0 | List customDialogActions = new ArrayList(); |
279 | 0 | customDialogActions.addAll(shaleDialogActions); |
280 | ||
281 | 0 | List devActions = dMetadata.getDialogActions(); |
282 | 0 | for (int i = 0; i < devActions.size(); i++) { |
283 | 0 | SCXMLAction scxmlAction = (SCXMLAction) devActions.get(i); |
284 | 0 | String actionname = scxmlAction.getName(); |
285 | 0 | String uri = scxmlAction.getUri(); |
286 | 0 | String actionFQCN = scxmlAction.getActionclassname(); |
287 | 0 | if (actionname == null || uri == null || actionFQCN == null) { |
288 | // shouldn't happen if dialog-config is validated | |
289 | 0 | throw new IllegalArgumentException("A custom Commons" |
290 | + " SCXML action (<scxmlaction> element) in the" | |
291 | + " dialog configuration is missing the 'name'," | |
292 | + " 'uri' or 'actionclassname'"); | |
293 | } | |
294 | 0 | Class customActionClass = null; |
295 | try { | |
296 | 0 | customActionClass = loader.loadClass(actionFQCN); |
297 | 0 | } catch (Exception e) { |
298 | 0 | throw new IllegalArgumentException("Cannot load " |
299 | + "custom Commons SCXML action class '" | |
300 | + actionFQCN + "' for action with name '" | |
301 | + actionname + "'"); | |
302 | 0 | } |
303 | 0 | CustomAction customAction = new CustomAction(uri, |
304 | actionname, customActionClass); | |
305 | 0 | customDialogActions.add(customAction); |
306 | } | |
307 | ||
308 | 0 | URL resource = new URL(getResource(), scxmlconfig); |
309 | ||
310 | 0 | SCXML dialog = null; |
311 | try { | |
312 | // Parse document, with rules for custom actions in place | |
313 | 0 | dialog = SCXMLDigester.digest(resource, |
314 | new SimpleErrorHandler(), customDialogActions); | |
315 | 0 | } catch (ModelException me) { |
316 | 0 | throw new SAXException(me.getMessage(), me); |
317 | 0 | } |
318 | ||
319 | 0 | dMetadata.setStateMachine(dialog); |
320 | 0 | dialogs.put(name, dMetadata); |
321 | ||
322 | 0 | } |
323 | ||
324 | 0 | } |
325 | ||
326 | ||
327 | // -------------------------------------------- Private Rule Implementations | |
328 | ||
329 | ||
330 | /** | |
331 | * <p>Custom <code>Digester</code> rule to add a dialog.</p> | |
332 | */ | |
333 | 0 | static class AddDialogMetadataRule extends Rule { |
334 | ||
335 | /** | |
336 | * Constructor. | |
337 | */ | |
338 | public AddDialogMetadataRule() { | |
339 | 0 | super(); |
340 | 0 | } |
341 | ||
342 | /** | |
343 | * {@inheritDoc} | |
344 | * | |
345 | * @see Rule#end(String,String) | |
346 | */ | |
347 | public void end(String namespace, String name) throws Exception { | |
348 | ||
349 | 0 | DialogMetadata dialog = (DialogMetadata) getDigester().peek(); |
350 | 0 | Map map = (Map) getDigester().peek(1); |
351 | 0 | map.put(dialog.getName(), dialog); |
352 | ||
353 | 0 | } |
354 | ||
355 | } | |
356 | ||
357 | } | |
358 |