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.CollectionUtils.*; 020 021import java.io.*; 022import java.util.*; 023 024import org.apache.juneau.http.*; 025import org.apache.juneau.http.entity.*; 026import org.apache.juneau.http.header.*; 027import org.apache.juneau.http.resource.*; 028import org.apache.juneau.http.response.*; 029 030/** 031 * Flavor-neutral favicon worker bean shared by the favicon {@code @Rest} flavors. 032 * 033 * <p> 034 * Holds the capability state (the icon bytes + {@code Cache-Control} header value) and the serve 035 * logic (build an {@link HttpResource} with {@code Content-Type: image/x-icon} + the cache header). 036 * The {@link FaviconMixin} (mixin), {@link FaviconResource} (child), and {@link FaviconServlet} 037 * (servlet) flavors are independent {@code @Rest} classes that each hold a {@code FaviconProvider} worker 038 * and delegate to it — so the three deployment forms cannot drift, and no flavor is another 039 * flavor's worker. 040 * 041 * <h5 class='section'>See Also:</h5><ul> 042 * <li class='jc'>{@link FaviconMixin} 043 * <li class='jc'>{@link FaviconResource} 044 * <li class='jc'>{@link FaviconServlet} 045 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerComposition">REST Server — Mixins and Multi-Mount Paths</a> 046 * </ul> 047 * 048 * @since 10.0.0 049 */ 050// @formatter:off 051public class FaviconProvider { 052 053 /** Default {@code Cache-Control} header value: {@code max-age=2592000, public} (30 days). */ 054 public static final String DEFAULT_CACHE_CONTROL = "max-age=2592000, public"; 055 056 /** Classpath location of the framework-shipped default favicon. */ 057 private static final String DEFAULT_FAVICON_RESOURCE = "/juneau-favicon.ico"; 058 059 /** 060 * Creates a new builder for configuring a {@link FaviconProvider}. 061 * 062 * @return A new builder. 063 */ 064 public static Builder create() { 065 return new Builder(); 066 } 067 068 private final byte[] bytes; 069 private final String cacheControl; 070 071 /** 072 * No-arg constructor — loads the default Juneau-branded favicon from the classpath. 073 */ 074 public FaviconProvider() { 075 this(create()); 076 } 077 078 /** 079 * Builder constructor. 080 * 081 * @param builder The builder. Must not be <jk>null</jk>. 082 */ 083 protected FaviconProvider(Builder builder) { 084 bytes = builder.resolveBytes(); 085 cacheControl = builder.cacheControl; 086 } 087 088 /** 089 * Serves the configured favicon bytes. 090 * 091 * @return The favicon as an {@link HttpResource} with {@code Content-Type: image/x-icon} and the 092 * configured {@code Cache-Control} header. 093 */ 094 public HttpResource serve() { 095 List<HttpHeader> hdrs = list(ContentType.of("image/x-icon"), CacheControl.of(cacheControl)); 096 return HttpResourceBean.of(ByteArrayBody.of(bytes, "image/x-icon"), hdrs); 097 } 098 099 /** 100 * Builder for {@link FaviconProvider} instances. 101 */ 102 public static class Builder { 103 104 private byte[] bytes; 105 private String classpath; 106 private String cacheControl = DEFAULT_CACHE_CONTROL; 107 108 /** Constructor — package access for {@link FaviconProvider#create()}. */ 109 protected Builder() {} 110 111 /** 112 * Sets the raw favicon bytes. 113 * 114 * <p> 115 * Mutually exclusive with {@link #classpath(String)} — whichever is set last wins 116 * at {@link #build()} time. 117 * 118 * @param value The favicon bytes (typically an {@code .ico} or {@code .png} payload). 119 * Must not be <jk>null</jk>. 120 * @return This object. 121 */ 122 public Builder bytes(byte[] value) { 123 bytes = value; 124 classpath = null; 125 return this; 126 } 127 128 /** 129 * Sets the classpath resource path from which to load the favicon bytes. 130 * 131 * <p> 132 * Resolved via {@link Class#getResourceAsStream(String) FaviconProvider.class.getResourceAsStream(...)} 133 * at {@link #build()} time. A resolved-to-{@code null} stream falls back to the framework's 134 * default favicon. Mutually exclusive with {@link #bytes(byte[])}. 135 * 136 * @param value The classpath resource path (e.g. {@code "/myapp/icon.ico"}). 137 * Must not be <jk>null</jk> or blank. 138 * @return This object. 139 */ 140 public Builder classpath(String value) { 141 classpath = value; 142 bytes = null; 143 return this; 144 } 145 146 /** 147 * Sets the {@code Cache-Control} header value emitted with the favicon response. 148 * 149 * <p> 150 * Defaults to {@value #DEFAULT_CACHE_CONTROL} (30 days). 151 * 152 * @param value The new {@code Cache-Control} value. Must not be <jk>null</jk> or blank. 153 * @return This object. 154 */ 155 public Builder cacheControl(String value) { 156 cacheControl = value; 157 return this; 158 } 159 160 /** 161 * Builds a {@link FaviconProvider} instance. 162 * 163 * @return A configured instance. 164 */ 165 public FaviconProvider build() { 166 return new FaviconProvider(this); 167 } 168 169 byte[] resolveBytes() { 170 if (bytes != null) 171 return bytes; 172 if (classpath != null) { 173 var resolved = readClasspath(classpath); 174 if (resolved != null) 175 return resolved; 176 } 177 // Falls through to the framework's default-shipping ICO; the resource is shipped in 178 // the same jar as this class so it must be present at runtime. 179 return readClasspath(DEFAULT_FAVICON_RESOURCE); 180 } 181 182 @SuppressWarnings({ 183 "java:S1168" // null is a meaningful "resource not found" sentinel: resolveBytes() falls through to the default favicon; an empty array would suppress that fallback. 184 }) 185 private static byte[] readClasspath(String path) { 186 try (var in = FaviconProvider.class.getResourceAsStream(path)) { 187 if (in == null) 188 return null; 189 return in.readAllBytes(); 190 } catch (IOException e) { 191 // readAllBytes on a classpath resource is effectively unreachable; the catch is 192 // here only to satisfy the checked exception contract. 193 throw new InternalServerError(e); 194 } 195 } 196 } 197}