diff --git a/openide.filesystems/apichanges.xml b/openide.filesystems/apichanges.xml --- a/openide.filesystems/apichanges.xml +++ b/openide.filesystems/apichanges.xml @@ -46,6 +46,21 @@ Filesystems API + + + Added new elements to MIME resolver DTD + + + + + +

+ It is now possible to write declarive MIME resolvers checking + file names (element name) and file content (element pattern). +

+
+ +
Support for annotation processors generating XML layer fragments diff --git a/openide.filesystems/src/org/netbeans/modules/openide/filesystems/declmime/MIMEResolverImpl.java b/openide.filesystems/src/org/netbeans/modules/openide/filesystems/declmime/MIMEResolverImpl.java --- a/openide.filesystems/src/org/netbeans/modules/openide/filesystems/declmime/MIMEResolverImpl.java +++ b/openide.filesystems/src/org/netbeans/modules/openide/filesystems/declmime/MIMEResolverImpl.java @@ -46,7 +46,9 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.openide.filesystems.FileChangeAdapter; @@ -212,8 +214,9 @@ } /** For debug purposes. */ + @Override public String toString() { - return "MIMEResolverImpl.Impl[" + data + ", " + smell + "]"; // NOI18N + return "MIMEResolverImpl.Impl[" + data.getPath() + "]"; // NOI18N } @@ -236,6 +239,10 @@ // references active resolver component private MIMEComponent component = null; private String componentDelimiter = null; + // holds level of pattern element + private int patternLevel = 0; + // used to prohibit more pattern elements on the same level + Set patternLevelSet; DescParser(FileObject fo) { @@ -247,6 +254,7 @@ private static final short IN_FILE = 2; private static final short IN_RESOLVER = 3; private static final short IN_COMPONENT = 4; + private static final short IN_PATTERN = 5; // second state dimension private static final short IN_EXIT = INIT + 1; @@ -259,13 +267,19 @@ private static final String RESOLVER = "resolver"; // NOI18N private static final String FATTR = "fattr"; // NOI18N private static final String NAME = "name"; // NOI18N + private static final String PATTERN = "pattern"; // NOI18N + private static final String VALUE = "value"; // NOI18N + private static final String RANGE = "range"; // NOI18N + private static final String IGNORE_CASE = "ignorecase"; // NOI18N + private static final String SUBSTRING = "substring"; // NOI18N private static final String MAGIC = "magic"; // NOI18N private static final String HEX = "hex"; // NOI18N private static final String MASK = "mask"; // NOI18N - private static final String VALUE = "text"; // NOI18N + private static final String TEXT = "text"; // NOI18N private static final String EXIT = "exit"; // NOI18N private static final String XML_RULE_COMPONENT = "xml-rule"; // NOI18N + @Override public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { String s; @@ -337,14 +351,56 @@ } else if (FATTR.equals(qName)) { s = atts.getValue(NAME); if (s == null) error(); - String val = atts.getValue(VALUE); + String val = atts.getValue(TEXT); template[0].fileCheck.addAttr(s, val); + + } else if (PATTERN.equals(qName)) { + + s = atts.getValue(VALUE); if (s == null) error(); + int range = Integer.valueOf(atts.getValue(RANGE)); + boolean ignoreCase = Type.FilePattern.DEFAULT_IGNORE_CASE; + String ignoreCaseAttr = atts.getValue(IGNORE_CASE); + if (ignoreCaseAttr != null) { + ignoreCase = Boolean.valueOf(ignoreCaseAttr); + } + if (file_state == IN_PATTERN) { + if (patternLevelSet == null) { + patternLevelSet = new HashSet(); + } + if (!patternLevelSet.add(patternLevel)) { + error("Second pattern element on the same level not allowed"); //NOI18N + } + template[0].fileCheck.addInnerPattern(s, range, ignoreCase); + } else { + template[0].fileCheck.addPattern(s, range, ignoreCase); + file_state = IN_PATTERN; + } + patternLevel++; + break; + + } else if (NAME.equals(qName)) { + + s = atts.getValue(NAME); if (s == null) error(); + String substringAttr = atts.getValue(SUBSTRING); + boolean substring = Type.FileName.DEFAULT_SUBSTRING; + if (substringAttr != null) { + substring = Boolean.valueOf(substringAttr); + } + boolean ignoreCase = Type.FileName.DEFAULT_IGNORE_CASE; + String ignoreCaseAttr = atts.getValue(IGNORE_CASE); + if (ignoreCaseAttr != null) { + ignoreCase = Boolean.valueOf(ignoreCaseAttr); + } + template[0].fileCheck.addName(s, substring, ignoreCase); + break; } else if (RESOLVER.equals(qName)) { if (template[0].fileCheck.exts == null && template[0].fileCheck.mimes == null - && template[0].fileCheck.fatts == null + && template[0].fileCheck.fatts == null + && template[0].fileCheck.patterns == null + && template[0].fileCheck.names == null && template[0].fileCheck.magic == null) { error(); // at least one must be specified } @@ -387,7 +443,7 @@ component.startElement(namespaceURI, localName, qName, atts); break; - + default: } @@ -402,12 +458,19 @@ state = IN_COMPONENT; } + @Override public void endElement(String namespaceURI, String localName, String qName) throws SAXException { switch (state) { case IN_FILE: if (FILE.equals(qName)) { state = IN_ROOT; file_state = INIT; + } + if (PATTERN.equals(qName)) { + if (--patternLevel == 0) { + patternLevelSet = null; + file_state = INIT; + } } break; @@ -427,6 +490,7 @@ } } + @Override public void characters(char[] data, int offset, int len) throws SAXException { if (state == IN_COMPONENT) component.characters(data, offset, len); } @@ -475,6 +539,7 @@ /** * For debug puroses only. */ + @Override public String toString() { StringBuffer buf = new StringBuffer(); buf.append("FileElement("); @@ -497,16 +562,176 @@ private static class Type { Type() {} private String[] exts; + private static final String EMPTY_EXTENSION = ""; private String[] mimes; private String[] fatts; + private List patterns; + private FilePattern lastAddedPattern = null; + private List names; private String[] vals; // contains null or value of attribute at the same index private byte[] magic; private byte[] mask; - + /** Used to search in the file for given pattern in given range. If there is an inner + * pattern element, it is used only if outer is fulfilled. Searching starts + * always from the beginning of the file. For example: + *

+ * Pattern <?php in first 255 bytes + *

+         *      <pattern value="<?php" range="255"/>
+         * 
+ *

+ *

+ * Pattern <HTML>> or <html> in first 255 bytes and pattern <?php in first 4000 bytes. + *

+         *      <pattern value="<HTML>" range="255" ignorecase="true">
+         *          <pattern value="<?php" range="4000"/>
+         *      </pattern>
+         * 
+ *

+ */ + private class FilePattern { + // case sensitive by default + private static final boolean DEFAULT_IGNORE_CASE = false; + private final String value; + private final int range; + private final boolean ignoreCase; + private FilePattern inner; + private int pointer = 0; + private final byte[] bytes; + private final int valueLength; + + public FilePattern(String value, int range, boolean ignoreCase) { + this.value = value; + this.valueLength = value.length(); + if (ignoreCase) { + this.bytes = value.toLowerCase().getBytes(); + } else { + this.bytes = value.getBytes(); + } + this.range = range; + this.ignoreCase = ignoreCase; + } + + public void setInner(FilePattern inner) { + this.inner = inner; + } + + private boolean match(byte b) { + if (pointer < valueLength) { + if (b == bytes[pointer]) { + pointer++; + } else { + pointer = 0; + return false; + } + } + return pointer >= valueLength; + } + + /** Read from given file and compare byte-by-byte if pattern + * appers in given range. + */ + public boolean match(FileObject fo) throws IOException { + pointer = 0; + InputStream is = null; + boolean matched = false; + try { + is = fo.getInputStream(); // it is CachedInputStream, so you can call getInputStream and read more times without performance penalty + byte[] byteRange = new byte[range]; + int read = is.read(byteRange); + for (int i = 0; i < read; i++) { + byte b = byteRange[i]; + if (ignoreCase) { + b = (byte) Character.toLowerCase(b); + } + if (match(b)) { + matched = true; + break; + } + } + } finally { + try { + if (is != null) { + is.close(); + } + } catch (IOException ioe) { + // already closed + } + } + if (matched) { + if (inner == null) { + return true; + } else { + return inner.match(fo); + } + } + return false; + } + + @Override + public String toString() { + return "[" + value + ", " + range + ", " + ignoreCase + (inner != null ? ", " + inner : "") + "]"; + } + } + + /** Used to compare filename with given name. + * For example: + *

+ * Filename matches makefile, Makefile, MaKeFiLe, mymakefile, gnumakefile, makefile1, .... + *

+         *      <name name="makefile" substring="true"/>
+         * 
+ *

+ *

+ * Filename exactly matches rakefile or Rakefile. + *

+         *      <name name="rakefile" ignorecase="false"/>
+         *      <name name="Rakefile" ignorecase="false"/>
+         * 
+ *

+ */ + private class FileName { + + // case insensitive by default + private static final boolean DEFAULT_IGNORE_CASE = true; + private static final boolean DEFAULT_SUBSTRING = false; + private final String name; + private final boolean substring; + private final boolean ignoreCase; + + public FileName(String name, boolean substring, boolean ignoreCase) { + if (ignoreCase) { + this.name = name.toLowerCase(); + } else { + this.name = name; + } + this.substring = substring; + this.ignoreCase = ignoreCase; + } + + public boolean match(FileObject fo) { + String nameAndExt = fo.getNameExt(); + if (ignoreCase) { + nameAndExt = nameAndExt.toLowerCase(); + } + if (substring) { + return nameAndExt.indexOf(name) != -1; + } else { + return nameAndExt.equals(name); + } + } + + @Override + public String toString() { + return "[" + name + ", " + substring + ", " + ignoreCase + "]"; + } + } + /** * For debug purposes only. */ + @Override public String toString() { int i = 0; StringBuffer buf = new StringBuffer(); @@ -529,6 +754,20 @@ buf.append("file-attributes:"); for (i = 0; i(); + } + lastAddedPattern = new FilePattern(value, range, ignoreCase); + patterns.add(lastAddedPattern); + } + + private void addInnerPattern(String value, int range, boolean ignoreCase) { + FilePattern inner = new FilePattern(value, range, ignoreCase); + lastAddedPattern.setInner(inner); + lastAddedPattern = inner; + } + + private void addName(String name, boolean substring, boolean ignoreCase) { + if (names == null) { + names = new ArrayList(); + } + names.add(new FileName(name, substring, ignoreCase)); + } + private boolean setMagic(byte[] magic, byte[] mask) { if (magic == null) return true; if (mask != null && magic.length != mask.length) return false; @@ -574,11 +834,26 @@ private static String getMIMEType(String extension) { return FileUtil.getMIMEType(extension); } + + /** #26521, 114976 - ignore not readable and windows' locked files. */ + private static void handleIOException(FileObject fo, IOException ioe) throws IOException { + if (fo.canRead()) { + if (!Utilities.isWindows() || !(ioe instanceof FileNotFoundException) || !fo.isValid() || fo.getName().toLowerCase().indexOf("ntuser") == -1) {//NOI18N + throw ioe; + } + } + } + private boolean accept(FileObject fo) throws IOException { // check for resource extension if (exts != null) { - if (fo.getExt() == null) return false; - if (!Util.contains(exts, fo.getExt(), CASE_INSENSITIVE)) return false; + String ext = fo.getExt(); + if (ext == null) { + ext = EMPTY_EXTENSION; + } + if (!Util.contains(exts, ext, CASE_INSENSITIVE)) { + return false; + } } // check for resource mime type @@ -613,63 +888,36 @@ if (magic != null) { byte[] header = new byte[magic.length]; -// System.err.println("FO" + fo); - -// String m = mask == null ? "" : " mask " + XMLUtil.toHex(mask, 0, mask.length); -// System.err.println("Magic test " + XMLUtil.toHex(magic, 0, magic.length) + m); - // fetch header InputStream in = null; - boolean unexpectedEnd = false; try { in = fo.getInputStream(); - for (int i = 0; i escape loop, otherwise continue + matched = true; + break; + } + } + if (!matched) { + return false; + } + } catch (IOException ioe) { + handleIOException(fo, ioe); + return false; + } + } + + // check file name + if (names != null) { + boolean matched = false; + for (FileName name : names) { + if(name.match(fo)) { + // at least one matched => escape loop, otherwise continue + matched = true; + break; + } + } + if (!matched) { + return false; + } + } + // all templates matched return true; } - } } diff --git a/openide.filesystems/src/org/netbeans/modules/openide/filesystems/declmime/resolver.dtd b/openide.filesystems/src/org/netbeans/modules/openide/filesystems/declmime/resolver.dtd --- a/openide.filesystems/src/org/netbeans/modules/openide/filesystems/declmime/resolver.dtd +++ b/openide.filesystems/src/org/netbeans/modules/openide/filesystems/declmime/resolver.dtd @@ -27,7 +27,7 @@ Contributor(s): The Original Software is NetBeans. The Initial Developer of the Original -Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun +Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun Microsystems, Inc. All Rights Reserved. If you wish your version of this file to be governed by only the CDDL @@ -85,10 +85,11 @@ so there is a danger of recursion if these call this.getMIMEType().

--> - + @@ -123,6 +124,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + ]]> + + + + + + + + + + + ]]> + + + + + + + + + + + + + + + + + + + + + ]]> + + + + + + + + + + + + + ]]> + + + + + + + + + ]]> + + + + + + + + + + ]]> + + + + + + + + ]]> + + + + + + + + + + + + + + ]]> + diff --git a/openide.filesystems/test/unit/src/org/netbeans/modules/openide/filesystems/declmime/data-fs.xml b/openide.filesystems/test/unit/src/org/netbeans/modules/openide/filesystems/declmime/data-fs.xml --- a/openide.filesystems/test/unit/src/org/netbeans/modules/openide/filesystems/declmime/data-fs.xml +++ b/openide.filesystems/test/unit/src/org/netbeans/modules/openide/filesystems/declmime/data-fs.xml @@ -37,5 +37,44 @@ "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> ]]> + + + + + +0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 + + + + + + + + + + + + + + + + + + +