View Javadoc

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 }