View Javadoc

1   /*
2    * Copyright 2012-2013 smartics, Kronseder & Reiner GmbH
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 de.smartics.properties.api.config.domain.key;
17  
18  import java.io.BufferedInputStream;
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.net.MalformedURLException;
22  import java.net.URL;
23  import java.util.Enumeration;
24  import java.util.HashMap;
25  import java.util.Map;
26  import java.util.Map.Entry;
27  import java.util.concurrent.locks.Lock;
28  import java.util.concurrent.locks.ReentrantReadWriteLock;
29  import java.util.jar.Attributes;
30  import java.util.jar.JarFile;
31  import java.util.jar.Manifest;
32  
33  import javax.annotation.concurrent.ThreadSafe;
34  
35  import org.apache.commons.io.IOUtils;
36  
37  /**
38   * Loads the application identifier from the Manifest file.
39   */
40  @ThreadSafe
41  public final class ApplicationIdLoader
42  {
43    // ********************************* Fields *********************************
44  
45    // --- constants ------------------------------------------------------------
46  
47    // --- members --------------------------------------------------------------
48  
49    /**
50     * The flag to signal that the manifest of a EAR file (with the extension
51     * <code>ear</code>) is preferred if present.
52     *
53     * @serial
54     */
55    private final boolean preferEarManifest;
56  
57    /**
58     * Cache with the root URL as a key and the application ID as value. The cache
59     * is set to <code>null</code>, if no caching is used.
60     *
61     * @serial
62     */
63    private final Map<String, ApplicationId> cache;
64  
65    /**
66     * The lock for synchronized access.
67     *
68     * @serial
69     */
70    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
71  
72    /**
73     * The read lock for synchronized access.
74     *
75     * @serial
76     */
77    private final Lock readLock = lock.readLock();
78  
79    /**
80     * The write lock for synchronized access.
81     *
82     * @serial
83     */
84    private final Lock writeLock = lock.writeLock();
85  
86    // ****************************** Initializer *******************************
87  
88    // ****************************** Constructors ******************************
89  
90    /**
91     * Convenience constructor preferring manifest information from EARs and no
92     * caching.
93     */
94    public ApplicationIdLoader()
95    {
96      this(true);
97    }
98  
99    /**
100    * Convenience constructor preferring no caching.
101    *
102    * @param preferEarManifest the flag to signal that the manifest of a EAR file
103    *          (with the extension (with the extension <code>ear</code>) is
104    *          preferred if present.
105    */
106   public ApplicationIdLoader(final boolean preferEarManifest)
107   {
108     this(preferEarManifest, false);
109   }
110 
111   /**
112    * Default constructor.
113    *
114    * @param preferEarManifest the flag to signal that the manifest of a EAR file
115    *          (with the extension (with the extension <code>ear</code>) is
116    *          preferred if present.
117    * @param useCache set to <code>true</code> if all loaded application IDs
118    *          should be cached, <code>false</code> otherwise.
119    */
120   public ApplicationIdLoader(final boolean preferEarManifest,
121       final boolean useCache)
122   {
123     this.preferEarManifest = preferEarManifest;
124     this.cache = (useCache ? new HashMap<String, ApplicationId>() : null);
125   }
126 
127   // ****************************** Inner Classes *****************************
128 
129   // ********************************* Methods ********************************
130 
131   // --- init -----------------------------------------------------------------
132 
133   // --- factory --------------------------------------------------------------
134 
135   /**
136    * Returns a caching loader that reads the Manifest file from the JAR that
137    * actually contains the resource.
138    *
139    * @return the loader instance.
140    * @see #ApplicationIdLoader(boolean, boolean)
141    */
142   public static ApplicationIdLoader createCachedJarLoader()
143   {
144     return new ApplicationIdLoader(false, true);
145   }
146 
147   // --- get&set --------------------------------------------------------------
148 
149   // --- business -------------------------------------------------------------
150 
151   /**
152    * Loads the application identifier from the manifest file pointed at by the
153    * {@link Thread#getContextClassLoader() context class loader}.
154    *
155    * @return the application identifier.
156    * @throws IllegalStateException if the application identifier cannot be read
157    *           from the manifest.
158    */
159   public ApplicationId load() throws IllegalStateException
160   {
161     return load(Thread.currentThread().getContextClassLoader());
162   }
163 
164   /**
165    * Loads the application identifier from the manifest file pointed at from the
166    * given class.
167    *
168    * @param locator the class to locate the manifest file to load. It is the
169    *          manifest file of the archive this class is part of.
170    * @return the application identifier.
171    * @throws IllegalStateException if the application identifier cannot be read
172    *           from the manifest.
173    */
174   public ApplicationId load(final Class<?> locator)
175     throws IllegalStateException
176   {
177     final URL selectorUrl = locator.getResource("");
178     final URL rootUrl = selectUrl(locator, selectorUrl);
179     final ApplicationId applicationId = load(rootUrl);
180     return applicationId;
181   }
182 
183   /**
184    * Loads the manifest information as application ID from the given root URL.
185    *
186    * @param rootUrl the URL to a class path root. The
187    *          {@link JarFile#MANIFEST_NAME} will be appended to locate the
188    *          manifest file.
189    * @return the application ID as read from the manifest file found in the
190    *         {@code rootUrl}.
191    */
192   public ApplicationId load(final URL rootUrl)
193   {
194     try
195     {
196       final URL manifestFileUrl = new URL(rootUrl, JarFile.MANIFEST_NAME);
197       final ApplicationId applicationId = getApplicatioIdFrom(manifestFileUrl);
198       return applicationId;
199     }
200     catch (final MalformedURLException e)
201     {
202       throw new IllegalStateException(
203           "Cannot read Manifest file from base URL: "
204               + rootUrl.toExternalForm(), e);
205     }
206   }
207 
208   private URL selectUrl(final Class<?> locator, final URL selectorUrl)
209   {
210     try
211     {
212       final String selectorUrlString = selectorUrl.toExternalForm();
213       for (final Enumeration<URL> en =
214           locator.getClassLoader().getResources(""); en.hasMoreElements();)
215       {
216         final URL rootUrl = en.nextElement();
217         if (selectorUrlString.startsWith(rootUrl.toExternalForm()))
218         {
219           return calcActualRootUrl(rootUrl);
220         }
221       }
222     }
223     catch (final IOException e)
224     {
225       throw new IllegalStateException(
226           "Cannot determine Manifest file location using selector URL: "
227               + selectorUrl.toExternalForm(), e);
228     }
229 
230     return locator.getClassLoader().getResource("");
231   }
232 
233   private URL calcActualRootUrl(final URL rootUrl)
234   {
235     URL actualUrl = rootUrl;
236     if (preferEarManifest)
237     {
238       final String string = rootUrl.toExternalForm();
239       final int index = string.lastIndexOf(".ear/");
240       if (index != -1)
241       {
242         try
243         {
244           actualUrl = new URL(string.substring(0, index + 5));
245         }
246         catch (final MalformedURLException e)
247         {
248           // Ignore
249         }
250       }
251     }
252     return actualUrl;
253   }
254 
255   /**
256    * Loads the application identifier from the manifest file pointed at from the
257    * given class loader.
258    *
259    * @param classLoader the class loader to load the manifest file.
260    * @return the application identifier.
261    * @throws IllegalStateException if the application identifier cannot be read
262    *           from the manifest.
263    */
264   public ApplicationId load(final ClassLoader classLoader)
265     throws IllegalStateException
266   {
267     final URL url = classLoader.getResource(JarFile.MANIFEST_NAME);
268     return getApplicatioIdFrom(url);
269   }
270 
271   private ApplicationId getApplicatioIdFrom(final URL url)
272   {
273     if (cache != null)
274     {
275       readLock.lock();
276       try
277       {
278         final ApplicationId applicationId = cache.get(url.toExternalForm());
279         return applicationId;
280       }
281       finally
282       {
283         readLock.unlock();
284       }
285     }
286 
287     final ApplicationId applicationId = readManifestFile(url);
288 
289     if (cache != null)
290     {
291       // It is ok to read the application ID more than once.
292       writeLock.lock();
293       try
294       {
295         cache.put(url.toExternalForm(), applicationId);
296       }
297       finally
298       {
299         writeLock.unlock();
300       }
301     }
302 
303     return applicationId;
304   }
305 
306   private ApplicationId readManifestFile(final URL url)
307     throws IllegalArgumentException, IllegalStateException
308   {
309     InputStream inputStream = null;
310     try
311     {
312       inputStream = url.openStream();
313       if (inputStream != null)
314       {
315         inputStream = new BufferedInputStream(inputStream);
316         final Manifest manifest = new Manifest(inputStream);
317 
318         final ApplicationId applicationId = createApplicationId(manifest);
319 
320         return applicationId;
321       }
322 
323       throw new IllegalStateException(
324           "Cannot find Manifest file to determine application ID: "
325               + url.toExternalForm());
326     }
327     catch (final IOException e)
328     {
329       throw new IllegalStateException("Cannot read Manifest file: "
330                                       + url.toExternalForm(), e);
331     }
332     finally
333     {
334       IOUtils.closeQuietly(inputStream);
335     }
336   }
337 
338   private ApplicationId createApplicationId(final Manifest manifest)
339     throws IllegalArgumentException
340   {
341     final Attributes attributes = manifest.getMainAttributes();
342     final String groupId =
343         attributes.getValue(Attributes.Name.IMPLEMENTATION_VENDOR_ID);
344     final String artifactId =
345         attributes.getValue(Attributes.Name.IMPLEMENTATION_TITLE);
346     final String version =
347         attributes.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
348     final ApplicationId applicationId =
349         new ApplicationId(groupId, artifactId, version);
350     return applicationId;
351   }
352 
353   // --- object basics --------------------------------------------------------
354 
355   /**
356    * Returns the string representation of the object.
357    *
358    * @return the string representation of the object.
359    */
360   @Override
361   public String toString()
362   {
363     final StringBuilder buffer = new StringBuilder();
364     readLock.lock();
365     try
366     {
367       for (final Entry<String, ApplicationId> entry : cache.entrySet())
368       {
369         buffer.append(entry.getKey()).append(": ").append(entry.getValue())
370             .append('\n');
371       }
372     }
373     finally
374     {
375       readLock.unlock();
376     }
377     return buffer.toString();
378   }
379 }