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}} &mdash; 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/}) &mdash; 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 &mdash; {@code /version}, {@code /info}, and {@code /about} &mdash; 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="/&#123;juneau.version.path:version&#125;")} 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 * &mdash; 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 &mdash; 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 &mdash; 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 &mdash; e.g. a user's own {@code @Bean VersionProvider}
201    *       or BeanStore bean, per the delegate-bean model &mdash; 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 &sect;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] &mdash; 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} &mdash; 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 &mdash; 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}