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.rest.server.convention; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.io.*; 022import java.util.*; 023import java.util.function.*; 024import java.util.jar.*; 025 026import org.apache.juneau.commons.inject.*; 027import org.apache.juneau.marshall.json.*; 028import org.apache.juneau.rest.server.*; 029 030/** 031 * Flavor-neutral version/build-metadata worker bean shared by the version {@code @Rest} flavors. 032 * 033 * <p> 034 * Holds the capability state (the assembled {@code name}/{@code version}/{@code git*}/{@code buildTime}/ 035 * {@code javaVersion} entry map) and the serve logic (write the map as readable JSON, format-pinned to 036 * {@code application/json} so the endpoint serves JSON even on a vanilla {@link org.apache.juneau.rest.server.servlet.RestServlet} host that 037 * hasn't wired up JSON serializers). The {@link VersionMixin} (mixin), {@link VersionResource} (child), 038 * and {@link VersionServlet} (servlet) flavors are independent {@code @Rest} classes that each hold a 039 * {@code VersionProvider} worker and delegate to it — so the three deployment forms cannot drift, and no 040 * flavor is another flavor's worker. 041 * 042 * <h5 class='section'>See Also:</h5><ul> 043 * <li class='jc'>{@link VersionMixin} 044 * <li class='jc'>{@link VersionResource} 045 * <li class='jc'>{@link VersionServlet} 046 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerComposition">REST Server — Mixins and Multi-Mount Paths</a> 047 * </ul> 048 * 049 * @since 10.0.0 050 */ 051// @formatter:off 052@SuppressWarnings({ 053 "java:S1192" // Duplicate string literals are version-related property keys; intentional 054}) 055public class VersionProvider { 056 057 /** Sentinel value returned for entries that the worker couldn't resolve. */ 058 public static final String UNKNOWN = "(unknown)"; 059 060 /** 061 * Creates a new builder, with {@link Value @Value}-annotated fields populated through 062 * {@link BeanInstantiator} from the active 063 * {@link org.apache.juneau.commons.settings.Settings Settings} chain. 064 * 065 * @return A new builder. 066 */ 067 public static Builder create() { 068 return create(BasicBeanStore.INSTANCE); 069 } 070 071 /** 072 * Creates a new builder using the supplied {@link BeanStore} for {@link Value @Value}-resolution 073 * and dependency injection. 074 * 075 * @param beanStore The bean store to use for dependency injection. 076 * @return A new builder. 077 */ 078 public static Builder create(BeanStore beanStore) { 079 return BeanInstantiator.of(Builder.class, beanStore).run(); 080 } 081 082 private final Map<String,String> info; 083 084 /** 085 * No-arg constructor — reads {@code MANIFEST.MF}, {@code git.properties}, and the JVM 086 * version from the framework classloader. Equivalent to {@code create().build()}. 087 */ 088 public VersionProvider() { 089 this(create()); 090 } 091 092 /** 093 * Builder constructor. 094 * 095 * <p> 096 * Applies the default lookup chain ({@link Builder#fromManifest()} + 097 * {@link Builder#fromGitProperties()} + {@link Builder#fromJavaVersion()}) when no other 098 * builder method has been called — ensuring that a freshly-created builder always produces 099 * a non-empty payload. 100 * 101 * @param builder The builder. Must not be <jk>null</jk>. 102 */ 103 protected VersionProvider(Builder builder) { 104 if (! builder.explicit) 105 builder.fromManifest().fromGitProperties().fromJavaVersion(); 106 info = u(cp(builder.entries)); 107 } 108 109 /** 110 * Serves the assembled metadata as a JSON map. 111 * 112 * <p> 113 * Format-pinned to {@code application/json} — bypasses the host's content negotiation so 114 * the endpoint serves JSON even on a vanilla {@link org.apache.juneau.rest.server.servlet.RestServlet} host that hasn't wired up JSON 115 * serializers explicitly. 116 * 117 * @param res The current REST response. Must not be <jk>null</jk>. 118 * @throws IOException If an I/O error occurs while writing the response. 119 */ 120 public void serve(RestResponse res) throws IOException { 121 try (var w = res.getDirectWriter("application/json")) { 122 JsonSerializer.DEFAULT_READABLE.write(info, w); 123 } 124 } 125 126 /** 127 * Returns the configured info map (test/inspection helper). 128 * 129 * @return The info map. 130 */ 131 public Map<String,String> getInfoMap() { 132 return info; 133 } 134 135 /** 136 * Builder for {@link VersionProvider} instances. 137 */ 138 public static class Builder { 139 140 private final Map<String,String> entries = m(); 141 private boolean explicit; 142 143 /** 144 * Env-driven default for the {@code javaVersion} entry; resolved from the {@code java.version} 145 * system property (defaulting to the literal {@code (unknown)}, matching {@link VersionProvider#UNKNOWN}). 146 * Populated by {@link BeanInstantiator} via {@link Value @Value} field injection. 147 * 148 * <p> 149 * The default is inlined as a string literal rather than referencing {@code UNKNOWN} via 150 * concatenation, per the project-wide rule that every {@code @Value} expression is a 151 * fully self-contained string literal. 152 */ 153 @Value("${java.version:(unknown)}") 154 String javaVersionDefault; 155 156 /** Constructor — protected access for {@link VersionProvider#create()}. */ 157 protected Builder() {} 158 159 /** 160 * Sets a single entry, overwriting any prior value for that key. 161 * 162 * @param key The entry key. Must not be <jk>null</jk> or blank. 163 * @param value The entry value (a <jk>null</jk> value is recorded as {@link VersionProvider#UNKNOWN}). 164 * @return This object. 165 */ 166 public Builder entry(String key, String value) { 167 entries.put(key, value == null ? UNKNOWN : value); 168 explicit = true; 169 return this; 170 } 171 172 /** 173 * Bulk-set entries from a map. 174 * 175 * @param values Entries to add. Can be <jk>null</jk> (no-op). Individual <jk>null</jk> values are recorded as {@link VersionProvider#UNKNOWN}. 176 * @return This object. 177 */ 178 public Builder entries(Map<String,String> values) { 179 if (values != null) 180 values.forEach(this::entry); 181 explicit = true; 182 return this; 183 } 184 185 /** 186 * Reads {@code Implementation-Title}, {@code Implementation-Version}, 187 * {@code Implementation-Vendor}, and {@code Build-Jdk} from 188 * {@code /META-INF/MANIFEST.MF} on the {@link VersionProvider} classloader. 189 * 190 * <p> 191 * Missing keys are recorded as {@link VersionProvider#UNKNOWN}. A missing {@code MANIFEST.MF} resource 192 * is silently skipped. 193 * 194 * @return This object. 195 */ 196 public Builder fromManifest() { 197 return fromManifest(VersionProvider.class.getClassLoader()); 198 } 199 200 /** 201 * Reads manifest attributes from {@code /META-INF/MANIFEST.MF} on the supplied classloader. 202 * 203 * <p> 204 * Useful under Spring Boot fat jars, where the importer's app manifest is reachable from 205 * its own classloader but not the framework's. Missing keys are recorded as 206 * {@link VersionProvider#UNKNOWN}; a missing {@code MANIFEST.MF} resource is silently skipped. 207 * 208 * @param classLoader The classloader to walk. Must not be <jk>null</jk>. 209 * @return This object. 210 */ 211 public Builder fromManifest(ClassLoader classLoader) { 212 var attrs = readManifestAttributes(classLoader); 213 ifNotEmpty(attrs, "Implementation-Title", v -> entry("name", v)); 214 ifNotEmpty(attrs, "Implementation-Version", v -> entry("version", v)); 215 ifNotEmpty(attrs, "Implementation-Vendor", v -> entry("vendor", v)); 216 ifNotEmpty(attrs, "Build-Jdk", v -> entry("javaVersion", v)); 217 entries.putIfAbsent("name", UNKNOWN); 218 entries.putIfAbsent("version", UNKNOWN); 219 explicit = true; 220 return this; 221 } 222 223 /** 224 * Reads a {@link Manifest} that the importer registered as a bean (e.g. 225 * {@code @Bean Manifest appManifest()}). 226 * 227 * @param manifest The manifest to read. Must not be <jk>null</jk>. 228 * @return This object. 229 */ 230 public Builder fromManifest(Manifest manifest) { 231 var main = manifest.getMainAttributes(); 232 var attrs = new HashMap<String,String>(); 233 for (var e : main.entrySet()) 234 attrs.put(e.getKey().toString(), String.valueOf(e.getValue())); 235 ifNotEmpty(attrs, "Implementation-Title", v -> entry("name", v)); 236 ifNotEmpty(attrs, "Implementation-Version", v -> entry("version", v)); 237 ifNotEmpty(attrs, "Implementation-Vendor", v -> entry("vendor", v)); 238 ifNotEmpty(attrs, "Build-Jdk", v -> entry("javaVersion", v)); 239 explicit = true; 240 return this; 241 } 242 243 /** 244 * Reads {@code git.commit.id}, {@code git.branch}, and {@code git.build.time} from 245 * {@code /git.properties} on the {@link VersionProvider} classloader. 246 * 247 * @return This object. 248 */ 249 public Builder fromGitProperties() { 250 return fromGitProperties(VersionProvider.class.getClassLoader()); 251 } 252 253 /** 254 * Reads {@code git.properties} from the supplied classloader. 255 * 256 * @param classLoader The classloader to walk. Must not be <jk>null</jk>. 257 * @return This object. 258 */ 259 public Builder fromGitProperties(ClassLoader classLoader) { 260 var props = readProperties(classLoader, "git.properties"); 261 if (props == null) 262 return this; 263 ifNotEmptyValue(props.getProperty("git.commit.id"), v -> entry("gitCommit", v)); 264 ifNotEmptyValue(props.getProperty("git.commit.id.abbrev"), v -> entries.putIfAbsent("gitCommit", v)); 265 ifNotEmptyValue(props.getProperty("git.branch"), v -> entry("gitBranch", v)); 266 ifNotEmptyValue(props.getProperty("git.build.time"), v -> entry("buildTime", v)); 267 explicit = true; 268 return this; 269 } 270 271 /** 272 * Adds the running JVM's {@code java.version} system property as the {@code javaVersion} 273 * entry, unless an earlier {@link #fromManifest() fromManifest(...)} call already supplied 274 * one from {@code Build-Jdk}. 275 * 276 * @return This object. 277 */ 278 public Builder fromJavaVersion() { 279 entries.putIfAbsent("javaVersion", o(javaVersionDefault).orElse(UNKNOWN)); 280 explicit = true; 281 return this; 282 } 283 284 /** 285 * Builds a {@link VersionProvider} instance. 286 * 287 * @return A configured instance. 288 */ 289 public VersionProvider build() { 290 return new VersionProvider(this); 291 } 292 293 private static Map<String,String> readManifestAttributes(ClassLoader cl) { 294 Manifest fallback = null; 295 try { 296 var resources = cl.getResources("META-INF/MANIFEST.MF"); 297 while (resources.hasMoreElements()) { 298 var u = resources.nextElement(); 299 try (var in = u.openStream()) { 300 var m = new Manifest(in); 301 // Prefer an Implementation-Title-bearing manifest; otherwise hold the 302 // most recent one as a fallback. ClassLoader.getResources walks the 303 // parent chain FIRST, so caller-supplied URLs come last in the 304 // enumeration — last-wins ensures the explicit classloader argument 305 // trumps any unrelated MANIFEST.MF leaked from a parent (e.g. JDK 306 // module manifests in the boot layer, Surefire's manifest-jar, etc.). 307 if (m.getMainAttributes().getValue("Implementation-Title") != null) 308 return toMap(m); 309 fallback = m; 310 } 311 } 312 } catch (IOException e) { 313 // Best-effort path: a malformed classpath manifest yields an empty map rather 314 // than bubbling up an exception during startup. 315 return Map.of(); 316 } 317 return fallback == null ? Map.of() : toMap(fallback); 318 } 319 320 private static Map<String,String> toMap(Manifest m) { 321 var attrs = new HashMap<String,String>(); 322 for (var k : m.getMainAttributes().keySet()) 323 attrs.put(k.toString(), String.valueOf(m.getMainAttributes().get(k))); 324 return attrs; 325 } 326 327 @SuppressWarnings({ 328 "java:S1168" // null is a meaningful "resource absent" sentinel: fromGitProperties() skips entirely (and leaves the explicit flag unset) when no git.properties is present. 329 }) 330 private static Properties readProperties(ClassLoader cl, String path) { 331 try (var in = cl.getResourceAsStream(path)) { 332 if (in == null) 333 return null; 334 var p = new Properties(); 335 p.load(in); 336 return p; 337 } catch (IOException e) { 338 return null; 339 } 340 } 341 342 private static void ifNotEmpty(Map<String,String> attrs, String key, Consumer<String> sink) { 343 var v = attrs.get(key); 344 if (ine(v)) 345 sink.accept(v); 346 } 347 348 private static void ifNotEmptyValue(String v, Consumer<String> sink) { 349 if (ine(v)) 350 sink.accept(v); 351 } 352 } 353}