001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.commons.runtime;
018
019import static org.apache.juneau.commons.utils.IoUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021import static org.apache.juneau.commons.utils.ThrowableUtils.*;
022
023import java.io.*;
024import java.net.*;
025import java.nio.file.*;
026import java.util.*;
027import java.util.jar.*;
028
029/**
030 * Lean accessor for the contents of a Jar manifest file.
031 *
032 * <p>
033 * Wraps a {@link Manifest} so callers can read main-attribute and per-section entries via an
034 * {@link Optional}-returning API without depending on the marshalling stack.
035 *
036 * <h5 class='section'>Example:</h5>
037 * <p class='bjava'>
038 *    <jc>// Load the manifest of the jar containing this class.</jc>
039 *    ManifestFile <jv>mf</jv> = <jk>new</jk> ManifestFile(MyClass.<jk>class</jk>);
040 *
041 *    <jc>// Pull a main attribute.</jc>
042 *    String <jv>version</jv> = <jv>mf</jv>.get(<js>"Bundle-Version"</js>).orElse(<js>"unknown"</js>);
043 *
044 *    <jc>// Pull a section attribute (new capability).</jc>
045 *    String <jv>sectionAttr</jv> = <jv>mf</jv>.get(<js>"my-section"</js>, <js>"some-key"</js>).orElse(<jk>null</jk>);
046 * </p>
047 *
048 * <h5 class='section'>Notes:</h5><ul>
049 *    <li class='note'>This class is immutable and thread-safe once constructed.
050 * </ul>
051 */
052public class ManifestFile {
053
054   private final Map<String,String> mainAttributes;
055   private final Map<String,Map<String,String>> sections;
056
057   /**
058    * Loads the manifest of the jar containing the given class.
059    *
060    * <p>
061    * If the class isn't packaged inside a Jar (e.g. running from an exploded classpath), the resulting
062    * instance has no main attributes and no sections.
063    *
064    * @param c The anchor class.  Must not be <jk>null</jk>.
065    * @throws IOException If the manifest could not be read.
066    */
067   public ManifestFile(Class<?> c) throws IOException {
068      var className = c.getSimpleName() + ".class";
069      var classPath = c.getResource(className).toString();
070      Manifest mf = null;
071      if (classPath.startsWith("jar")) {
072         var manifestPath = classPath.substring(0, classPath.lastIndexOf("!") + 1) + "/META-INF/MANIFEST.MF";
073         try (var in = URI.create(manifestPath).toURL().openStream()) {
074            mf = new Manifest(in);
075         } catch (IllegalArgumentException | MalformedURLException e) {
076            throw castException(IOException.class, e);
077         }
078      }
079      this.mainAttributes = readMain(mf);
080      this.sections = readSections(mf);
081   }
082
083   /**
084    * Loads a manifest from a file on disk.
085    *
086    * @param f The manifest file.  Must not be <jk>null</jk>.
087    * @throws IOException If the manifest could not be read.
088    */
089   public ManifestFile(File f) throws IOException {
090      Manifest mf;
091      try (var fis = new FileInputStream(f)) {
092         mf = new Manifest();
093         mf.read(fis);
094      } catch (IOException e) {
095         throw ioex(e, "Problem detected in MANIFEST.MF.  Contents below:\n%s", read(f));
096      }
097      this.mainAttributes = readMain(mf);
098      this.sections = readSections(mf);
099   }
100
101   /**
102    * Loads a manifest from an input stream.
103    *
104    * <p>
105    * The stream must end in a newline to pick up the last line.
106    *
107    * @param in The manifest stream.  Must not be <jk>null</jk>.
108    * @throws IOException If the manifest could not be read.
109    */
110   public ManifestFile(InputStream in) throws IOException {
111      var mf = new Manifest(in);
112      this.mainAttributes = readMain(mf);
113      this.sections = readSections(mf);
114   }
115
116   /**
117    * Wraps an existing {@link Manifest}.
118    *
119    * @param mf The manifest.  Can be <jk>null</jk>, in which case the manifest is treated as having no attributes or sections.
120    */
121   public ManifestFile(Manifest mf) {
122      this.mainAttributes = readMain(mf);
123      this.sections = readSections(mf);
124   }
125
126   /**
127    * Loads a manifest from a path.
128    *
129    * @param path The manifest path.  Must not be <jk>null</jk>.
130    * @throws IOException If the manifest could not be read.
131    */
132   public ManifestFile(Path path) throws IOException {
133      Manifest mf;
134      try (var in = Files.newInputStream(path)) {
135         mf = new Manifest();
136         mf.read(in);
137      } catch (IOException e) {
138         throw ioex(e, "Problem detected in MANIFEST.MF.  Contents below:\n%s", read(path));
139      }
140      this.mainAttributes = readMain(mf);
141      this.sections = readSections(mf);
142   }
143
144   /**
145    * Loads a manifest from a reader.
146    *
147    * <p>
148    * The reader's contents must end in a newline to pick up the last line.
149    *
150    * @param r The manifest reader.  Must not be <jk>null</jk>.
151    * @throws IOException If the manifest could not be read.
152    */
153   public ManifestFile(Reader r) throws IOException {
154      var mf = new Manifest(new ByteArrayInputStream(read(r).getBytes(UTF8)));
155      this.mainAttributes = readMain(mf);
156      this.sections = readSections(mf);
157   }
158
159   private static Map<String,String> readMain(Manifest mf) {
160      if (mf == null)
161         return Collections.emptyMap();
162      var out = new LinkedHashMap<String,String>();
163      mf.getMainAttributes().forEach((k,v) -> out.put(k.toString(), v.toString()));
164      return u(out);
165   }
166
167   private static Map<String,Map<String,String>> readSections(Manifest mf) {
168      if (mf == null)
169         return Collections.emptyMap();
170      var out = new LinkedHashMap<String,Map<String,String>>();
171      mf.getEntries().forEach((sectionName, attrs) -> {
172         var section = new LinkedHashMap<String,String>();
173         attrs.forEach((k,v) -> section.put(k.toString(), v.toString()));
174         out.put(sectionName, u(section));
175      });
176      return u(out);
177   }
178
179   /**
180    * Returns the value of a main-attribute key.
181    *
182    * @param key The main-attribute key.  Can be <jk>null</jk>, in which case an empty value is returned.
183    * @return The value, or empty if the key is unset.
184    */
185   public Optional<String> get(String key) {
186      return o(mainAttributes.get(key));
187   }
188
189   /**
190    * Returns the value of a key inside the named section.
191    *
192    * @param section The section name.  Can be <jk>null</jk>, in which case an empty value is returned.
193    * @param key The attribute key.  Can be <jk>null</jk>, in which case an empty value is returned.
194    * @return The value, or empty if the section or key is unset.
195    */
196   public Optional<String> get(String section, String key) {
197      var s = sections.get(section);
198      return s == null ? oe() : o(s.get(key));
199   }
200
201   /**
202    * Returns the names of all per-entry sections in the manifest.
203    *
204    * @return An unmodifiable set of section names.
205    */
206   public Set<String> sections() {
207      return sections.keySet();
208   }
209
210   /**
211    * Returns the main attributes as an unmodifiable map.
212    *
213    * @return The main-attribute map.
214    */
215   public Map<String,String> asMap() {
216      return mainAttributes;
217   }
218
219   /**
220    * Returns the attributes of the named section as an unmodifiable map.
221    *
222    * @param section The section name.  Can be <jk>null</jk>, in which case an empty map is returned.
223    * @return The section's attribute map, or an empty map if the section is unknown.
224    */
225   public Map<String,String> asMap(String section) {
226      var s = sections.get(section);
227      return s == null ? Collections.emptyMap() : s;
228   }
229
230   @Override /* Object */
231   public String toString() {
232      var sb = new StringBuilder();
233      mainAttributes.forEach((k,v) -> sb.append(k).append(": ").append(v).append('\n'));
234      return sb.toString();
235   }
236}