001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.cp;
014
015import static org.apache.juneau.internal.IOUtils.*;
016import static org.apache.juneau.internal.ObjectUtils.*;
017
018import java.io.*;
019import java.util.*;
020import java.util.concurrent.*;
021
022import org.apache.juneau.internal.*;
023import org.apache.juneau.parser.*;
024
025/**
026 * Class for retrieving and caching resource files from the classpath.
027 */
028public final class ResourceManager {
029
030   // Maps resource names+locales to found resources.
031   private final ConcurrentHashMap<ResourceKey,byte[]> byteCache;
032   private final ConcurrentHashMap<ResourceKey,String> stringCache;
033
034   private final Class<?> baseClass;
035   private final ResourceFinder resourceFinder;
036   private final boolean useCache;
037
038   /**
039    * Constructor.
040    *
041    * @param baseClass The default class to use for retrieving resources from the classpath.
042    * @param resourceFinder The resource finder implementation.
043    * @param useCache If <jk>true</jk>, retrieved resources are stored in an in-memory cache for fast lookup.
044    */
045   public ResourceManager(Class<?> baseClass, ResourceFinder resourceFinder, boolean useCache) {
046      this.baseClass = baseClass;
047      this.resourceFinder = resourceFinder;
048      this.useCache = useCache;
049      if (useCache) {
050         this.byteCache = new ConcurrentHashMap<>();
051         this.stringCache = new ConcurrentHashMap<>();
052      } else {
053         this.byteCache = null;
054         this.stringCache = null;
055      }
056   }
057
058   /**
059    * Constructor.
060    *
061    * <p>
062    * Uses default {@link BasicResourceFinder} for finding resources.
063    *
064    * @param baseClass The default class to use for retrieving resources from the classpath.
065    */
066   public ResourceManager(Class<?> baseClass) {
067      this(baseClass, BasicResourceFinder.INSTANCE, false);
068   }
069
070   /**
071    * Finds the resource with the given name.
072    *
073    * @param name Name of the desired resource.
074    * @return An input stream to the object, or <jk>null</jk> if the resource could not be found.
075    * @throws IOException Thrown by underlying stream.
076    */
077   public InputStream getStream(String name) throws IOException {
078      return getStream(name, null);
079   }
080
081   /**
082    * Finds the resource with the given name for the specified locale and returns it as an input stream.
083    *
084    * @param name Name of the desired resource.
085    * @param locale The locale.  Can be <jk>null</jk>.
086    * @return An input stream to the object, or <jk>null</jk> if the resource could not be found.
087    * @throws IOException Thrown by underlying stream.
088    */
089   public InputStream getStream(String name, Locale locale) throws IOException {
090
091      if (isEmpty(name))
092         return null;
093
094      if (! useCache)
095         return resourceFinder.findResource(baseClass, name, locale);
096
097      ResourceKey key = new ResourceKey(name, locale);
098
099      byte[] r = byteCache.get(key);
100      if (r == null) {
101         try (InputStream is = resourceFinder.findResource(baseClass, name, locale)) {
102            if (is != null)
103               byteCache.putIfAbsent(key, IOUtils.readBytes(is, 1024));
104         }
105      }
106
107      r = byteCache.get(key);
108      return r == null ? null : new ByteArrayInputStream(r);
109   }
110
111   /**
112    * Finds the resource with the given name and converts it to a simple string.
113    *
114    * @param name Name of the desired resource.
115    * @return The resource converted to a string, or <jk>null</jk> if the resource could not be found.
116    * @throws IOException Thrown by underlying stream.
117    */
118   public String getString(String name) throws IOException {
119      return getString(name, null);
120   }
121
122   /**
123    * Finds the resource with the given name and converts it to a simple string.
124    *
125    * @param name Name of the desired resource.
126    * @param locale The locale.  Can be <jk>null</jk>.
127    * @return The resource converted to a string, or <jk>null</jk> if the resource could not be found.
128    * @throws IOException Thrown by underlying stream.
129    */
130   public String getString(String name, Locale locale) throws IOException {
131
132      if (isEmpty(name))
133         return null;
134
135      if (! useCache) {
136         try (InputStream is = resourceFinder.findResource(baseClass, name, locale)) {
137            return IOUtils.read(is, IOUtils.UTF8);
138         }
139      }
140
141      ResourceKey key = new ResourceKey(name, locale);
142
143      String r = stringCache.get(key);
144      if (r == null) {
145         try (InputStream is = resourceFinder.findResource(baseClass, name, locale)) {
146            if (is != null)
147               stringCache.putIfAbsent(key, IOUtils.read(is, IOUtils.UTF8));
148         }
149      }
150
151      return stringCache.get(key);
152   }
153
154   /**
155    * Reads the input stream and parses it into a POJO using the specified parser.
156    *
157    * @param c The class type of the POJO to create.
158    * @param parser The parser to use to parse the stream.
159    * @param name The resource name (e.g. "htdocs/styles.css").
160    * @return The parsed resource, or <jk>null</jk> if the resource could not be found.
161    * @throws IOException Thrown by underlying stream.
162    * @throws ParseException If stream could not be parsed using the specified parser.
163    */
164   public <T> T getResource(Class<T> c, Parser parser, String name) throws IOException, ParseException {
165      return getResource(c, parser, name, null);
166   }
167
168   /**
169    * Reads the input stream and parses it into a POJO using the specified parser.
170    *
171    * @param c The class type of the POJO to create.
172    * @param parser The parser to use to parse the stream.
173    * @param name The resource name (e.g. "htdocs/styles.css").
174    * @param locale
175    *    Optional locale.
176    *    <br>If <jk>null</jk>, won't look for localized file names.
177    * @return The parsed resource, or <jk>null</jk> if the resource could not be found.
178    * @throws IOException Thrown by underlying stream.
179    * @throws ParseException If stream could not be parsed using the specified parser.
180    */
181   public <T> T getResource(Class<T> c, Parser parser, String name, Locale locale) throws IOException, ParseException {
182
183      InputStream is = getStream(name, locale);
184      if (is == null)
185         return null;
186      try (Closeable in = parser.isReaderParser() ? new InputStreamReader(is, UTF8) : is) {
187         return parser.parse(in, c);
188      }
189   }
190
191   private class ResourceKey {
192      final String name;
193      final Locale locale;
194
195      ResourceKey(String name, Locale locale) {
196         this.name = name;
197         this.locale = locale;
198      }
199
200      @Override
201      public int hashCode() {
202         return name.hashCode() + (locale == null ? 0 : locale.hashCode());
203      }
204
205      @Override
206      public boolean equals(Object o) {
207         return eq(this, (ResourceKey)o, (x,y)->eq(x.name, y.name) && eq(x.locale, y.locale));
208      }
209   }
210}