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 }