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 &mdash; 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 &mdash; 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 &mdash; 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} &mdash; 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 &mdash; 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}