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.staticfile;
018
019import static org.apache.juneau.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.FileUtils.*;
021import static org.apache.juneau.commons.utils.Shorts.*;
022
023import java.io.*;
024import java.util.*;
025
026import org.apache.juneau.commons.collections.*;
027import org.apache.juneau.commons.inject.*;
028import org.apache.juneau.commons.io.*;
029import org.apache.juneau.http.*;
030import org.apache.juneau.http.entity.*;
031import org.apache.juneau.http.header.*;
032import org.apache.juneau.http.resource.*;
033import org.apache.juneau.http.response.*;
034import org.apache.juneau.marshall.cp.*;
035import org.apache.juneau.rest.server.*;
036
037/**
038 * API for retrieving localized static files from either the classpath or file system.
039 *
040 * <p>
041 * Provides the same functionality as {@link BasicFileFinder} but adds support for returning files as {@link HttpResource}
042 * objects with arbitrary headers.
043 *
044 * <h5 class='section'>See Also:</h5><ul>
045 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/StaticFiles">Static files</a>
046 * </ul>
047 */
048@SuppressWarnings({
049   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
050   "resource" // Resource management handled externally
051})
052public class BasicStaticFiles implements StaticFiles {
053
054   /**
055    * Creates a new builder for this object.
056    *
057    * @param beanStore The bean store to use for creating beans.
058    * @return A new builder for this object.
059    */
060   public static StaticFiles.Builder create(BeanStore beanStore) {
061      return new StaticFiles.Builder(beanStore);
062   }
063
064   private final HttpHeader[] headers;
065   private final MimeTypeDetector mimeTypes;
066   private final int hashCode;
067
068   private final FileFinder fileFinder;
069
070   /**
071    * Constructor.
072    *
073    * @param beanStore The bean store containing injectable beans for this static files object. Must not be <jk>null</jk>.
074    */
075   public BasicStaticFiles(BeanStore beanStore) {
076      // @formatter:off
077      this(StaticFiles
078         .create(beanStore)
079         .dir("static")
080         .dir("htdocs")
081         .cp(beanStore.getBean(ResourceSupplier.class).orElseThrow(() -> new IllegalStateException("ResourceSupplier not found")).getResourceClass(), "htdocs", true)
082         .cp(beanStore.getBean(ResourceSupplier.class).orElseThrow(() -> new IllegalStateException("ResourceSupplier not found")).getResourceClass(), "/htdocs", true)
083         .caching(1_000_000)
084         .exclude("(?i).*\\.(class|properties)")
085         .headers(CacheControl.of("max-age=86400, public"))
086      );
087      // @formatter:on
088   }
089
090   /**
091    * Constructor.
092    *
093    * @param builder The builder object. Must not be <jk>null</jk>.
094    */
095   public BasicStaticFiles(StaticFiles.Builder builder) {
096      this.headers = builder.headers.toArray(new HttpHeader[builder.headers.size()]);
097      this.mimeTypes = builder.mimeTypes;
098      this.hashCode = h(hashCode(), headers);
099      this.fileFinder = builder.fileFinder.build();
100   }
101
102   /**
103    * Constructor.
104    *
105    * <p>
106    * Can be used when subclassing and overriding the {@link #resolve(String, Locale)} method.
107    */
108   protected BasicStaticFiles() {
109      this.headers = new HttpHeader[0];
110      this.mimeTypes = null;
111      this.hashCode = h(hashCode(), headers);
112      this.fileFinder = null;
113   }
114
115   @Override /* Overridden from Object */
116   public boolean equals(Object o) {
117      return this == o || (o instanceof BasicStaticFiles o2 && eq(this, o2, (x, y) -> Arrays.equals(x.headers, y.headers)));
118   }
119
120   @Override /* Overridden from FileFinder */
121   public Optional<InputStream> getStream(String name, Locale locale) throws IOException {
122      return fileFinder.getStream(name, locale);
123   }
124
125   @Override /* Overridden from FileFinder */
126   public Optional<String> getString(String name, Locale locale) throws IOException {
127      return fileFinder.getString(name, locale);
128   }
129
130   @Override
131   public int hashCode() {
132      return hashCode;
133   }
134
135   /**
136    * Resolve the specified path.
137    *
138    * <p>
139    * Subclasses can override this method to provide specialized handling.
140    *
141    * @param path The path to resolve to a static file.
142    * @param locale Optional locale.
143    * @return The resource, or an empty {@link Optional} if not found.
144    */
145   @Override /* Overridden from StaticFiles */
146   public Optional<HttpResource> resolve(String path, Locale locale) {
147      try {
148         Optional<InputStream> is = getStream(path, locale);
149         if (! is.isPresent())
150            return oe();
151         var ct = mimeTypes == null ? null : mimeTypes.getContentType(getFileName(path));
152         var hdrs = new ArrayList<HttpHeader>();
153         if (ct != null)
154            hdrs.add(ContentType.of(ct));
155         Arrays.stream(headers).filter(Objects::nonNull).forEach(hdrs::add);
156         return o(HttpResourceBean.of(StreamBody.of(is.get()), hdrs));
157      } catch (IOException e) {
158         throw new InternalServerError(e);
159      }
160   }
161
162   protected FluentMap<String,Object> properties() {
163      // @formatter:off
164      return filteredBeanPropertyMap()
165         .a("headers", headers);
166      // @formatter:on
167   }
168
169   @Override /* Overridden from Object */
170   public String toString() {
171      return r(properties());
172   }
173}