1 /*
2 * Copyright 2006-2016 The JGUIraffe Team.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License")
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package net.sf.jguiraffe.gui.builder.impl;
17
18 import java.io.IOException;
19 import java.net.MalformedURLException;
20 import java.net.URL;
21 import java.util.Set;
22
23 import net.sf.jguiraffe.di.BeanStore;
24 import net.sf.jguiraffe.di.ClassLoaderProvider;
25 import net.sf.jguiraffe.di.DependencyProvider;
26 import net.sf.jguiraffe.di.InvocationHelper;
27 import net.sf.jguiraffe.di.MutableBeanStore;
28 import net.sf.jguiraffe.di.impl.DefaultClassLoaderProvider;
29 import net.sf.jguiraffe.di.impl.RestrictedDependencyProvider;
30 import net.sf.jguiraffe.gui.builder.BeanBuilder;
31 import net.sf.jguiraffe.gui.builder.BeanBuilderResult;
32 import net.sf.jguiraffe.gui.builder.BuilderException;
33 import net.sf.jguiraffe.gui.builder.di.DIBuilderData;
34 import net.sf.jguiraffe.gui.builder.di.tags.DITagLibrary;
35 import net.sf.jguiraffe.locators.Locator;
36 import net.sf.jguiraffe.locators.LocatorException;
37 import net.sf.jguiraffe.locators.LocatorUtils;
38
39 import org.apache.commons.jelly.JellyContext;
40 import org.apache.commons.jelly.JellyException;
41 import org.apache.commons.jelly.XMLOutput;
42 import org.xml.sax.InputSource;
43
44 /**
45 * <p>
46 * An implementation of the <code>BeanBuilder</code> interface that is able to
47 * process bean definitions defined in a <a
48 * href="http://commons.apache.org/jelly/"> Apache Commons Jelly</a> script.
49 * </p>
50 * <p>
51 * This class prepares a <code>JellyContext</code> object and registers the
52 * <em>dependency injection tag library</em> ({@link DITagLibrary}).
53 * Then it invokes <em>Jelly</em> for evaluating the specified script.
54 * </p>
55 *
56 * @author Oliver Heger
57 * @version $Id: JellyBeanBuilder.java 205 2012-01-29 18:29:57Z oheger $
58 */
59 public class JellyBeanBuilder implements BeanBuilder
60 {
61 /** Stores the name space URI to be used for the DI builder tag library. */
62 private String diBuilderNameSpaceURI;
63
64 /**
65 * Creates a new instance of <code>JellyBeanBuilder</code>. Instances
66 * should only be created using the factory.
67 */
68 protected JellyBeanBuilder()
69 {
70 }
71
72 /**
73 * Returns the name space URI, under which the DI tag library must be
74 * registered.
75 *
76 * @return the name space URI for the DI tag library
77 */
78 public String getDiBuilderNameSpaceURI()
79 {
80 return diBuilderNameSpaceURI;
81 }
82
83 /**
84 * Sets the name space URI for the DI tag library.
85 *
86 * @param diBuilderNameSpaceURI the new name space URI
87 */
88 public void setDiBuilderNameSpaceURI(String diBuilderNameSpaceURI)
89 {
90 this.diBuilderNameSpaceURI = diBuilderNameSpaceURI;
91 }
92
93 /**
94 * {@inheritDoc} This implementation just calls the other {@code build()}
95 * method passing in a <b>null</b> {@link InvocationHelper}.
96 */
97 public BeanBuilderResult build(Locator script, MutableBeanStore rootStore,
98 ClassLoaderProvider loaderProvider) throws BuilderException
99 {
100 return build(script, rootStore, loaderProvider, null);
101 }
102
103 /**
104 * {@inheritDoc} Delegates to {@code #executeScript()} which does the real
105 * work.
106 */
107 public BeanBuilderResult build(Locator script, MutableBeanStore rootStore,
108 ClassLoaderProvider loaderProvider, InvocationHelper invHlp)
109 throws BuilderException
110 {
111 JellyContext context = setUpJellyContext();
112 return executeScript(script, context, rootStore, loaderProvider, invHlp);
113 }
114
115 /**
116 * Releases the specified {@code BeanBuilderResult} object. This will
117 * especially invoke the {@code shutdown()} method on all bean providers
118 * stored in one of the {@code BeanContext} objects contained in the result
119 * object.
120 *
121 * @param builderResult the {@code BeanBuilderResult} object to be released
122 * @throws IllegalArgumentException if the passed in result object is
123 * <b>null</b> or invalid
124 */
125 public void release(BeanBuilderResult builderResult)
126 {
127 if (builderResult == null)
128 {
129 throw new IllegalArgumentException(
130 "Builder result must not be null!");
131 }
132
133 DependencyProvider depProvider =
134 createReleaseDependencyProvider(builderResult);
135 boolean foundRootStore = false;
136 for (String storeName : builderResult.getBeanStoreNames())
137 {
138 releaseBeanStore(builderResult.getBeanStore(storeName), depProvider);
139 if (storeName == null)
140 {
141 foundRootStore = true;
142 }
143 }
144 if (!foundRootStore)
145 {
146 // release the root bean store manually
147 releaseBeanStore(builderResult.getBeanStore(null), depProvider);
148 }
149 }
150
151 /**
152 * Executes the specified script on the given Jelly context. Occurring Jelly
153 * exceptions are caught and re-thrown as builder exceptions.
154 *
155 * @param script the script to be executed (must not be <b>null</b>)
156 * @param context the Jelly context
157 * @param rootStore the root bean store (can be <b>null</b>)
158 * @param loaderProvider a data object with the registered class loaders
159 * (can be <b>null</b>)
160 * @param invHlp a helper object for reflection operations (can be
161 * <b>null</b>)
162 * @return an object with the results of the builder operation
163 * @throws BuilderException if an error occurs while executing the script
164 * @throws IllegalArgumentException if the script is <b>null</b>
165 */
166 protected BeanBuilderResult executeScript(Locator script,
167 JellyContext context, MutableBeanStore rootStore,
168 ClassLoaderProvider loaderProvider, InvocationHelper invHlp)
169 throws BuilderException
170 {
171 if (script == null)
172 {
173 throw new IllegalArgumentException("Script must not be null!");
174 }
175 DIBuilderData builderData =
176 createBuilderData(context, rootStore, loaderProvider, invHlp);
177
178 try
179 {
180 InputSource source = prepareInputSource(script);
181 context.runScript(source, XMLOutput.createDummyXMLOutput());
182 return new BeanBuilderResultImpl(builderData);
183 }
184 catch (JellyException jex)
185 {
186 throw new BuilderException(extractScriptURL(script),
187 "Error when executing builder script", jex);
188 }
189 catch (IOException ioex)
190 {
191 throw new BuilderException(extractScriptURL(script),
192 "IO error when executing builder script", ioex);
193 }
194 catch (LocatorException locex)
195 {
196 throw new BuilderException(extractScriptURL(script),
197 "Locator threw an exception", locex);
198 }
199 }
200
201 /**
202 * Creates the {@code DIBuilderData} object used during the builder
203 * operation. This object holds central data required by multiple components
204 * involved in the builder operation.
205 *
206 * @param context the Jelly context
207 * @param rootStore the root bean store
208 * @param loaderProvider the class loader provider
209 * @param invHlp the invocation helper
210 * @return the newly created {@code DIBuilderData} object
211 */
212 protected DIBuilderData createBuilderData(JellyContext context,
213 MutableBeanStore rootStore, ClassLoaderProvider loaderProvider,
214 InvocationHelper invHlp)
215 {
216 DIBuilderData builderData = new DIBuilderData();
217 builderData.initRootBeanStore(rootStore);
218 builderData
219 .setClassLoaderProvider(fetchClassLoaderProvider(loaderProvider));
220 builderData.setInvocationHelper(invHlp);
221 builderData.put(context);
222 return builderData;
223 }
224
225 /**
226 * Prepares an <code>InputSource</code> object for the specified
227 * <code>Locator</code>. This method is called by
228 * <code>executeScript()</code>. The resulting <code>InputSource</code> is
229 * then passed to Jelly for processing the represented script. Note that the
230 * way Jelly deals with URLs is not compatible with <code>Locator</code>
231 * implementations derived from <code>ByteArrayLocator</code> (in-memory
232 * locators). This is due to the fact that the URL is first transformed into
233 * a string and later back into a URL. This implementation tries to work
234 * around this problem by creating the <code>InputSource</code> from the
235 * stream the <code>Locator</code> provides. If the URL can be transformed
236 * to a string and back to a URL, it is also set as the system ID of the
237 * input source (this makes it possible to resolve relative files).
238 *
239 * @param script the <code>Locator</code> pointing the the Jelly script
240 * @return an <code>InputSource</code> for this script
241 * @throws IOException if an IO error occurs
242 */
243 protected InputSource prepareInputSource(Locator script) throws IOException
244 {
245 InputSource source = new InputSource(LocatorUtils.openStream(script));
246 String sysID = script.getURL().toString();
247 try
248 {
249 new URL(sysID); // test whether a valid URL can be constructed
250 source.setSystemId(sysID);
251 }
252 catch (MalformedURLException mex)
253 {
254 // This is a special URL => don't set a system ID
255 }
256
257 return source;
258 }
259
260 /**
261 * Creates and initializes the Jelly context to be used for executing the
262 * builder script. This method will also register the required tag libraries
263 * and perform other mandatory initialization steps.
264 *
265 * @return the fully initialized context
266 */
267 protected JellyContext setUpJellyContext()
268 {
269 JellyContext result = createJellyContext();
270 registerTagLibraries(result);
271 return result;
272 }
273
274 /**
275 * Creates the Jelly context for executing the builder script. This method
276 * is called by <code>setUpJellyContext()</code>. Its task is only the
277 * creation of the context, not its initialization.
278 *
279 * @return the newly created context
280 */
281 protected JellyContext createJellyContext()
282 {
283 return new JellyContext();
284 }
285
286 /**
287 * Registers the required builder tag libraries at the given context. This
288 * method is called by <code>setUpJellyContext()</code> before the builder
289 * script will be executed.
290 *
291 * @param context the context
292 */
293 protected void registerTagLibraries(JellyContext context)
294 {
295 context.registerTagLibrary(getDiBuilderNameSpaceURI(),
296 new DITagLibrary());
297 }
298
299 /**
300 * Returns a {@code ClassLoaderProvider}. Some of the builder methods need a
301 * {@code ClassLoaderProvider}, but the corresponding parameter is optional.
302 * This method is called to obtain a valid {@code ClassLoaderProvider}
303 * reference. If the passed in {@code ClassLoaderProvider} object is
304 * defined, it is directly returned. Otherwise a default {@code
305 * ClassLoaderProvider} is created, which does not has any registered class
306 * loaders.
307 *
308 * @param clp the input {@code ClassLoaderProvider}
309 * @return the {@code ClassLoaderProvider} to be used
310 */
311 protected ClassLoaderProvider fetchClassLoaderProvider(
312 ClassLoaderProvider clp)
313 {
314 return (clp != null) ? clp : new DefaultClassLoaderProvider();
315 }
316
317 /**
318 * Creates a {@code DependencyProvider} object that can be used during a
319 * {@code release()} operation. This method creates a restricted dependency
320 * provider that can be used for executing simple shutdown scripts, but does
321 * not support access to external beans.
322 *
323 * @param result the {@code BeanBuilderResult} object that is to be released
324 * @return the {@code DependencyProvider} for release operations
325 * @throws IllegalArgumentException if helper objects required for the
326 * dependency provider are undefined
327 */
328 protected DependencyProvider createReleaseDependencyProvider(
329 BeanBuilderResult result)
330 {
331 assert result != null : "Result object is null!";
332 ClassLoaderProvider clp = result.getClassLoaderProvider();
333 if (clp == null)
334 {
335 throw new IllegalArgumentException(
336 "No ClassLoaderProvider found in builder result!");
337 }
338 InvocationHelper ih = result.getInvocationHelper();
339 if (ih == null)
340 {
341 throw new IllegalArgumentException(
342 "No InvocationHelper found in builder result!");
343 }
344
345 return new RestrictedDependencyProvider(clp, ih);
346 }
347
348 /**
349 * Releases the specified {@code BeanStore}. This method will invoke the
350 * {@code shutdown()} method on all bean providers found in the store.
351 *
352 * @param store the store to be released
353 * @param depProvider the dependency provider
354 */
355 private void releaseBeanStore(BeanStore store,
356 DependencyProvider depProvider)
357 {
358 for (String name : store.providerNames())
359 {
360 store.getBeanProvider(name).shutdown(depProvider);
361 }
362 }
363
364 /**
365 * Extracts the script URL from the given {@code Locator}. Occurring
366 * exceptions are caught; in this case result is <b>null</b>. (This method
367 * is just to provide details for exception messages; therefore it is not
368 * necessary to handle exceptions more gracefully.)
369 *
370 * @param script the {@code Locator}
371 * @return the URL of this {@code Locator} or <b>null</b>
372 */
373 private static URL extractScriptURL(Locator script)
374 {
375 try
376 {
377 return script.getURL();
378 }
379 catch (LocatorException locex)
380 {
381 return null;
382 }
383 }
384
385 /**
386 * An implementation of the {@code BeanBuilderResult} interface
387 * specific for this builder implementation. This class delegates to a
388 * {@link DIBuilderData} object, which holds the actual data.
389 */
390 private static class BeanBuilderResultImpl implements BeanBuilderResult
391 {
392 /** Stores the wrapped builder data object. */
393 private final DIBuilderData builderData;
394
395 /**
396 * Creates a new instance of <code>BeanBuilderResultImpl</code> and
397 * initializes it with the builder data object.
398 *
399 * @param data the wrapped builder data object
400 */
401 public BeanBuilderResultImpl(DIBuilderData data)
402 {
403 builderData = data;
404 }
405
406 /**
407 * Returns the bean store with the given name.
408 *
409 * @param name the name of the bean store
410 * @return the store with this name
411 * @throws java.util.NoSuchElementException if the name is unknown
412 */
413 public BeanStore getBeanStore(String name)
414 {
415 return builderData.getBeanStore(name);
416 }
417
418 /**
419 * Returns an set with the names of all defined bean stores.
420 *
421 * @return the names of the defined bean stores
422 */
423 public Set<String> getBeanStoreNames()
424 {
425 return builderData.getBeanStoreNames();
426 }
427
428 /**
429 * Returns the {@code ClassLoaderProvider} used during the build.
430 *
431 * @return the {@code ClassLoaderProvider}
432 */
433 public ClassLoaderProvider getClassLoaderProvider()
434 {
435 return builderData.getClassLoaderProvider();
436 }
437
438 /**
439 * Returns the {@code InvocationHelper} used during the build.
440 *
441 * @return the {@code InvocationHelper}
442 */
443 public InvocationHelper getInvocationHelper()
444 {
445 return builderData.getInvocationHelper();
446 }
447 }
448 }