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.util;
018
019import static org.apache.juneau.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021
022import java.io.*;
023import java.util.concurrent.*;
024
025import org.apache.juneau.commons.utils.*;
026import org.apache.juneau.http.*;
027import org.apache.juneau.http.entity.*;
028import org.apache.juneau.http.header.*;
029import org.apache.juneau.http.resource.*;
030
031/**
032 * A themeable/configurable asset-serving mixin's cache of classpath-shipped assets: reads each configured
033 * resource's bytes once, computes and caches its {@link ChecksumUtils#hash8(byte[]) content hash}, and wraps
034 * bytes as a cacheable {@link HttpResource} - the read+cache+hash+cache-buster-URL+serve bundle that
035 * {@code ConsoleChromeMixin} (juneau-console) and {@code ViewsMixin} (juneau-console-views)
036 * previously each hand-rolled as their own private statics and static cache maps.
037 *
038 * <h5 class='section'>Version-anchor:</h5>
039 * <p>
040 * {@link #buildVersion()} resolves {@link Package#getImplementationVersion()} from the constructor-supplied
041 * <b>anchor</b> class, not from this class's own class/package. A mixin composes one {@link ClasspathAssetCache}
042 * per mixin class (mirroring the static caches this replaces) and anchors it on <b>its own</b> class, so the
043 * resolved implementation version - and therefore every {@code ?v=<buildVersion>-<hash8>} cache-buster URL this
044 * cache builds - matches what that mixin's own jar shipped with, exactly as it did before extraction. Anchoring
045 * on this class instead would resolve {@code juneau-rest-server}'s own implementation version for every
046 * consuming mixin, silently changing every consumer's cache-buster URLs.
047 *
048 * <h5 class='section'>Thread-safety:</h5>
049 * <p>
050 * Backed by {@link ConcurrentHashMap}s; safe to share across concurrent requests. Classpath-shipped resources
051 * are assumed immutable for the lifetime of the JVM, so bytes and hashes are computed at most once per resource.
052 *
053 * @since 10.0.0
054 */
055public class ClasspathAssetCache {
056
057   private final Class<?> anchor;
058   private final ConcurrentHashMap<String,byte[]> byteCache = new ConcurrentHashMap<>();
059   private final ConcurrentHashMap<String,String> hashCache = new ConcurrentHashMap<>();
060
061   /**
062    * Constructor.
063    *
064    * @param anchor
065    *    The class that anchors both classpath resource resolution ({@link Class#getResourceAsStream(String)}) and
066    *    {@link #buildVersion()}'s {@link Package#getImplementationVersion()} resolution. Typically the owning
067    *    mixin's own class (see the class javadoc's version-anchor section). Must not be <jk>null</jk>.
068    */
069   public ClasspathAssetCache(Class<?> anchor) {
070      this.anchor = rnn(anchor);
071   }
072
073   /** Test-only accessor for the constructor-supplied anchor class. */
074   Class<?> anchor() {
075      return anchor;
076   }
077
078   /**
079    * Reads (and caches) the given classpath resource's bytes, resolved relative to this cache's anchor class.
080    *
081    * @param classpathResource A classpath-root-absolute resource path (e.g. {@code "/org/apache/juneau/foo/bar.js"}).
082    *    Must not be <jk>null</jk> and must resolve to an existing resource on the anchor's classpath.
083    * @return The resource's bytes (served from cache on every call after the first for that resource).
084    */
085   public byte[] bytes(String classpathResource) {
086      return byteCache.computeIfAbsent(classpathResource, this::read);
087   }
088
089   private byte[] read(String classpathResource) {
090      try (var in = anchor.getResourceAsStream(classpathResource)) {
091         if (in == null)
092            throw new IOException("Classpath resource not found: " + classpathResource);
093         return IoUtils.readBytes(in);
094      } catch (IOException e) {  // HTT: unreachable - callers only pass already-validated, jar-shipped classpath resources.
095         throw new UncheckedIOException(e);
096      }
097   }
098
099   /**
100    * Computes (and caches) the given classpath resource's 8-hex-char content hash.
101    *
102    * @param classpathResource A classpath-root-absolute resource path.  Same constraints as {@link #bytes(String)}.
103    * @return The resource's {@link ChecksumUtils#hash8(byte[])} content hash.
104    */
105   public String hash(String classpathResource) {
106      return hashCache.computeIfAbsent(classpathResource, r -> ChecksumUtils.hash8(bytes(r)));
107   }
108
109   /**
110    * Resolves the framework build version for asset cache-busting from this cache's anchor class's package,
111    * falling back to {@code "dev"} when unset (e.g. running from an IDE/test classpath rather than a packaged
112    * jar) - see the class javadoc's version-anchor section.
113    *
114    * @return The anchor's {@link Package#getImplementationVersion()}, or {@code "dev"} if unset.
115    */
116   public String buildVersion() {
117      var v = anchor.getPackage().getImplementationVersion();
118      return v == null ? "dev" : v;  // HTT: the non-null branch only fires when running from a packaged jar with a manifest Implementation-Version - unreachable when tests run against unpackaged target/classes.
119   }
120
121   /**
122    * Builds the {@code ?v=<buildVersion>-<hash8>} content-sensitive cache-buster query suffix for the given
123    * classpath resource, suitable for appending to that resource's served URL.
124    *
125    * @param classpathResource A classpath-root-absolute resource path.  Same constraints as {@link #bytes(String)}.
126    * @return The {@code "?v=" + buildVersion() + "-" + hash(classpathResource)} suffix.
127    */
128   public String cacheBuster(String classpathResource) {
129      return "?v=" + buildVersion() + "-" + hash(classpathResource);
130   }
131
132   /**
133    * Wraps pre-computed bytes as a cacheable {@link HttpResource} carrying the given content type and
134    * {@code Cache-Control} header.
135    *
136    * @param bytes The response body bytes.  Must not be <jk>null</jk>.
137    * @param contentType The {@code Content-Type} header value (e.g. {@code "text/css;charset=utf-8"}).
138    * @param cacheControl The {@code Cache-Control} header value (e.g. {@code "max-age=86400, public"}).
139    * @return The wrapped, cacheable resource.
140    */
141   public HttpResource wrap(byte[] bytes, String contentType, String cacheControl) {
142      return HttpResourceBean.of(
143         ByteArrayBody.of(bytes, contentType),
144         list(ContentType.of(contentType), CacheControl.of(cacheControl)));
145   }
146
147   /**
148    * Reads (and caches) the given classpath resource and wraps it as a cacheable {@link HttpResource} - the
149    * {@link #bytes(String)} + {@link #wrap(byte[],String,String)} bundle every classpath-asset-serving endpoint
150    * needs.
151    *
152    * @param classpathResource A classpath-root-absolute resource path.  Same constraints as {@link #bytes(String)}.
153    * @param contentType The {@code Content-Type} header value.
154    * @param cacheControl The {@code Cache-Control} header value.
155    * @return The wrapped, cacheable resource.
156    */
157   public HttpResource serve(String classpathResource, String contentType, String cacheControl) {
158      return wrap(bytes(classpathResource), contentType, cacheControl);
159   }
160}