/** * Licensed to The Apereo Foundation under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * * The Apereo Foundation licenses this file to you under the Educational * Community License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License * at: * * http://opensource.org/licenses/ecl2.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * */ package org.opencastproject.util; import static org.opencastproject.util.MimeType.mimeType; import static org.opencastproject.util.data.Monadics.mlist; import static org.opencastproject.util.data.Option.none; import static org.opencastproject.util.data.Option.option; import static org.opencastproject.util.data.Option.some; import org.opencastproject.util.data.Collections; import org.opencastproject.util.data.Option; import org.opencastproject.util.data.functions.Options; import org.opencastproject.util.data.functions.Strings; import com.entwinemedia.fn.Fn; import com.entwinemedia.fn.data.Opt; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.helpers.DefaultHandler; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringReader; import java.net.URI; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; /** * This class represents the mime type registry that is responsible for providing resolving mime types through all * system components. * <p> * The registry is initialized from the file <code>org.opencastproject.util.MimeTypes.xml</code>. */ public final class MimeTypes { /** Disallow construction of this utility class */ private MimeTypes() { } public static final Pattern MIME_TYPE_PATTERN = Pattern.compile("([a-zA-Z0-9-]+)/([a-zA-Z0-9-+.]+)"); /** Name of the mime type files */ public static final String DEFINITION_FILE = "/org/opencastproject/util/MimeTypes.xml"; /** The mime types */ private static final List<MimeType> mimeTypes = new ArrayList<MimeType>(); /** the logging facility provided by log4j */ private static final Logger logger = LoggerFactory.getLogger(MimeType.class); /** Common mime types */ public static final MimeType XML; public static final MimeType TEXT; public static final MimeType JSON; public static final MimeType JPG; public static final MimeType MJPEG; public static final MimeType MPEG4; public static final MimeType MATROSKA; public static final MimeType MPEG4_AAC; public static final MimeType DV; public static final MimeType MJPEG2000; public static final MimeType MP3; public static final MimeType AAC; public static final MimeType CALENDAR; public static final MimeType ZIP; public static final MimeType JAR; public static final MimeType SMIL; public static final MimeType PNG; // Initialize common mime types static { XML = MimeTypes.parseMimeType("text/xml"); TEXT = MimeTypes.parseMimeType("text/plain"); JSON = MimeTypes.parseMimeType("application/json"); JPG = MimeTypes.parseMimeType("image/jpg"); MJPEG = MimeTypes.parseMimeType("video/x-motion-jpeg"); MPEG4 = MimeTypes.parseMimeType("video/mp4"); MATROSKA = MimeTypes.parseMimeType("video/x-matroska"); MPEG4_AAC = MimeTypes.parseMimeType("video/x-m4v"); DV = MimeTypes.parseMimeType("video/x-dv"); MJPEG2000 = MimeTypes.parseMimeType("video/mj2"); MP3 = MimeTypes.parseMimeType("audio/mpeg"); AAC = MimeTypes.parseMimeType("audio/x-m4a"); CALENDAR = MimeTypes.parseMimeType("text/calendar"); ZIP = MimeTypes.parseMimeType("application/zip"); JAR = MimeTypes.parseMimeType("application/java-archive"); SMIL = MimeTypes.parseMimeType("application/smil"); PNG = MimeTypes.parseMimeType("image/png"); // initialize from file InputStream is = null; InputStreamReader isr = null; try { String definitions = null; is = MimeTypes.class.getResourceAsStream(DEFINITION_FILE); StringBuilder buf = new StringBuilder(); if (is == null) throw new FileNotFoundException(DEFINITION_FILE); isr = new InputStreamReader(is); char[] chars = new char[1024]; int count = 0; while ((count = isr.read(chars)) > 0) { for (int i = 0; i < count; i++) { buf.append(chars[i]); } } definitions = buf.toString(); SAXParserFactory parserFactory = SAXParserFactory.newInstance(); SAXParser parser = parserFactory.newSAXParser(); DefaultHandler handler = new MimeTypeParser(mimeTypes); parser.parse(new InputSource(new StringReader(definitions)), handler); } catch (FileNotFoundException e) { logger.error("Error initializing mime type registry: definition file not found!"); } catch (IOException e) { logger.error("Error initializing mime type registry: " + e.getMessage()); } catch (ParserConfigurationException e) { logger.error("Configuration error while parsing mime type registry: " + e.getMessage()); } catch (SAXException e) { logger.error("Error parsing mime type registry: " + e.getMessage()); } finally { IOUtils.closeQuietly(isr); IOUtils.closeQuietly(is); } } /** Get a mime type from the registry. */ public static Option<MimeType> get(String type, String subtype) { for (MimeType t : mimeTypes) { if (t.getType().equals(type) && t.getSubtype().equals(subtype)) return some(t); } return none(); } /** Get a mime type from the registry or create a new one if not available. */ public static MimeType getOrCreate(String type, String subtype) { for (MimeType t : get(type, subtype)) return t; return mimeType(type, subtype); } public static final Fn<String, Opt<MimeType>> toMimeType = new Fn<String, Opt<MimeType>>() { @Override public Opt<MimeType> apply(String name) { try { return Opt.some(fromString(name)); } catch (Exception e) { return Opt.none(); } } }; /** * Returns a mime type for the given type and subtype, e. g. <code>video/mj2</code>. * * @param mimeType * the mime type * @return the corresponding mime type */ public static MimeType parseMimeType(String mimeType) { final Matcher m = MIME_TYPE_PATTERN.matcher(mimeType); if (!m.matches()) throw new IllegalArgumentException("Malformed mime type '" + mimeType + "'"); final String type = m.group(1); final String subtype = m.group(2); for (MimeType t : mimeTypes) { if (t.getType().equals(type) && t.getSubtype().equals(subtype)) return t; } return mimeType(type, subtype); } /** * Returns a mime type for the provided file suffix. * <p> * For example, if the suffix is <code>mj2</code>, the mime type will be that of a ISO Motion JPEG 2000 document. * <p> * If no mime type is found for the suffix, a <code>UnknownFileTypeException</code> is thrown. * * @param suffix * the file suffix * @return the corresponding mime type * @throws UnknownFileTypeException * if the suffix does not map to a mime type */ public static MimeType fromSuffix(String suffix) throws UnknownFileTypeException { if (suffix == null) throw new IllegalArgumentException("Argument 'suffix' was null!"); for (MimeType m : mimeTypes) { if (m.supportsSuffix(suffix)) return m; } throw new UnknownFileTypeException("File suffix '" + suffix + "' cannot be matched to any mime type"); } /** * Returns a mime type for the provided file. * <p> * This method tries various ways to extract mime type information from the files name or its contents. * <p> * If no mime type can be derived from either the file name or its contents, a <code>UnknownFileTypeException</code> * is thrown. * * @param url * the file * @return the corresponding mime type * @throws UnknownFileTypeException * if the mime type cannot be derived from the file */ public static MimeType fromURL(URL url) throws UnknownFileTypeException { if (url == null) throw new IllegalArgumentException("Argument 'url' is null"); return fromString(url.getFile()); } /** * Returns a mime type for the provided file. * <p> * This method tries various ways to extract mime type information from the files name or its contents. * <p> * If no mime type can be derived from either the file name or its contents, a <code>UnknownFileTypeException</code> * is thrown. * * @param uri * the file * @return the corresponding mime type * @throws UnknownFileTypeException * if the mime type cannot be derived from the file */ public static MimeType fromURI(URI uri) throws UnknownFileTypeException { if (uri == null) throw new IllegalArgumentException("Argument 'uri' is null"); return fromString(uri.getPath()); } /** * Returns a mime type for the provided file name. * <p> * This method tries various ways to extract mime type information from the files name or its contents. * <p> * If no mime type can be derived from either the file name or its contents, a <code>UnknownFileTypeException</code> * is thrown. * * @param name * the file * @return the corresponding mime type * @throws UnknownFileTypeException * if the mime type cannot be derived from the file */ public static MimeType fromString(String name) throws UnknownFileTypeException { if (name == null) throw new IllegalArgumentException("Argument 'name' is null"); MimeType mimeType = null; // Extract suffix String filename = name; String suffix = null; int separatorPos = filename.lastIndexOf('.'); if (separatorPos > 0 && separatorPos < filename.length() - 1) { suffix = filename.substring(separatorPos + 1); } else { throw new UnknownFileTypeException("Unable to get mime type without suffix"); } // Try to get mime type for file suffix try { mimeType = fromSuffix(suffix); if (mimeType != null) return mimeType; } catch (UnknownFileTypeException e) { throw e; } // TODO // Try to match according to file contents // if (mimeType == null) { // for (MimeType m : mimeTypes_.values()) { // // TODO: Search file contents for mime type using magic bits // } // } throw new UnknownFileTypeException("File '" + name + "' cannot be matched to any mime type"); } /** * Reads the mime type definitions from the xml file comming with this distribution. */ private static class MimeTypeParser extends DefaultHandler { /** The mime types */ private List<MimeType> registry = null; /** Element content */ private StringBuffer content = new StringBuffer(); /** Type */ private String type = null; /** Description */ private String description = null; /** Extensions, comma separated */ private String extensions = null; /** * Creates a new mime type reader. * * @param registry * the registry */ MimeTypeParser(List<MimeType> registry) { this.registry = registry; } @Override public void characters(char[] ch, int start, int length) throws SAXException { super.characters(ch, start, length); content.append(ch, start, length); } /** * Returns the element content. * * @return the element content */ private String getContent() { String str = content.toString(); content = new StringBuffer(); return str; } @Override public void endElement(String uri, String localName, String name) throws SAXException { super.endElement(uri, localName, name); if ("Type".equals(name)) { this.type = getContent(); return; } else if ("Description".equals(name)) { this.description = getContent(); return; } if ("Extensions".equals(name)) { this.extensions = getContent(); return; } else if ("MimeType".equals(name)) { String[] t = type.split("/"); MimeType mimeType = mimeType(t[0].trim(), t[1].trim(), mlist(extensions.split(",")).bind(Options.<String> asList().o(Strings.trimToNone)).value(), Collections.<MimeType> nil(), option(description), none(""), none("")); registry.add(mimeType); } } @Override public void warning(SAXParseException e) throws SAXException { super.warning(e); } @Override public void error(SAXParseException e) throws SAXException { super.error(e); } @Override public void fatalError(SAXParseException e) throws SAXException { super.fatalError(e); } } }