Coverage Report - com.jcabi.manifests.Manifests
 
Classes in this File Line Coverage Branch Coverage Complexity
Manifests
62%
47/75
63%
14/22
1.905
 
 1  
 /**
 2  
  * Copyright (c) 2012-2014, jcabi.com
 3  
  * All rights reserved.
 4  
  *
 5  
  * Redistribution and use in source and binary forms, with or without
 6  
  * modification, are permitted provided that the following conditions
 7  
  * are met: 1) Redistributions of source code must retain the above
 8  
  * copyright notice, this list of conditions and the following
 9  
  * disclaimer. 2) Redistributions in binary form must reproduce the above
 10  
  * copyright notice, this list of conditions and the following
 11  
  * disclaimer in the documentation and/or other materials provided
 12  
  * with the distribution. 3) Neither the name of the jcabi.com nor
 13  
  * the names of its contributors may be used to endorse or promote
 14  
  * products derived from this software without specific prior written
 15  
  * permission.
 16  
  *
 17  
  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 18  
  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
 19  
  * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 20  
  * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 21  
  * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 22  
  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 23  
  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 24  
  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 25  
  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 26  
  * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 27  
  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 28  
  * OF THE POSSIBILITY OF SUCH DAMAGE.
 29  
  */
 30  
 package com.jcabi.manifests;
 31  
 
 32  
 import com.jcabi.log.Logger;
 33  
 import java.io.File;
 34  
 import java.io.IOException;
 35  
 import java.io.InputStream;
 36  
 import java.util.Collection;
 37  
 import java.util.HashMap;
 38  
 import java.util.Map;
 39  
 import java.util.Set;
 40  
 import java.util.TreeSet;
 41  
 import java.util.concurrent.ConcurrentHashMap;
 42  
 import java.util.concurrent.ConcurrentMap;
 43  
 import java.util.jar.Attributes;
 44  
 import java.util.jar.Manifest;
 45  
 import javax.servlet.ServletContext;
 46  
 
 47  
 /**
 48  
  * Static reader of {@code META-INF/MANIFEST.MF} files.
 49  
  *
 50  
  * The class provides convenient methods to read
 51  
  * all {@code MANIFEST.MF} files available in classpath
 52  
  * and all attributes from them.
 53  
  *
 54  
  * <p>This mechanism may be very useful for transferring
 55  
  * information from continuous integration environment to the production
 56  
  * environment. For example, you want your site to show project version and
 57  
  * the date of {@code WAR} file packaging. First, you configure
 58  
  * {@code maven-war-plugin} to add this information to {@code MANIFEST.MF}:
 59  
  *
 60  
  * <pre> &lt;plugin>
 61  
  *  &lt;artifactId>maven-war-plugin&lt;/artifactId>
 62  
  *  &lt;configuration>
 63  
  *   &lt;archive>
 64  
  *    &lt;manifestEntries>
 65  
  *     &lt;Foo-Version>${project.version}&lt;/Foo-Version>
 66  
  *     &lt;Foo-Date>${maven.build.timestamp}&lt;/Foo-Date>
 67  
  *    &lt;/manifestEntries>
 68  
  *   &lt;/archive>
 69  
  *  &lt;/configuration>
 70  
  * &lt;/plugin></pre>
 71  
  *
 72  
  * <p>{@code maven-war-plugin} will add these attributes to your
 73  
  * {@code MANIFEST.MF} file and the
 74  
  * project will be deployed to the production environment. Then, you can read
 75  
  * these attributes where it's necessary (in one of your JAXB annotated objects,
 76  
  * for example) and show to users:
 77  
  *
 78  
  * <pre> import com.jcabi.manifests.Manifest;
 79  
  * import java.text.SimpleDateFormat;
 80  
  * import java.util.Date;
 81  
  * import java.util.Locale;
 82  
  * import javax.xml.bind.annotation.XmlElement;
 83  
  * import javax.xml.bind.annotation.XmlRootElement;
 84  
  * &#64;XmlRootElement
 85  
  * public final class Page {
 86  
  *   &#64;XmlElement
 87  
  *   public String version() {
 88  
  *     return Manifests.read("Foo-Version");
 89  
  *   }
 90  
  *   &#64;XmlElement
 91  
  *   public Date date() {
 92  
  *     return new SimpleDateFormat("yyyy.MM.dd", Locale.ENGLISH).parse(
 93  
  *       Manifests.read("Foo-Date");
 94  
  *     );
 95  
  *   }
 96  
  * }</pre>
 97  
  *
 98  
  * <p>If you want to add more manifests to the collection, use
 99  
  * its static instance:
 100  
  *
 101  
  * <pre>Manifests.DEFAULT.append(new FilesMfs(new File("MANIFEST.MF")));</pre>
 102  
  *
 103  
  * <p>You can also modify the map directly:
 104  
  *
 105  
  * <pre>Manifests.DEFAULT.put("Hello", "world");</pre>
 106  
  *
 107  
  * <p>The only dependency you need (check the latest version at
 108  
  * <a href="http://manifests.jcabi.com/">jcabi-manifests</a>):
 109  
  *
 110  
  * <pre> &lt;dependency>
 111  
  *  &lt;groupId>com.jcabi&lt;/groupId>
 112  
  *  &lt;artifactId>jcabi-manifests&lt;/artifactId>
 113  
  * &lt;/dependency></pre>
 114  
  *
 115  
  * @author Yegor Bugayenko (yegor@tpc2.com)
 116  
  * @version $Id$
 117  
  * @since 0.7
 118  
  * @see <a href="http://download.oracle.com/javase/1,5.0/docs/guide/jar/jar.html#JAR%20Manifest">JAR Manifest</a>
 119  
  * @see <a href="http://maven.apache.org/shared/maven-archiver/index.html">Maven Archiver</a>
 120  
  * @see <a href="http://manifests.jcabi.com/index.html">manifests.jcabi.com</a>
 121  
  * @see <a href="http://www.yegor256.com/2014/07/03/how-to-read-manifest-mf.html">How to Read MANIFEST.MF Files</a>
 122  
  */
 123  1
 @SuppressWarnings("PMD.TooManyMethods")
 124  
 public final class Manifests implements MfMap {
 125  
 
 126  
     /**
 127  
      * Default singleton.
 128  
      */
 129  1
     public static final MfMap DEFAULT = new Manifests();
 130  
 
 131  
     /**
 132  
      * Attributes retrieved.
 133  
      */
 134  
     private final transient Map<String, String> attributes;
 135  
 
 136  
     static {
 137  
         try {
 138  1
             Manifests.DEFAULT.append(new ClasspathMfs());
 139  0
         } catch (final IOException ex) {
 140  0
             Logger.error(
 141  
                 Manifests.class,
 142  
                 "#load(): '%s' failed %[exception]s", ex
 143  
             );
 144  1
         }
 145  1
     }
 146  
 
 147  
     /**
 148  
      * Public ctor.
 149  
      * @since 1.0
 150  
      */
 151  
     public Manifests() {
 152  4
         this(new HashMap<String, String>(0));
 153  4
     }
 154  
 
 155  
     /**
 156  
      * Public ctor.
 157  
      * @param attrs Attributes to encapsulate
 158  
      * @since 1.0
 159  
      */
 160  
     public Manifests(final Map<String, String> attrs) {
 161  4
         super();
 162  4
         this.attributes = new ConcurrentHashMap<String, String>(attrs);
 163  4
     }
 164  
 
 165  
     @Override
 166  
     public int size() {
 167  2
         return this.attributes.size();
 168  
     }
 169  
 
 170  
     @Override
 171  
     public boolean isEmpty() {
 172  0
         return this.attributes.isEmpty();
 173  
     }
 174  
 
 175  
     @Override
 176  
     public boolean containsKey(final Object key) {
 177  4
         return this.attributes.containsKey(key);
 178  
     }
 179  
 
 180  
     @Override
 181  
     public boolean containsValue(final Object value) {
 182  0
         return this.attributes.containsValue(value);
 183  
     }
 184  
 
 185  
     @Override
 186  
     public String get(final Object key) {
 187  3
         return this.attributes.get(key);
 188  
     }
 189  
 
 190  
     @Override
 191  
     public String put(final String key, final String value) {
 192  0
         return this.attributes.put(key, value);
 193  
     }
 194  
 
 195  
     @Override
 196  
     public String remove(final Object key) {
 197  0
         return this.attributes.remove(key);
 198  
     }
 199  
 
 200  
     @Override
 201  
     public void putAll(final Map<? extends String, ? extends String> attrs) {
 202  0
         this.attributes.putAll(attrs);
 203  0
     }
 204  
 
 205  
     @Override
 206  
     public void clear() {
 207  0
         this.attributes.clear();
 208  0
     }
 209  
 
 210  
     @Override
 211  
     public Set<String> keySet() {
 212  2
         return this.attributes.keySet();
 213  
     }
 214  
 
 215  
     @Override
 216  
     public Collection<String> values() {
 217  0
         return this.attributes.values();
 218  
     }
 219  
 
 220  
     @Override
 221  
     public Set<Map.Entry<String, String>> entrySet() {
 222  0
         return this.attributes.entrySet();
 223  
     }
 224  
 
 225  
     @Override
 226  
     public MfMap append(final Mfs streams) throws IOException {
 227  4
         final long start = System.currentTimeMillis();
 228  4
         final Collection<InputStream> list = streams.fetch();
 229  4
         int saved = 0;
 230  4
         int ignored = 0;
 231  4
         for (final InputStream stream : list) {
 232  
             for (final Map.Entry<String, String> attr
 233  36
                 : Manifests.load(stream).entrySet()) {
 234  326
                 if (this.attributes.containsKey(attr.getKey())) {
 235  285
                     ++ignored;
 236  
                 } else {
 237  41
                     this.attributes.put(attr.getKey(), attr.getValue());
 238  41
                     ++saved;
 239  
                 }
 240  326
             }
 241  35
         }
 242  3
         Logger.info(
 243  
             this,
 244  
             // @checkstyle LineLength (1 line)
 245  
             "%d attributes loaded from %d stream(s) in %[ms]s, %d saved, %d ignored: %[list]s",
 246  
             this.attributes.size(), list.size(),
 247  
             System.currentTimeMillis() - start,
 248  
             saved, ignored,
 249  
             new TreeSet<String>(this.attributes.keySet())
 250  
         );
 251  3
         return this;
 252  
     }
 253  
 
 254  
     /**
 255  
      * Read one attribute available in one of {@code MANIFEST.MF} files.
 256  
      *
 257  
      * <p>If such a attribute doesn't exist {@link IllegalArgumentException}
 258  
      * will be thrown. If you're not sure whether the attribute is present or
 259  
      * not use {@link #exists(String)} beforehand.
 260  
      *
 261  
      * <p>The method is thread-safe.
 262  
      *
 263  
      * @param name Name of the attribute
 264  
      * @return The value of the attribute retrieved
 265  
      */
 266  
     public static String read(final String name) {
 267  3
         if (name == null) {
 268  0
             throw new IllegalArgumentException("attribute can't be NULL");
 269  
         }
 270  3
         if (name.isEmpty()) {
 271  0
             throw new IllegalArgumentException("attribute can't be empty");
 272  
         }
 273  3
         if (!Manifests.exists(name)) {
 274  2
             throw new IllegalArgumentException(
 275  
                 Logger.format(
 276  
                     // @checkstyle LineLength (1 line)
 277  
                     "Attribute '%s' not found in MANIFEST.MF file(s) among %d other attribute(s): %[list]s",
 278  
                     name,
 279  
                     Manifests.DEFAULT.size(),
 280  
                     new TreeSet<String>(Manifests.DEFAULT.keySet())
 281  
                 )
 282  
             );
 283  
         }
 284  1
         return Manifests.DEFAULT.get(name);
 285  
     }
 286  
 
 287  
     /**
 288  
      * Check whether attribute exists in any of {@code MANIFEST.MF} files.
 289  
      *
 290  
      * <p>Use this method before {@link #read(String)} to check whether an
 291  
      * attribute exists, in order to avoid a runtime exception.
 292  
      *
 293  
      * <p>The method is thread-safe.
 294  
      *
 295  
      * @param name Name of the attribute to check
 296  
      * @return Returns {@code TRUE} if it exists, {@code FALSE} otherwise
 297  
      */
 298  
     public static boolean exists(final String name) {
 299  3
         if (name == null) {
 300  0
             throw new IllegalArgumentException("attribute name can't be NULL");
 301  
         }
 302  3
         if (name.isEmpty()) {
 303  0
             throw new IllegalArgumentException("attribute name can't be empty");
 304  
         }
 305  3
         return Manifests.DEFAULT.containsKey(name);
 306  
     }
 307  
 
 308  
     /**
 309  
      * Append attributes from the web application {@code MANIFEST.MF}.
 310  
      *
 311  
      * <p>The method is deprecated. Instead, use this code:
 312  
      *
 313  
      * <pre>Manifests.DEFAULT.append(new ServletMfs());</pre>
 314  
      *
 315  
      * @param ctx Servlet context
 316  
      * @see #Manifests()
 317  
      * @throws IOException If some I/O problem inside
 318  
      * @deprecated Use {@link #append(Mfs)} and {@link ServletMfs} instead
 319  
      */
 320  
     @Deprecated
 321  
     public static void append(final ServletContext ctx) throws IOException {
 322  0
         Manifests.DEFAULT.append(new ServletMfs(ctx));
 323  0
     }
 324  
 
 325  
     /**
 326  
      * Append attributes from the file.
 327  
      *
 328  
      * <p>The method is deprecated. Instead, use this code:
 329  
      *
 330  
      * <pre>Manifests.DEFAULT.append(new FilesMfs(file));</pre>
 331  
      *
 332  
      * @param file The file to load attributes from
 333  
      * @throws IOException If some I/O problem inside
 334  
      * @deprecated Use {@link #append(Mfs)} and {@link FilesMfs} instead
 335  
      */
 336  
     @Deprecated
 337  
     public static void append(final File file) throws IOException {
 338  0
         if (file == null) {
 339  0
             throw new IllegalArgumentException("file can't be NULL");
 340  
         }
 341  0
         Manifests.DEFAULT.append(new FilesMfs(file));
 342  0
     }
 343  
 
 344  
     /**
 345  
      * Append attributes from input stream.
 346  
      *
 347  
      * <p>The method is deprecated. Instead, use this code:
 348  
      *
 349  
      * <pre>Manifests.DEFAULT.append(new StreamsMfs(stream));</pre>
 350  
      *
 351  
      * @param stream Stream to use
 352  
      * @throws IOException If some I/O problem inside
 353  
      * @since 0.8
 354  
      * @deprecated Use {@link #append(Mfs)} and {@link StreamsMfs} instead
 355  
      */
 356  
     @Deprecated
 357  
     public static void append(final InputStream stream) throws IOException {
 358  0
         if (stream == null) {
 359  0
             throw new IllegalArgumentException("input stream can't be NULL");
 360  
         }
 361  0
         Manifests.DEFAULT.append(new StreamsMfs(stream));
 362  0
     }
 363  
 
 364  
     /**
 365  
      * Load attributes from input stream.
 366  
      *
 367  
      * <p>Inside the method we catch {@code RuntimeException} (which may look
 368  
      * suspicious) in order to protect our execution flow from expected (!)
 369  
      * exceptions from {@link Manifest#getMainAttributes()}. For some reason,
 370  
      * this JDK method doesn't throw checked exceptions if {@code MANIFEST.MF}
 371  
      * file format is broken. Instead, it throws a runtime exception (an
 372  
      * unchecked one), which we should catch in such an inconvenient way.
 373  
      *
 374  
      * @param stream Stream to load from
 375  
      * @return The attributes loaded
 376  
      * @throws IOException If some problem happens
 377  
      * @since 0.8
 378  
      */
 379  
     @SuppressWarnings("PMD.AvoidCatchingGenericException")
 380  
     private static Map<String, String> load(final InputStream stream)
 381  
         throws IOException {
 382  36
         final ConcurrentMap<String, String> props =
 383  
             new ConcurrentHashMap<String, String>(0);
 384  
         try {
 385  36
             final Manifest manifest = new Manifest(stream);
 386  35
             final Attributes attrs = manifest.getMainAttributes();
 387  35
             for (final Object key : attrs.keySet()) {
 388  326
                 final String value = attrs.getValue(
 389  
                     Attributes.Name.class.cast(key)
 390  
                 );
 391  326
                 props.put(key.toString(), value);
 392  326
             }
 393  35
             Logger.debug(
 394  
                 Manifests.class,
 395  
                 "%d attribute(s) loaded %[list]s",
 396  
                 props.size(), new TreeSet<String>(props.keySet())
 397  
             );
 398  
         // @checkstyle IllegalCatch (1 line)
 399  0
         } catch (final RuntimeException ex) {
 400  0
             Logger.error(Manifests.class, "#load(): failed %[exception]s", ex);
 401  
         } finally {
 402  36
             stream.close();
 403  35
         }
 404  35
         return props;
 405  
     }
 406  
 
 407  
 }