View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors. 
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   *
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.common.util;
25  
26  import java.io.File;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.util.Collections;
30  import java.util.HashMap;
31  import java.util.HashSet;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Set;
35  import java.util.regex.Matcher;
36  import java.util.regex.Pattern;
37  import net.jcip.annotations.Immutable;
38  import org.modeshape.common.CommonI18n;
39  import org.modeshape.common.i18n.I18n;
40  
41  /**
42   * A simple utility that determines an appropriate MIME type by matching the extension of the supplied filename against a set of
43   * known file extensions.
44   * <p>
45   * This utility class may be instantiated to create a new instance with its own set of mappings. If desired, the default mappings
46   * will be loaded using the current thread's {@link Thread#getContextClassLoader() context class loader}, and will be loaded from
47   * the <code>/org/modeshape/mime.types</code> file located on the classpath. The {@link #load(InputStream, Map)} method may be
48   * used to load custom mappings (in the standard format) into a {@link Map} instance, which can then be passed to one of the
49   * constructors.
50   * </p>
51   */
52  @Immutable
53  public class MimeTypeUtil {
54  
55      /**
56       * The default location of the properties file containing the extension patterns to MIME types. Value is "{@value} ".
57       */
58      public static final String MIME_TYPE_EXTENSIONS_RESOURCE_PATH = "org/modeshape/mime.types";
59  
60      /**
61       * The mapping of extension (which includes the leading '.') to MIME type.
62       */
63      private final Map<String, String> mimeTypesByExtension;
64  
65      /**
66       * Create a default instance of the extension-based MIME type detector. The set of extension patterns to MIME-types is loaded
67       * from the "org/modeshape/mime.types" classpath resource.
68       */
69      public MimeTypeUtil() {
70          this(null, true);
71      }
72  
73      /**
74       * Create an instance of the extension-based MIME type detector by using the supplied mappings. The set of extension patterns
75       * to MIME-types is loaded from the "org/modeshape/mime.types" classpath resource, but the supplied extension mappings
76       * override any default mappings.
77       * 
78       * @param extensionsToMimeTypes the mapping of extension patterns to MIME types, which will override the default mappings; may
79       *        be null if the default mappings are to be used
80       */
81      public MimeTypeUtil( Map<String, String> extensionsToMimeTypes ) {
82          this(extensionsToMimeTypes, true);
83      }
84  
85      /**
86       * Create an instance of the extension-based MIME type detector by using the supplied mappings. If requested, the set of
87       * extension patterns to MIME-types is loaded from the "org/modeshape/mime.types" classpath resource and any supplied
88       * extension mappings override any default mappings.
89       * 
90       * @param extensionsToMimeTypes the mapping of extension patterns to MIME types, which will override the default mappings; may
91       *        be null if the default mappings are to be used
92       * @param initWithDefaults true if the default mappings are to be loaded first, or false if the default mappings are not to be
93       *        used at all
94       */
95      public MimeTypeUtil( Map<String, String> extensionsToMimeTypes,
96                           boolean initWithDefaults ) {
97          Map<String, String> mappings = initWithDefaults ? getDefaultMappings() : new HashMap<String, String>();
98          if (extensionsToMimeTypes != null) {
99              for (Map.Entry<String, String> entry : extensionsToMimeTypes.entrySet()) {
100                 String extensionString = entry.getKey();
101                 if (extensionString == null) continue;
102                 // Lowercase, trim, and remove all leading '.' characters ...
103                 extensionString = extensionString.toLowerCase().trim().replaceAll("^[.]+", "");
104                 if (extensionString.length() == 0) continue;
105                 String mimeType = entry.getValue();
106                 if (mimeType == null) continue;
107                 mimeType = entry.getValue().trim();
108                 if (mimeType.length() == 0) continue;
109                 assert extensionString.length() != 0;
110                 assert mimeType.length() != 0;
111                 mappings.put(extensionString, mimeType);
112             }
113         }
114         // Now put the mappings into the different maps ...
115         Map<String, String> mappingsByAnyCharExtension = new HashMap<String, String>();
116         for (Map.Entry<String, String> entry : mappings.entrySet()) {
117             String extensionString = entry.getKey();
118             String mimeType = entry.getValue();
119             assert extensionString != null;
120             assert extensionString.length() != 0;
121             assert mimeType != null;
122             assert mimeType.length() != 0;
123             mappingsByAnyCharExtension.put("." + extensionString, mimeType);
124         }
125         mimeTypesByExtension = Collections.unmodifiableMap(mappingsByAnyCharExtension);
126     }
127 
128     /**
129      * Load the default extensions from {@link #MIME_TYPE_EXTENSIONS_RESOURCE_PATH}, which can either be a property file or a
130      * tab-delimited *nix-style MIME types file (common in web servers and libraries). If an extension applies to more than one
131      * MIME type, the first one in the file wins.
132      * 
133      * @return the default mappings; never null
134      */
135     protected static Map<String, String> getDefaultMappings() {
136         Map<String, Set<String>> duplicates = new HashMap<String, Set<String>>();
137         return load(Thread.currentThread().getContextClassLoader().getResourceAsStream(MIME_TYPE_EXTENSIONS_RESOURCE_PATH),
138                     duplicates);
139     }
140 
141     /**
142      * Load the extensions from the supplied stream, which may provide the contents in the format of property file or a
143      * tab-delimited *nix-style MIME types file (common in web servers and libraries). If an extension applies to more than one
144      * MIME type, the first one in the file wins.
145      * 
146      * @param stream the stream containing the content; may not be null
147      * @param duplicateMimeTypesByExtension a map into which any extension should be placed if there are multiple MIME types that
148      *        apply; may be null if this information is not required
149      * @return the default mappings; never null
150      */
151     public static Map<String, String> load( InputStream stream,
152                                             Map<String, Set<String>> duplicateMimeTypesByExtension ) {
153         CheckArg.isNotNull(stream, "stream");
154         // Create a Regex pattern that can be used for each line. This pattern looks for a mime type
155         // (which may contain no whitespace or '=') followed by an optional whitespace, an optional equals sign,
156         // optionally more whitespace, and finally by a string of one or more extensions.
157         Pattern linePattern = Pattern.compile("\\s*([^\\s=]+)\\s*=?\\s*(.*)");
158         List<String> lines = null;
159         try {
160             String content = IoUtil.read(stream);
161             lines = StringUtil.splitLines(content);
162         } catch (IOException e) {
163             I18n msg = CommonI18n.unableToAccessResourceFileFromClassLoader;
164             Logger.getLogger(MimeTypeUtil.class).warn(e, msg, MIME_TYPE_EXTENSIONS_RESOURCE_PATH);
165         }
166         Map<String, String> mimeTypesByExtension = new HashMap<String, String>();
167         if (lines != null) {
168             for (String line : lines) {
169                 line = line.trim();
170                 if (line.length() == 0 || line.startsWith("#")) continue;
171                 // Apply the pattern to each line ...
172                 Matcher matcher = linePattern.matcher(line);
173                 if (matcher.matches()) {
174                     String mimeType = matcher.group(1).trim().toLowerCase();
175                     String extensions = matcher.group(2).trim().toLowerCase();
176                     if (extensions.length() != 0) {
177                         // A valid mime type with at least one extension was found, so for each extension ...
178                         for (String extensionString : extensions.split("\\s+")) {
179                             extensionString = extensionString.trim();
180                             if (extensionString.length() != 0) {
181                                 // Register the extension with the MIME type ...
182                                 String existingMimeType = mimeTypesByExtension.put(extensionString, mimeType);
183                                 if (existingMimeType != null) {
184                                     // A MIME type already had this extension, so use the first one ...
185                                     mimeTypesByExtension.put(extensionString, existingMimeType);
186                                     if (duplicateMimeTypesByExtension != null) {
187                                         // And record the duplicate ...
188                                         Set<String> dups = duplicateMimeTypesByExtension.get(extensionString);
189                                         if (dups == null) {
190                                             dups = new HashSet<String>();
191                                             duplicateMimeTypesByExtension.put(extensionString, dups);
192                                         }
193                                         dups.add(existingMimeType);
194                                         dups.add(mimeType);
195                                     }
196                                 }
197                             }
198                         }
199                     }
200                 }
201             }
202         }
203         return mimeTypesByExtension;
204     }
205 
206     /**
207      * Returns the MIME-type of a file given the supplied name. If the MIME-type cannot be determined, a <code>null</code> is
208      * returned.
209      * 
210      * @param filename the file name; may be <code>null</code>.
211      * @return The MIME-type of the file, or optionally <code>null</code> if the MIME-type could not be determined.
212      */
213     public String mimeTypeOf( String filename ) {
214         if (filename == null || filename.length() == 0) return null;
215         String trimmedName = filename.trim();
216         if (trimmedName.length() == 0) return null;
217 
218         // Find the extension ...
219         int indexOfDelimiter = trimmedName.lastIndexOf('.');
220         if (indexOfDelimiter < 0) return null;
221         String extension = trimmedName.substring(indexOfDelimiter).toLowerCase();
222 
223         // Look for a match ...
224         return mimeTypesByExtension.get(extension);
225     }
226 
227     /**
228      * Returns the MIME-type of the file using its name. If the MIME-type cannot be determined, a <code>null</code> is returned.
229      * 
230      * @param file the file; may be <code>null</code>.
231      * @return The MIME-type of the file, or optionally <code>null</code> if the MIME-type could not be determined.
232      */
233     public String mimeTypeOf( File file ) {
234         if (file == null) return null;
235         return mimeTypeOf(file.getName());
236     }
237 }