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