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}