001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.core.filesystems;
043:
044: import java.io.File;
045: import java.io.FileNotFoundException;
046: import java.io.IOException;
047: import java.io.InputStream;
048: import java.util.logging.Level;
049: import java.util.logging.Logger;
050: import org.openide.cookies.InstanceCookie;
051: import org.openide.filesystems.FileObject;
052: import org.openide.filesystems.FileUtil;
053: import org.openide.filesystems.MIMEResolver;
054: import org.openide.loaders.DataObject;
055: import org.openide.loaders.Environment;
056: import org.openide.util.Utilities;
057: import org.openide.util.lookup.InstanceContent;
058: import org.openide.xml.XMLUtil;
059: import org.xml.sax.Attributes;
060: import org.xml.sax.SAXException;
061:
062: /**
063: * MIMEResolver implementation driven by an XML document instance
064: * following PUBLIC "-//NetBeans//DTD MIME Resolver 1.0//EN".
065: *
066: * <p>
067: * 1. It provides Environment for XMLDataObjects with above public ID.
068: * <p>
069: * 2. Provided environment returns (InstanceCookie) Impl instance.
070: * <p>
071: * 3. [Instance]Lookup return that Impl instance.
072: * <p>
073: * 4. MIMEResolver's findMIMEType() parses description file and applies checks on passed files.
074: * <p>
075: * <b>Note:</b> It is public to be accessible by XML layer.
076: *
077: * @author Petr Kuzel
078: */
079: public final class MIMEResolverImpl extends XMLEnvironmentProvider
080: implements Environment.Provider {
081:
082: private static final long serialVersionUID = 18975L;
083:
084: // enable some tracing
085: private static final Logger ERR = Logger
086: .getLogger(MIMEResolverImpl.class.getName());
087:
088: private static final boolean CASE_INSENSITIVE = Utilities
089: .isWindows()
090: || Utilities.getOperatingSystem() == Utilities.OS_VMS;
091:
092: // DefaultEnvironmentProvider~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
093:
094: protected InstanceContent createInstanceContent(DataObject obj) {
095: FileObject fo = obj.getPrimaryFile();
096: InstanceContent ic = new InstanceContent();
097: ic.add(new Impl(fo));
098: return ic;
099: }
100:
101: // MIMEResolver ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
102:
103: //
104: // It implements InstanceCookie because it is added to environment of XML document.
105: // The cookie return itself i.e. MIMEResolver to be searchable by Lookup.
106: //
107: static class Impl extends MIMEResolver implements InstanceCookie {
108: // This file object describes rules that drive ths instance
109: private final FileObject data;
110:
111: // Resolvers in reverse order
112: private FileElement[] smell = null;
113:
114: private short state = DescParser.INIT;
115:
116: Impl(FileObject obj) {
117: if (ERR.isLoggable(Level.FINE))
118: ERR.fine("MIMEResolverImpl.Impl.<init>(" + obj + ")"); // NOI18N
119: data = obj;
120: }
121:
122: /**
123: * Resolves FileObject and returns recognized MIME type
124: * @param fo is FileObject which should be resolved
125: * @return recognized MIME type or null if not recognized
126: */
127: public String findMIMEType(FileObject fo) {
128:
129: synchronized (this ) { // lazy init
130:
131: if (state == DescParser.INIT) {
132: state = parseDesc();
133: }
134:
135: if (state == DescParser.ERROR) {
136: return null;
137: }
138: }
139:
140: // smell is filled in reverse order
141:
142: for (int i = smell.length - 1; i >= 0; i--) {
143: String s = smell[i].resolve(fo);
144: if (s != null) {
145: if (ERR.isLoggable(Level.FINE))
146: ERR.fine("MIMEResolverImpl.findMIMEType(" + fo
147: + ")=" + s); // NOI18N
148: return s;
149: }
150: }
151:
152: return null;
153: }
154:
155: // description document is parsed in the same thread
156: private short parseDesc() {
157: smell = new FileElement[0];
158: DescParser parser = new DescParser(data);
159: parser.parse();
160: smell = (parser.template != null) ? parser.template : smell;
161: if (ERR.isLoggable(Level.FINE)) {
162: if (parser.state == DescParser.ERROR) {
163: ERR.fine("MIMEResolverImpl.Impl parsing error!");
164: } else {
165: StringBuffer buf = new StringBuffer();
166: buf.append("Parse: ");
167: for (int i = 0; i < smell.length; i++)
168: buf.append('\n').append(smell[i]);
169: ERR.fine(buf.toString());
170: }
171: }
172: return parser.state;
173: }
174:
175: // InstanceCookie ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
176:
177: public Object instanceCreate() {
178: return this ;
179: }
180:
181: public Class instanceClass() {
182: return this .getClass();
183: }
184:
185: public String instanceName() {
186: return this .getClass().getName();
187: }
188:
189: /** For debug purposes. */
190: public String toString() {
191: return "MIMEResolverImpl.Impl[" + data + ", " + smell + "]"; // NOI18N
192: }
193:
194: }
195:
196: // XML -> memory representation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
197:
198: /**
199: * Resonsible for parsing backend FileObject and filling resolvers
200: * in memory structure according to it.
201: */
202: private static class DescParser extends DefaultParser {
203:
204: private FileElement[] template = null;
205:
206: // file state substates
207: private short file_state = INIT;
208:
209: // references active resolver component
210: private MIMEComponent component = null;
211: private String componentDelimiter = null;
212:
213: DescParser(FileObject fo) {
214: super (fo);
215: }
216:
217: // pseudo validation states
218: private static final short IN_ROOT = 1;
219: private static final short IN_FILE = 2;
220: private static final short IN_RESOLVER = 3;
221: private static final short IN_COMPONENT = 4;
222:
223: // second state dimension
224: private static final short IN_EXIT = INIT + 1;
225:
226: // grammar elements
227: private static final String ROOT = "MIME-resolver"; // NOI18N
228: private static final String FILE = "file"; // NOI18N
229: private static final String MIME = "mime"; // NOI18N
230: private static final String EXT = "ext"; // NOI18N
231: private static final String RESOLVER = "resolver"; // NOI18N
232: private static final String FATTR = "fattr"; // NOI18N
233: private static final String NAME = "name"; // NOI18N
234: private static final String MAGIC = "magic"; // NOI18N
235: private static final String HEX = "hex"; // NOI18N
236: private static final String MASK = "mask"; // NOI18N
237: private static final String VALUE = "text"; // NOI18N
238: private static final String EXIT = "exit"; // NOI18N
239: private static final String XML_RULE_COMPONENT = "xml-rule"; // NOI18N
240:
241: public void startElement(String namespaceURI, String localName,
242: String qName, Attributes atts) throws SAXException {
243:
244: String s;
245:
246: switch (state) {
247:
248: case INIT:
249:
250: if (ROOT.equals(qName) == false)
251: error();
252: state = IN_ROOT;
253: break;
254:
255: case IN_ROOT:
256: if (FILE.equals(qName) == false)
257: error();
258:
259: // prepare file element structure
260: // actual one is at index 0
261:
262: if (template == null) {
263: template = new FileElement[] { new FileElement() };
264: } else {
265: FileElement[] n = new FileElement[template.length + 1];
266: System
267: .arraycopy(template, 0, n, 1,
268: template.length);
269: n[0] = new FileElement();
270: template = n;
271: }
272:
273: state = IN_FILE;
274: break;
275:
276: case IN_FILE:
277:
278: if (file_state == IN_EXIT)
279: error();
280:
281: if (EXT.equals(qName)) {
282:
283: s = atts.getValue(NAME);
284: if (s == null)
285: error();
286: template[0].fileCheck.addExt(s);
287:
288: } else if (MAGIC.equals(qName)) {
289:
290: s = atts.getValue(HEX);
291: if (s == null)
292: error();
293: String mask = atts.getValue(MASK);
294:
295: char[] chars = s.toCharArray();
296: byte[] mask_bytes = null; // mask is optional
297:
298: try {
299:
300: if (mask != null) {
301: char[] mask_chars = mask.toCharArray();
302: mask_bytes = XMLUtil.fromHex(mask_chars, 0,
303: mask_chars.length);
304: }
305:
306: byte[] magic = XMLUtil.fromHex(chars, 0,
307: chars.length);
308: if (template[0].fileCheck.setMagic(magic,
309: mask_bytes) == false) {
310: error();
311: }
312: } catch (IOException ioex) {
313: error();
314: }
315:
316: } else if (MIME.equals(qName)) {
317:
318: s = atts.getValue(NAME);
319: if (s == null)
320: error();
321: template[0].fileCheck.addMIME(s);
322:
323: } else if (FATTR.equals(qName)) {
324:
325: s = atts.getValue(NAME);
326: if (s == null)
327: error();
328: String val = atts.getValue(VALUE);
329: template[0].fileCheck.addAttr(s, val);
330:
331: } else if (RESOLVER.equals(qName)) {
332:
333: if (template[0].fileCheck.exts == null
334: && template[0].fileCheck.mimes == null
335: && template[0].fileCheck.fatts == null
336: && template[0].fileCheck.magic == null) {
337: error(); // at least one must be specified
338: }
339:
340: s = atts.getValue(MIME);
341: if (s == null)
342: error();
343: template[0].setMIME(s);
344:
345: state = IN_RESOLVER;
346:
347: break;
348:
349: } else if (EXIT.equals(qName)) {
350:
351: file_state = IN_EXIT;
352: break;
353:
354: } else {
355: String reason = "Unexpected element: " + qName;
356: error(reason);
357: }
358: break;
359:
360: case IN_RESOLVER:
361:
362: // it is switch to hardcoded components
363: // you can smooth;y add new ones by entering them
364:
365: // PLEASE update DTD public ID register it to XML Environment.Provider
366: // Let the DTD is backward compatible
367:
368: if (XML_RULE_COMPONENT.equals(qName)) {
369: enterComponent(XML_RULE_COMPONENT,
370: new XMLMIMEComponent());
371: component.startElement(namespaceURI, localName,
372: qName, atts);
373: }
374:
375: break;
376:
377: case IN_COMPONENT:
378:
379: component.startElement(namespaceURI, localName, qName,
380: atts);
381: break;
382:
383: default:
384:
385: }
386: }
387:
388: private void enterComponent(String name, MIMEComponent component) {
389: this .component = component;
390: componentDelimiter = name;
391:
392: component.setDocumentLocator(getLocator());
393: template[0].rule = component;
394: state = IN_COMPONENT;
395: }
396:
397: public void endElement(String namespaceURI, String localName,
398: String qName) throws SAXException {
399: switch (state) {
400: case IN_FILE:
401: if (FILE.equals(qName)) {
402: state = IN_ROOT;
403: file_state = INIT;
404: }
405: break;
406:
407: case IN_RESOLVER:
408: if (RESOLVER.equals(qName)) {
409: state = IN_FILE;
410: }
411: break;
412:
413: case IN_COMPONENT:
414: component.endElement(namespaceURI, localName, qName);
415: if (componentDelimiter.equals(qName)) {
416: state = IN_RESOLVER;
417: component.setDocumentLocator(null);
418: }
419: break;
420: }
421: }
422:
423: public void characters(char[] data, int offset, int len)
424: throws SAXException {
425: if (state == IN_COMPONENT)
426: component.characters(data, offset, len);
427: }
428: }
429:
430: /**
431: * Represents a resolving process made using a <tt>file</tt> element.
432: * <p>
433: * Responsible for pairing and performing fast check followed by optional
434: * rules and if all matches returning MIME type.
435: */
436: private static class FileElement {
437: FileElement() {
438: }
439:
440: private Type fileCheck = new Type();
441: private String mime = null;
442: private MIMEComponent rule = null;
443:
444: private void setMIME(String mime) {
445: if ("null".equals(mime))
446: return; // NOI18N
447: this .mime = mime;
448: }
449:
450: private String resolve(FileObject file) {
451:
452: try {
453: if (fileCheck.accept(file)) {
454: if (mime == null)
455: return null;
456: if (rule == null)
457: return mime;
458: if (rule.acceptFileObject(file))
459: return mime;
460: }
461: } catch (IOException io) {
462: Logger.getLogger(MIMEResolverImpl.class.getName()).log(
463: Level.INFO, null, io);
464: }
465: return null;
466: }
467:
468: /**
469: * For debug puroses only.
470: */
471: public String toString() {
472: StringBuffer buf = new StringBuffer();
473: buf.append("FileElement(");
474: buf.append(fileCheck).append(' ');
475: buf.append(rule).append(' ');
476: buf.append("Result:").append(mime);
477: return buf.toString();
478: }
479: }
480:
481: /**
482: * Hold data from XML document and performs first stage check according to them.
483: * <p>
484: * The first stage check is resonsible for filtering files according to their
485: * attributes provided by lower layers.
486: * <p>
487: * We could generate hardwired class bytecode on a fly.
488: */
489: private static class Type {
490: Type() {
491: }
492:
493: private String[] exts;
494: private String[] mimes;
495: private String[] fatts;
496: private String[] vals; // contains null or value of attribute at the same index
497: private byte[] magic;
498: private byte[] mask;
499:
500: /**
501: * For debug purposes only.
502: */
503: public String toString() {
504: int i = 0;
505: StringBuffer buf = new StringBuffer();
506:
507: buf.append("fast-check(");
508:
509: if (exts != null) {
510: buf.append("exts:");
511: for (i = 0; i < exts.length; i++)
512: buf.append(exts[i]).append(", ");
513: }
514:
515: if (mimes != null) {
516: buf.append("mimes:");
517: for (i = 0; i < mimes.length; i++)
518: buf.append(mimes[i]).append(", ");
519: }
520:
521: if (fatts != null) {
522: buf.append("file-attributes:");
523: for (i = 0; i < fatts.length; i++)
524: buf.append(fatts[i]).append("='").append(vals[i])
525: .append("', ");
526: }
527:
528: if (magic != null) {
529: buf.append("magic:").append(
530: XMLUtil.toHex(magic, 0, magic.length));
531: }
532:
533: if (mask != null) {
534: buf.append("mask:").append(
535: XMLUtil.toHex(mask, 0, mask.length));
536: }
537:
538: buf.append(')');
539:
540: return buf.toString();
541: }
542:
543: private void addExt(String ext) {
544: exts = Util.addString(exts, ext);
545: }
546:
547: private void addMIME(String mime) {
548: mimes = Util.addString(mimes, mime.toLowerCase());
549: }
550:
551: private void addAttr(String name, String value) {
552: fatts = Util.addString(fatts, name);
553: vals = Util.addString(vals, value);
554: }
555:
556: private boolean setMagic(byte[] magic, byte[] mask) {
557: if (magic == null)
558: return true;
559: if (mask != null && magic.length != mask.length)
560: return false;
561: this .magic = magic;
562: if (mask != null) {
563: this .mask = mask;
564: for (int i = 0; i < mask.length; i++) {
565: this .magic[i] &= mask[i];
566: }
567: }
568: return true;
569: }
570:
571: private boolean accept(FileObject fo) throws IOException {
572: // check for resource extension
573: if (exts != null) {
574: if (fo.getExt() == null)
575: return false;
576: if (!Util.contains(exts, fo.getExt(), CASE_INSENSITIVE))
577: return false;
578: }
579:
580: // check for resource mime type
581:
582: if (mimes != null) {
583: boolean match = false;
584: String s = FileUtil.getMIMEType(fo.getExt()); //!!! how to obtain resource MIME type as classified by lower layers?
585: if (s == null)
586: return false;
587:
588: // RFC2045; remove content type paramaters and ignore case
589: int l = s.indexOf(';');
590: if (l >= 0)
591: s = s.substring(0, l);
592: s = s.toLowerCase();
593:
594: for (int i = mimes.length - 1; i >= 0; i--) {
595: if (s.equals(mimes[i])) {
596: match = true;
597: break;
598: }
599:
600: // RFC3023; allows "+xml" suffix
601: if (mimes[i].length() > 0
602: && mimes[i].charAt(0) == '+'
603: && s.endsWith(mimes[i])) {
604: match = true;
605: break;
606: }
607: }
608: if (!match)
609: return false;
610: }
611:
612: // check for magic
613:
614: if (magic != null) {
615: byte[] header = new byte[magic.length];
616:
617: // System.err.println("FO" + fo);
618:
619: // String m = mask == null ? "" : " mask " + XMLUtil.toHex(mask, 0, mask.length);
620: // System.err.println("Magic test " + XMLUtil.toHex(magic, 0, magic.length) + m);
621:
622: // fetch header
623:
624: InputStream in = null;
625: boolean unexpectedEnd = false;
626: try {
627: in = fo.getInputStream();
628: for (int i = 0; i < magic.length;) {
629: try {
630: int read = in.read(header, i, magic.length
631: - i);
632: if (read < 0)
633: unexpectedEnd = true;
634: i += read;
635: } catch (IOException ex) {
636: unexpectedEnd = true;
637: break;
638: }
639: if (unexpectedEnd)
640: break;
641: }
642: } catch (IOException openex) {
643: unexpectedEnd = true;
644: boolean isBug114976 = false;
645: if (Utilities.isWindows()
646: && fo.canRead()
647: && (openex instanceof FileNotFoundException)) {
648: if (fo.isValid()
649: && fo.getName().toLowerCase().indexOf(
650: "ntuser") != -1) {//NOI18N
651: isBug114976 = true;
652: }
653: }
654:
655: if (fo.canRead() == true && !isBug114976) {
656: throw openex;
657: } else {
658: // #26521 silently do not recognize it
659: }
660: } finally {
661: try {
662: if (in != null)
663: in.close();
664: } catch (IOException ioe) {
665: // already closed
666: }
667: }
668:
669: // System.err.println("Header " + XMLUtil.toHex(header, 0, header.length));
670:
671: // compare it
672:
673: if (unexpectedEnd) {
674: return false;
675: } else {
676: for (int i = 0; i < magic.length; i++) {
677: if (mask != null)
678: header[i] &= mask[i];
679: if (magic[i] != header[i]) {
680: return false;
681: }
682: }
683: }
684: }
685:
686: // check for fileobject attributes
687:
688: if (fatts != null) {
689: for (int i = fatts.length - 1; i >= 0; i--) {
690: Object attr = fo.getAttribute(fatts[i]);
691: if (attr != null) {
692: if (!attr.toString().equals(vals[i])
693: && vals[i] != null)
694: return false;
695: }
696: }
697: }
698:
699: // all templates matched
700: return true;
701: }
702:
703: }
704: }
|