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}