#

File Types

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
Tags:, , ,

7 Responses to “File Types” »»

  1. Comment by Secco | 12:47 27.10.06|X

    Ãbrigens kann man sich die entsprechenden Informationen über MimeType und Magicbytes auf der Seite filext.com holen.

  2. Comment by andrej | 13:42 27.10.06|X

    hast du mal auch die bereits vorhanden implementierungen dir angeschaut? z.b. im jdic-packet gibt es was für file-types: http://java.sun.com/developer/technicalArticles/J2SE/Desktop/jdic_assoc/

    in swt gibt es ebefalls eine erweiterung, die für bestimmte zwecke nuetzlich ist.

    wofür brauchst du das ganze eigentlich? um den mime-type fest zu stellen?

  3. Comment by Secco | 0:37 28.10.06|X

    Mein Anliegen war es nicht einfach, wie es bei JDIC passiert, ein Programm zu öffnen, das mit einer Endung verknüpft ist, sondern den „echten“ Dateitypen einer Datei zu ermitteln.

    Diesen kann man üblicherweise nur über den Fileheader (magic bytes) herausfinden (z. B. wenn man eine ZIP Datei zu txt umbenennt).

    Ich wollte genau das machen. Meine Lösung identifiziert den Filetype, falls es den Fielheader nicht identifizieren kann, einfach durch die Endung.

    Das mit den Mime-Types hat folgenden Hintergrund:
    Ich arbeite gerade an einem Webprojekt, bei dem der Benutzer Dateien up- und downloaden kann. Nach dem upload benötige ich den echten Filetype, um die Datei weiter zu verarbeiten, wie z. B. über PDFBox PDF’s lesen, oder über POI Office Dokumente. Beim Downloaden möchte ich zu der Datei den entsprechenden Mime-Typen liefern, damit der Browser direkt etwas damit anfangen kann.

    Ich habe die Klassen wie oben bereits weiterenwickelt und verbessert.

    Wozu ich das alles mache werde ich vielleicht zu einem späteren Zeitpunkt, wenn die Anwendung etwas von dem, was es können soll, mehr kann, mal posten 😉

  4. Comment by Ridvan | 12:40 11.03.08|X

    Wo kann ich denn librarian.util.StringUtils finden?

  5. Comment by Ridvan | 12:46 11.03.08|X

    Gibt’s das ganze Paket zum Herunterladen?
    Ich habe nämlich gerade gemerkt es gibt noch welche Klassen von Dir die im Code gebraucht wird.

  6. Comment by Secco | 13:46 18.03.08|X

    Hi Ridvan. Die ExtendedProperties findest du in einem anderen Posting von mir.
    Die wenigen Methoden, die aus StringUtils und FileUtils genutzt werden sind selbsterklärend.
    Bei gelegenheit kann ich ja mal schauen, ob ich den entsprechenden Kode noch da habe und aufspielen kann.

  7. Comment by Michal | 12:22 18.03.10|X

    Hallo, finden Sie Informationen über den MIME-Typen es auf Web-Seiten file-extensions.org.

Leave a Reply »»

Note: All comments are manually approved to avoid spam. So if your comment doesn't appear immediately, that's ok. Have patience, it can take some days until I have the time to approve my comments.