Und wo wir doch mal gerade dabei sind, alles selber zu machen:
In der JDK fehlt eine komfortable Art, den Dateityp einer Datei zu bestimmen.
Zwar gibt es im Packet activation Möglichkeiten, über eine DataSource den MimeTyp einer Datei zu ermitteln, aber sehr komfortabel ist dies nicht. Und es wird wahrscheinlich nur die Dateinamenerweiterung benutzt.
Ich werde hier mal erörtern, wie ich das samt Fileheader-Auswertung gelöst habe.
Diesem Problem ist auch Marco Schmidt begegnet und hat eine kleine API namens “ffident” geschrieben, welche sogar die Fileheader auswerten kann.
Fileheader sind meistens die ersten paar Bytes einer Datei, die für fast jeden Dateityp fix sind (ausgenommen Textdateien).
Doch auch diese Lösung erschien mir nicht passend genug, also habe ich kurzerhand eine eigene Version geschrieben.
Nicht alle Dateiformate besitzen einen eigenen Header, und wenn doch, kann es dennoch vorkommen, dass mehrere Formate denselben Header haben, wie z. B. Morgokruf Office Dokumente, wo Wort und Eksel Dokuemnte dieselben Header besitzen.
Die von mir geschriebenen Klassen lesen ihre Informationen aus einer Properties-Datei und werten beim bestimmen der Dateitypen wahlweise den Dateiheader, die Dateinamenerweiterung oder beides aus.
Zusätzlich werden einige Meta-Informationen über den Dateityp abgelegt, wie z. B. eine Beschreibung und ob es sich um ein Archiv handelt.
mimetypes.properties
mimetype.0=application/zip mimetype.0.fileext=zip, jar mimetype.0.browser=application/zip mimetype.0.description=ZIP Archive mimetype.0.magicbytes=50 4B 03 04 mimetype.0.isarchive=true mimetype.1=application/pdf mimetype.1.fileext=pdf mimetype.1.browser=application/pdf mimetype.1.description=Acrobat PDF Document mimetype.1.magicbytes=25 50 44 46 2D mimetype.1.isarchive=false [...] mimetype.16=application/java-byte-code mimetype.16.fileext=class mimetype.16.browser=application/java-byte-code mimetype.16.description=Java Bytecode mimetype.16.magicbytes=CA FE BA BE 00 mimetype.16.isarchive=false
Dank meiner ExtendedProperties steht bei fileext eine Liste von Dateiendungen, die typisch für das jeweilige Format sind.
In magicbytes steht der Fileheader in Hexkodierter Form. So kann die Erkennung auf beliebige Dateitypen erweitert werden (Bilder, Audio, Video etc.). Da ich derzeit an einer Anwendung für Texte arbeite, enthält meine Datei lediglich weiter verbreitete Textformate und Archive.
Die entsprechende MimeType Klasse sieht dann so aus:
package librarian.util.mimetype; import java.util.HashSet; import java.util.Set; import librarian.util.StringUtils; public class MimeType{ private String mimeType; private HashSet extensions = new HashSet(); private String description; private String browserContentType; private String magicBytes; // as hexstring private boolean isArchive; public String getBrowserContentType() { return browserContentType; } public void setBrowserContentType(String browserContentType) { this.browserContentType = browserContentType; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Set getExtensions() { return extensions; } public void addExtension(String extension){ if (!StringUtils.isEmptyWithTrim(extension)){ extensions.add(extension.trim()); } } public String getMagicBytes() { return magicBytes; } public void setMagicBytes(String magicBytes) { this.magicBytes = magicBytes; } public String getMimeType() { return mimeType; } public void setMimeType(String mimeType) { this.mimeType = mimeType; } public boolean isArchive(){ return isArchive; } public void setIsArchive(boolean isArchive){ this.isArchive = isArchive; } public String toString(){ return "mime="+mimeType+", browser="+browserContentType+ ", magic="+magicBytes+ ", ext="+extensions+ ", archiv="+Boolean.toString(isArchive)+ ", desc="+description; } }
Die Magicbytes werden von Leerzeichen befreit und als String in einer Map abgelegt. Sobald eine Datei geprüft werden soll, wird sie byteweise ausgelesen (unter zur Hilfenahme eines BufferedInputStreams), in einen Hexstring umgewandelt und anschlie�end mit der Map verglichen. So kann die Erkennung recht schnell arbeiten und greift nur einmal auf die Datei zu.
package librarian.util.mimetype; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import librarian.util.StringUtils; import librarian.util.file.FileUtil; import librarian.util.properties.ExtendedProperties; import org.apache.log4j.Logger; public class MimeTypeDetector { private static final Logger LOG = Logger.getLogger(MimeTypeDetector.class); private HashMap fileExtensionsToMimeTypes = new HashMap(); // sets private HashMap magicBytesToMimeTypes = new HashMap(); private HashMap mimeTypesToPropertyIds = new HashMap(); private int maxMagicByteLen; // längster header public MimeTypeDetector(ExtendedProperties config){ int number = 0; List tokens; String token; boolean bool; while (true){ token = config.getString("mimetype."+number); if (StringUtils.isEmptyWithTrim(token)) break; else{ if (LOG.isInfoEnabled()) LOG.info("registering mimetype "+token); MimeType mimetype = new MimeType(); mimetype.setMimeType( token.trim().toLowerCase()); tokens = config.getList( "mimetype."+number+".fileext", true); if (tokens.size()>0){ for (int i=0; i<tokens.size(); i++){ token = (String) tokens.get(i); if (!StringUtils.isEmptyWithTrim(token)) mimetype.addExtension(token.trim(). toLowerCase()); } } else{ LOG.warn("mimetype \""+ mimetype.getMimeType()+ "\" is missing file extensions. it will be skipped."); number++; continue; } token = config.getString("mimetype."+number+ ".browser"); if (StringUtils.isEmptyWithTrim(token)) token = mimetype.getMimeType(); mimetype.setBrowserContentType( token.trim().toLowerCase()); token = config.getString("mimetype."+number+ ".description"); if (StringUtils.isEmptyWithTrim(token)) token = mimetype.getMimeType(); mimetype.setDescription(token.trim()); bool = config.getBoolean("mimetype."+number+ ".isarchive"); mimetype.setIsArchive(bool); token = config.getString("mimetype."+number+ ".magicbytes"); if (!StringUtils.isEmptyWithTrim(token)){ mimetype.setMagicBytes( token.trim().toUpperCase(). replaceAll("\\s", "")); magicBytesToMimeTypes.put( mimetype.getMagicBytes(), mimetype); if (maxMagicByteLen<token.length()) maxMagicByteLen = token.length(); } Set mfileext = mimetype.getExtensions(); Iterator iter = mfileext.iterator(); while (iter.hasNext()){ String ext = (String) iter.next(); Set fileext = (Set) fileExtensionsToMimeTypes. get(ext); if (fileext==null){ fileext = new HashSet(); fileExtensionsToMimeTypes.put( ext, fileext); } fileext.add(mimetype); } mimeTypesToPropertyIds.put( new Integer(number), mimetype); number++; } } } /** * Try to detect by extension. * * @param f * @return */ public MimeType[] detectByFileExtension(File f){ String extension = FileUtil.getFileExtension( f.getName()); if (StringUtils.isEmptyWithTrim(extension)) return null; Set mimes = (Set) this.fileExtensionsToMimeTypes. get(extension); if (mimes==null || mimes.size()<=0) return null; return (MimeType[]) mimes.toArray( new MimeType[mimes.size()]); } /** * Try to detect by fileheader. * * @param f * @return */ public MimeType[] detectByFileHeader(File f){ LinkedHashSet mimes = new LinkedHashSet(); InputStream fi = null; MimeType mime = null; try { fi = new BufferedInputStream( new FileInputStream(f), maxMagicByteLen+10); int readBytes = 0; int readByte = 0; StringBuffer magicBytes = new StringBuffer(maxMagicByteLen+10); String magic; String hex; while (readBytes<=maxMagicByteLen && readByte>-1){ readByte = fi.read(); readBytes++; hex = Integer.toHexString(readByte); if (hex.length()<2) hex = "0"+hex; magicBytes.append(hex); magic = magicBytes.toString(). toUpperCase(); if (LOG.isDebugEnabled()) LOG.debug("magic: "+magic); mime = (MimeType) magicBytesToMimeTypes. get(magic); if (mime!=null) mimes.add(mime); } if (mimes.size()<=0) return null; return (MimeType[]) mimes. toArray(new MimeType[mimes.size()]); } catch (IOException e) { LOG.warn(e); return null; } finally{ try{ if (fi!=null) fi.close(); } catch(Throwable t){} } } /** * First try to detect filetype by file header, * then by fileextension. * * @param f * @return */ public MimeType [] detect(File f){ MimeType [] mimes = detectByFileHeader(f); if (mimes==null) mimes = detectByFileExtension(f); return mimes; } }
Eine Beispielausgabe für eine ZIP und eine PDF Datei sieht wie folgt aus (inkl. Logging):
10:29:08 INFO MimeTypeDetector.: registering mimetype application/zip 10:29:08 INFO MimeTypeDetector. : registering mimetype application/pdf 10:29:08 INFO MimeTypeDetector. : registering mimetype application/gzip 10:29:08 INFO MimeTypeDetector. : registering mimetype application/postscript 10:29:08 INFO MimeTypeDetector. : registering mimetype text/plain 10:29:08 INFO MimeTypeDetector. : registering mimetype application/msword 10:29:08 INFO MimeTypeDetector. : registering mimetype application/rtf 10:29:08 INFO MimeTypeDetector. : registering mimetype text/xml 10:29:08 INFO MimeTypeDetector. : registering mimetype text/html 10:29:08 INFO MimeTypeDetector. : registering mimetype application/mspowerpoint 10:29:08 INFO MimeTypeDetector. : registering mimetype application/x-tex 10:29:08 INFO MimeTypeDetector. : registering mimetype application/tar 10:29:14 DEBUG MimeTypeDetector.detect: magic: 50 10:29:14 DEBUG MimeTypeDetector.detect: magic: 504B 10:29:14 DEBUG MimeTypeDetector.detect: magic: 504B03 10:29:14 DEBUG MimeTypeDetector.detect: magic: 504B0304 mime=application/zip, browser=application/zip, magic=504B0304, ext=[zip, jar], archiv=true, desc=ZIP Archive 10:29:15 DEBUG MimeTypeDetector.detect: magic: 25 10:29:15 DEBUG MimeTypeDetector.detect: magic: 2550 10:29:15 DEBUG MimeTypeDetector.detect: magic: 255044 10:29:15 DEBUG MimeTypeDetector.detect: magic: 25504446 10:29:15 DEBUG MimeTypeDetector.detect: magic: 255044462D mime=application/pdf, browser=application/pdf, magic=255044462D, ext=[pdf], archiv=false, desc=Acrobat PDF Document

