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 java.io.*; 020import java.util.*; 021import java.util.jar.*; 022 023import org.apache.juneau.commons.inject.*; 024import org.apache.juneau.rest.server.*; 025import org.apache.juneau.rest.server.servlet.*; 026 027/** 028 * Mixin that serves deployment-introspection metadata at {@code /version} (configurable). 029 * 030 * <p> 031 * Sibling of {@link FaviconMixin}, {@link SeoMixin}, and 032 * {@link WellKnownMixin}. All four classes live in the 033 * {@code org.apache.juneau.rest.server.convention} convention-endpoints mixin pack. 034 * 035 * <p> 036 * Compose into a host resource via 037 * {@link Rest#mixins() @Rest(mixins=VersionMixin.class)}; the {@code /version} URL becomes available 038 * alongside the host's own endpoints with no further wiring. 039 * 040 * <h5 class='section'>Configurable mount path:</h5> 041 * 042 * <p> 043 * The default mount {@code /version} can be overridden via the SVL variable 044 * {@code ${juneau.version.path:version}} — set via system property 045 * ({@code -Djuneau.version.path=build-info}), environment variable 046 * ({@code JUNEAU_VERSION_PATH=build-info}), or {@code Config} key 047 * ({@code juneau.version.path = build-info}) to change the runtime mount without subclassing. 048 * Resolution happens once at {@link RestContext} construction time. 049 * 050 * <p> 051 * Override accepts bare token ({@code version}), leading slash ({@code /version}), or trailing 052 * slash ({@code version/}) — all resolve to the same mount. 053 * 054 * <p> 055 * <b>Migration note (10.0.0):</b> Earlier development snapshots of this mixin mounted at three 056 * historical aliases — {@code /version}, {@code /info}, and {@code /about} — on a 057 * single op. That multi-path default has been collapsed to a single SVL-configurable mount as 058 * part of the "single path per op" principle. Deployers who relied on the 059 * {@code /info} or {@code /about} aliases must now either set 060 * {@code -Djuneau.version.path=info} (or {@code about}), compose a second instance with the 061 * desired override, or subclass and supply their own {@code @RestGet(path=...)} method. 062 * 063 * <h5 class='section'>Mixin-only deployment:</h5> 064 * 065 * <p> 066 * This resource is designed for composition via {@code @Rest(mixins=...)}. The mount path is 067 * pinned at the op level by {@link RestGet @RestGet(path="/{juneau.version.path:version}")} on 068 * {@link #getInfo}; a class-level {@code @Rest(paths=...)} declaration would be silently 069 * ignored under the mixin pattern (see {@link Rest#paths() @Rest(paths)} Javadoc). 070 * 071 * <h5 class='figure'>Composition example:</h5> 072 * 073 * <p class='bjava'> 074 * <ja>@Rest</ja>(path=<js>"/api"</js>, mixins=VersionMixin.<jk>class</jk>) 075 * <jk>public class</jk> ApiResource <jk>extends</jk> RestServlet { } 076 * 077 * <jc>// Default behavior reads MANIFEST.MF + git.properties from the importer's classpath.</jc> 078 * <jc>// Override programmatically via a @Bean factory:</jc> 079 * <ja>@Bean</ja> VersionMixin version() { 080 * <jk>return</jk> VersionMixin.<jsm>create</jsm>() 081 * .entry(<js>"name"</js>, <js>"my-app"</js>) 082 * .entry(<js>"version"</js>, <js>"1.2.3"</js>) 083 * .build(); 084 * } 085 * </p> 086 * 087 * <h5 class='section'>Default lookup chain:</h5> 088 * 089 * <p> 090 * The default {@link Builder#fromManifest() fromManifest()} reader walks the 091 * {@link VersionMixin} classloader for {@code /META-INF/MANIFEST.MF} and reads the standard 092 * {@code Implementation-Title}, {@code Implementation-Version}, {@code Implementation-Vendor}, and 093 * {@code Build-Jdk} attributes (lowercased to {@code name}, {@code version}, {@code vendor}, 094 * {@code javaVersion}). The default {@link Builder#fromGitProperties() fromGitProperties()} reader 095 * walks the same classloader for {@code /git.properties} (the canonical 096 * {@code git-commit-id-maven-plugin} output) and surfaces {@code git.commit.id}, 097 * {@code git.branch}, and {@code git.build.time} as {@code gitCommit}, {@code gitBranch}, and 098 * {@code buildTime}. Missing files / missing keys map to {@code "(unknown)"} or are simply omitted 099 * — never an exception. 100 * 101 * <p> 102 * <b>Spring Boot fat-jar caveat:</b> Spring Boot rewrites {@code MANIFEST.MF} during repackaging. 103 * The mixin's no-arg-default reader uses {@code VersionMixin.class.getClassLoader()}; 104 * users who want the importer's app manifest under a Spring Boot executable jar should register an 105 * explicit {@link Builder#fromManifest(ClassLoader) fromManifest(importerClassLoader)} call so the 106 * lookup starts from the importer's classloader rather than the framework's. 107 * 108 * <h5 class='section'>Output:</h5> 109 * 110 * <p> 111 * The endpoint returns a JSON map — e.g.: 112 * <p class='bjson'> 113 * { 114 * <jok>"name"</jok>: <jov>"my-app"</jov>, 115 * <jok>"version"</jok>: <jov>"1.2.3"</jov>, 116 * <jok>"gitCommit"</jok>: <jov>"abc123"</jov>, 117 * <jok>"gitBranch"</jok>: <jov>"main"</jov>, 118 * <jok>"buildTime"</jok>: <jov>"2026-05-24T18:00:00Z"</jov>, 119 * <jok>"javaVersion"</jok>: <jov>"21"</jov> 120 * } 121 * </p> 122 * 123 * <p> 124 * Endpoints are excluded from generated Swagger / OpenAPI specs via 125 * {@link OpSwagger#ignore() @OpSwagger(ignore=true)}. 126 * 127 * <h5 class='section'>See Also:</h5><ul> 128 * <li class='jc'>{@link FaviconMixin} 129 * <li class='jc'>{@link SeoMixin} 130 * <li class='jc'>{@link WellKnownMixin} 131 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerComposition">REST Server — Mixins and Multi-Mount Paths</a> 132 * </ul> 133 * 134 * @since 10.0.0 135 */ 136// @formatter:off 137@Rest 138@SuppressWarnings({ 139 "java:S1192" // Duplicate string literals are REST endpoint paths and content-type values; intentional 140}) 141public class VersionMixin extends RestMixin { 142 143 /** Sentinel value returned for entries that the worker couldn't resolve. */ 144 public static final String UNKNOWN = VersionProvider.UNKNOWN; 145 146 /** 147 * Creates a new builder, with {@link Value @Value}-annotated fields populated through 148 * {@link BeanInstantiator} from the active 149 * {@link org.apache.juneau.commons.settings.Settings Settings} chain. 150 * 151 * @return A new builder. 152 */ 153 public static Builder create() { 154 return create(BasicBeanStore.INSTANCE); 155 } 156 157 /** 158 * Creates a new builder using the supplied {@link BeanStore} for {@link Value @Value}-resolution 159 * and dependency injection. 160 * 161 * @param beanStore The bean store to use for dependency injection. 162 * @return A new builder. 163 */ 164 public static Builder create(BeanStore beanStore) { 165 return new Builder(VersionProvider.create(beanStore)); 166 } 167 168 private final VersionProvider worker; 169 170 /** 171 * No-arg constructor — delegates to a default {@link VersionProvider} worker (reads 172 * {@code MANIFEST.MF}, {@code git.properties}, and the JVM version from the framework 173 * classloader). 174 */ 175 public VersionMixin() { 176 this(new VersionProvider()); 177 } 178 179 /** 180 * Worker constructor. 181 * 182 * @param worker The shared {@link VersionProvider} worker this flavor delegates to. Must not be 183 * <jk>null</jk>. 184 */ 185 protected VersionMixin(VersionProvider worker) { 186 this.worker = worker; 187 } 188 189 /** 190 * Worker + builder constructor. 191 * 192 * <p> 193 * Used by {@link Builder#build()} to both delegate to the shared {@link VersionProvider} worker and stash the 194 * programmatic {@link RestBuilder}(carrying any {@code @Rest}-level overrides such as {@code path}) so those 195 * values take precedence over the mixin class's own {@link Rest @Rest} annotation. 196 * 197 * <h5 class='section'>Why worker + builder, not the flavor Builder:</h5> 198 * <ul class='spaced-list'> 199 * <li>Takes the already-built <b>worker bean</b> (not the flavor {@link Builder}) so the flavor can be 200 * constructed from ANY independently-supplied worker — e.g. a user's own {@code @Bean VersionProvider} 201 * or BeanStore bean, per the delegate-bean model — not only via this flavor's own builder. 202 * <li>Takes the generic {@link RestBuilder}(here {@code this} from {@link Builder#build()}) so it honors the 203 * uniform §2.4 {@code Foo(RestBuilder<?>)} injection contract the base class and DI resolution key on; the 204 * base knows nothing about the concrete flavor builder or the worker type. 205 * <li>Holds the finished worker product (the {@code final} {@link VersionProvider} field), not a transient 206 * builder; the worker is materialized exactly once at {@link Builder#build()} time. 207 * <li>Keeps the capability worker and the REST-level config as two distinct inputs. 208 * </ul> 209 * 210 * @param worker The shared {@link VersionProvider} worker this flavor delegates to. Must not be <jk>null</jk>. 211 * @param builder The programmatic configuration builder. May be <jk>null</jk>. 212 */ 213 protected VersionMixin(VersionProvider worker, RestBuilder<?> builder) { 214 super(builder); 215 this.worker = worker; 216 } 217 218 /** 219 * [GET /version] — emit the assembled metadata as a JSON map. 220 * 221 * @param res The current REST response. 222 * @throws IOException If an I/O error occurs while writing the response. 223 */ 224 @RestGet( 225 path="/#{pathToken(${juneau.version.path:version})}", 226 summary="Version / build metadata", 227 description="Deployment-introspection metadata (name, version, git, build).", 228 swagger=@OpSwagger(ignore=true) 229 ) 230 public void getInfo(RestResponse res) throws IOException { 231 worker.serve(res); 232 } 233 234 /** 235 * Returns the configured info map (test/inspection helper). 236 * 237 * @return The info map. 238 */ 239 public Map<String,String> getInfoMap() { 240 return worker.getInfoMap(); 241 } 242 243 /** 244 * Builder for {@link VersionMixin} instances. 245 * 246 * <p> 247 * Mirrors {@link VersionProvider.Builder}'s configuration methods on the mixin's own surface and 248 * forwards each call to an underlying {@link VersionProvider.Builder}, which builds the shared worker 249 * the mixin delegates to. 250 * 251 * <p> 252 * Extends {@link org.apache.juneau.rest.server.servlet.RestMixin.Builder RestMixin.Builder} so the mixin's bespoke worker-config setters chain 253 * with true covariant returns alongside the inherited {@link RestBuilder}surface (e.g. {@code path}, 254 * {@code roleGuard}). The worker config is forwarded once into {@link VersionProvider.Builder} and the REST 255 * config is inherited once from {@link AbstractRestBuilder} — no triplication across the Version flavors. 256 */ 257 public static class Builder extends RestMixin.Builder<VersionMixin,Builder> { 258 259 private final VersionProvider.Builder worker; 260 261 /** Constructor — protected access for {@link VersionMixin#create()}. */ 262 protected Builder(VersionProvider.Builder worker) { 263 super(VersionMixin.class); 264 this.worker = worker; 265 } 266 267 /** 268 * Sets a single entry, overwriting any prior value for that key. 269 * 270 * @param key The entry key. Must not be <jk>null</jk> or blank. 271 * @param value The entry value (a <jk>null</jk> value is recorded as 272 * {@link VersionMixin#UNKNOWN}). 273 * @return This object. 274 */ 275 public Builder entry(String key, String value) { 276 worker.entry(key, value); 277 return this; 278 } 279 280 /** 281 * Bulk-set entries from a map. 282 * 283 * @param values Entries to add. Can be <jk>null</jk> (no-op). Individual <jk>null</jk> values are recorded as 284 * {@link VersionMixin#UNKNOWN}. 285 * @return This object. 286 */ 287 public Builder entries(Map<String,String> values) { 288 worker.entries(values); 289 return this; 290 } 291 292 /** 293 * Reads {@code Implementation-Title}, {@code Implementation-Version}, 294 * {@code Implementation-Vendor}, and {@code Build-Jdk} from 295 * {@code /META-INF/MANIFEST.MF} on the framework classloader. 296 * 297 * @return This object. 298 */ 299 public Builder fromManifest() { 300 worker.fromManifest(); 301 return this; 302 } 303 304 /** 305 * Reads manifest attributes from {@code /META-INF/MANIFEST.MF} on the supplied classloader. 306 * 307 * @param classLoader The classloader to walk. Must not be <jk>null</jk>. 308 * @return This object. 309 */ 310 public Builder fromManifest(ClassLoader classLoader) { 311 worker.fromManifest(classLoader); 312 return this; 313 } 314 315 /** 316 * Reads a {@link Manifest} that the importer registered as a bean (e.g. 317 * {@code @Bean Manifest appManifest()}). 318 * 319 * @param manifest The manifest to read. Must not be <jk>null</jk>. 320 * @return This object. 321 */ 322 public Builder fromManifest(Manifest manifest) { 323 worker.fromManifest(manifest); 324 return this; 325 } 326 327 /** 328 * Reads {@code git.commit.id}, {@code git.branch}, and {@code git.build.time} from 329 * {@code /git.properties} on the framework classloader. 330 * 331 * @return This object. 332 */ 333 public Builder fromGitProperties() { 334 worker.fromGitProperties(); 335 return this; 336 } 337 338 /** 339 * Reads {@code git.properties} from the supplied classloader. 340 * 341 * @param classLoader The classloader to walk. Must not be <jk>null</jk>. 342 * @return This object. 343 */ 344 public Builder fromGitProperties(ClassLoader classLoader) { 345 worker.fromGitProperties(classLoader); 346 return this; 347 } 348 349 /** 350 * Adds the running JVM's {@code java.version} system property as the {@code javaVersion} 351 * entry, unless an earlier {@link #fromManifest() fromManifest(...)} call already supplied 352 * one from {@code Build-Jdk}. 353 * 354 * @return This object. 355 */ 356 public Builder fromJavaVersion() { 357 worker.fromJavaVersion(); 358 return this; 359 } 360 361 /** 362 * Builds a {@link VersionMixin} instance. 363 * 364 * @return A configured instance. 365 */ 366 @Override /* AbstractRestBuilder */ 367 public VersionMixin build() { 368 return new VersionMixin(worker.build(), this); 369 } 370 } 371}