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 = constructUrl(rootUrl);
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 constructUrl(final URL rootUrl) throws MalformedURLException
209   {
210     final String urlString = rootUrl.toExternalForm();
211     final int index = urlString.indexOf("/WEB-INF/classes");
212     if (index != -1)
213     {
214       final String baseUrl = urlString.substring(0, index);
215       return new URL(baseUrl + '/' + JarFile.MANIFEST_NAME);
216     }
217     return new URL(rootUrl, JarFile.MANIFEST_NAME);
218   }
219 
220   private URL selectUrl(final Class<?> locator, final URL selectorUrl)
221   {
222     try
223     {
224       final String selectorUrlString = selectorUrl.toExternalForm();
225       for (final Enumeration<URL> en =
226           locator.getClassLoader().getResources(""); en.hasMoreElements();)
227       {
228         final URL rootUrl = en.nextElement();
229         if (selectorUrlString.startsWith(rootUrl.toExternalForm()))
230         {
231           return calcActualRootUrl(rootUrl);
232         }
233       }
234     }
235     catch (final IOException e)
236     {
237       throw new IllegalStateException(
238           "Cannot determine Manifest file location using selector URL: "
239               + selectorUrl.toExternalForm(), e);
240     }
241 
242     return locator.getClassLoader().getResource("");
243   }
244 
245   private URL calcActualRootUrl(final URL rootUrl)
246   {
247     URL actualUrl = rootUrl;
248     if (preferEarManifest)
249     {
250       final String string = rootUrl.toExternalForm();
251       final int index = string.lastIndexOf(".ear/");
252       if (index != -1)
253       {
254         try
255         {
256           actualUrl = new URL(string.substring(0, index + 5));
257         }
258         catch (final MalformedURLException e)
259         {
260           // Ignore
261         }
262       }
263     }
264     return actualUrl;
265   }
266 
267   /**
268    * Loads the application identifier from the manifest file pointed at from the
269    * given class loader.
270    *
271    * @param classLoader the class loader to load the manifest file.
272    * @return the application identifier.
273    * @throws IllegalStateException if the application identifier cannot be read
274    *           from the manifest.
275    */
276   public ApplicationId load(final ClassLoader classLoader)
277     throws IllegalStateException
278   {
279     final URL url = classLoader.getResource(JarFile.MANIFEST_NAME);
280     return getApplicatioIdFrom(url);
281   }
282 
283   private ApplicationId getApplicatioIdFrom(final URL url)
284   {
285     if (cache != null)
286     {
287       readLock.lock();
288       try
289       {
290         final ApplicationId applicationId = cache.get(url.toExternalForm());
291         return applicationId;
292       }
293       finally
294       {
295         readLock.unlock();
296       }
297     }
298 
299     final ApplicationId applicationId = readManifestFile(url);
300 
301     if (cache != null)
302     {
303       // It is ok to read the application ID more than once.
304       writeLock.lock();
305       try
306       {
307         cache.put(url.toExternalForm(), applicationId);
308       }
309       finally
310       {
311         writeLock.unlock();
312       }
313     }
314 
315     return applicationId;
316   }
317 
318   private ApplicationId readManifestFile(final URL url)
319     throws IllegalArgumentException, IllegalStateException
320   {
321     InputStream inputStream = null;
322     try
323     {
324       inputStream = url.openStream();
325       if (inputStream != null)
326       {
327         inputStream = new BufferedInputStream(inputStream);
328         final Manifest manifest = new Manifest(inputStream);
329 
330         final ApplicationId applicationId = createApplicationId(manifest);
331 
332         return applicationId;
333       }
334 
335       throw new IllegalStateException(
336           "Cannot find Manifest file to determine application ID: "
337               + url.toExternalForm());
338     }
339     catch (final IOException e)
340     {
341       throw new IllegalStateException("Cannot read Manifest file: "
342                                       + url.toExternalForm(), e);
343     }
344     finally
345     {
346       IOUtils.closeQuietly(inputStream);
347     }
348   }
349 
350   private ApplicationId createApplicationId(final Manifest manifest)
351     throws IllegalArgumentException
352   {
353     final Attributes attributes = manifest.getMainAttributes();
354     final String groupId =
355         attributes.getValue(Attributes.Name.IMPLEMENTATION_VENDOR_ID);
356     final String artifactId =
357         attributes.getValue(Attributes.Name.IMPLEMENTATION_TITLE);
358     final String version =
359         attributes.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
360     final ApplicationId applicationId =
361         new ApplicationId(groupId, artifactId, version);
362     return applicationId;
363   }
364 
365   // --- object basics --------------------------------------------------------
366 
367   /**
368    * Returns the string representation of the object.
369    *
370    * @return the string representation of the object.
371    */
372   @Override
373   public String toString()
374   {
375     final StringBuilder buffer = new StringBuilder();
376     readLock.lock();
377     try
378     {
379       for (final Entry<String, ApplicationId> entry : cache.entrySet())
380       {
381         buffer.append(entry.getKey()).append(": ").append(entry.getValue())
382             .append('\n');
383       }
384     }
385     finally
386     {
387       readLock.unlock();
388     }
389     return buffer.toString();
390   }
391 }