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.xml;
043:
044: import java.io.IOException;
045: import java.beans.PropertyChangeListener;
046: import java.io.BufferedInputStream;
047: import java.io.InputStream;
048: import java.lang.ref.Reference;
049: import java.lang.ref.WeakReference;
050:
051: import org.xml.sax.InputSource;
052: import org.xml.sax.SAXException;
053: import org.w3c.dom.DocumentType;
054:
055: import org.openide.filesystems.FileObject;
056: import org.openide.filesystems.Repository;
057: import org.openide.loaders.*;
058: import org.openide.cookies.InstanceCookie;
059: import org.openide.util.Lookup;
060: import org.openide.util.lookup.*;
061: import org.openide.xml.EntityCatalog;
062: import org.openide.filesystems.FileChangeListener;
063: import org.openide.filesystems.FileRenameEvent;
064: import org.openide.filesystems.FileEvent;
065: import org.openide.filesystems.FileAttributeEvent;
066: import java.net.URL;
067: import java.util.logging.Level;
068: import java.util.logging.Logger;
069: import org.openide.util.Exceptions;
070:
071: /**
072: * Entity resolver which loads entities (typically DTDs) from fixed
073: * locations in the system file system, according to public ID.
074: * <p>
075: * It expects that PUBLIC has at maximum three "//" parts
076: * (standard // vendor // entity name // language). It is basically
077: * converted to <tt>"/xml/entities/{vendor}/{entity_name}"</tt> resource name.
078: * <p>
079: * It also attaches <tt>Environment</tt> according to registrations
080: * at <tt>/xml/lookups/</tt> area. There can be registered:
081: * <tt>Environment.Provider</tt> or deprecated <tt>XMLDataObject.Processor</tt>
082: * and <tt>XMLDataObject.Info</tt> instances.
083: * <p>
084: * All above are core implementation features.
085: *
086: * @author Jaroslav Tulach
087: */
088: public final class FileEntityResolver extends EntityCatalog implements
089: Environment.Provider {
090: private static final String ENTITY_PREFIX = "/xml/entities"; // NOI18N
091: private static final String LOOKUP_PREFIX = "/xml/lookups"; // NOI18N
092:
093: static final Logger ERR = Logger.getLogger(FileEntityResolver.class
094: .getName());
095:
096: /** Constructor
097: */
098: public FileEntityResolver() {
099: }
100:
101: /** Tries to find the entity on system file system.
102: */
103: public InputSource resolveEntity(String publicID, String systemID)
104: throws IOException, SAXException {
105: if (publicID == null) {
106: return null;
107: }
108:
109: String id = convertPublicId(publicID);
110:
111: StringBuffer sb = new StringBuffer(200);
112: sb.append(ENTITY_PREFIX);
113: sb.append(id);
114:
115: FileObject fo = Repository.getDefault().getDefaultFileSystem()
116: .findResource(sb.toString());
117: if (fo != null) {
118:
119: // fill in InputSource instance
120:
121: InputSource in = new InputSource(fo.getInputStream());
122: try {
123: Object myPublicID = fo
124: .getAttribute("hint.originalPublicID"); //NOI18N
125: if (myPublicID instanceof String) {
126: in.setPublicId((String) myPublicID);
127: }
128: URL url = fo.getURL();
129: in.setSystemId(url.toString()); // we get nasty nbfs: instead nbres: but it is enough
130: } catch (IOException ex) {
131: // do no care just no system id
132: }
133: return in;
134: } else {
135: return null;
136: }
137: }
138:
139: /** A method that tries to find the correct lookup for given XMLDataObject.
140: * @return the lookup
141: */
142: public Lookup getEnvironment(DataObject obj) {
143: if (obj instanceof XMLDataObject) {
144: XMLDataObject xml = (XMLDataObject) obj;
145:
146: String id = null;
147: try {
148: DocumentType domDTD = xml.getDocument().getDoctype();
149: if (domDTD != null)
150: id = domDTD.getPublicId();
151: } catch (IOException ex) {
152: Exceptions.printStackTrace(ex);
153: return null;
154: } catch (org.xml.sax.SAXException ex) {
155: Exceptions.printStackTrace(ex);
156: return null;
157: }
158:
159: if (id == null) {
160: return null;
161: }
162:
163: id = convertPublicId(id);
164:
165: return new Lkp(id, xml);
166: } else if (obj instanceof InstanceDataObject) {
167: return getEnvForIDO((InstanceDataObject) obj);
168: }
169: return null;
170: }
171:
172: private Lookup getEnvForIDO(InstanceDataObject ido) {
173: FileEntityResolver.DTDParser parser = new DTDParser(ido
174: .getPrimaryFile());
175: parser.parse();
176: String id = parser.getPublicId();
177: if (id == null)
178: return null;
179: id = convertPublicId(id);
180: return new Lkp(id, ido);
181: }
182:
183: /** A method that extracts a listener from data object.
184: *
185: * @param obj the data object that we are looking for environment of
186: * @param source the obj that provides the environment
187: * @return lookup provided by the obj or null if none has been found
188: */
189: @SuppressWarnings("deprecation")
190: private static Lookup findLookup(DataObject obj, DataObject source) {
191: if (source == null) {
192: return null;
193: }
194:
195: try {
196: InstanceCookie cookie = source
197: .getCookie(InstanceCookie.class);
198:
199: if (cookie != null) {
200: Object inst = cookie.instanceCreate();
201: if (inst instanceof Environment.Provider) {
202: return ((Environment.Provider) inst)
203: .getEnvironment(obj);
204: }
205:
206: if (!(obj instanceof XMLDataObject))
207: return null;
208:
209: if (inst instanceof XMLDataObject.Processor) {
210: // convert provider
211: XMLDataObject.Info info = new XMLDataObject.Info();
212: info.addProcessorClass(inst.getClass());
213: inst = info;
214: }
215:
216: if (inst instanceof XMLDataObject.Info) {
217: return createInfoLookup((XMLDataObject) obj,
218: ((XMLDataObject.Info) inst));
219: }
220:
221: }
222: } catch (IOException ex) {
223: Exceptions.printStackTrace(ex);
224: } catch (ClassNotFoundException ex) {
225: Exceptions.printStackTrace(ex);
226: }
227:
228: return null;
229: }
230:
231: /** Ugly hack to get to openide hidden functionality.
232: */
233: private static java.lang.reflect.Method method;
234:
235: @SuppressWarnings("deprecation")
236: private static Lookup createInfoLookup(XMLDataObject obj,
237: XMLDataObject.Info info) {
238: // well, it is a wormhole, but just for default compatibility
239: synchronized (FileEntityResolver.class) {
240: if (method == null) {
241: try {
242: java.lang.reflect.Method m = XMLDataObject.class
243: .getDeclaredMethod("createInfoLookup",
244: new Class[] { // NOI18N
245: XMLDataObject.class,
246: XMLDataObject.Info.class });
247: m.setAccessible(true);
248: method = m;
249: } catch (Exception ex) {
250: Exceptions.printStackTrace(ex);
251: return null;
252: }
253: }
254: }
255: try {
256: return (Lookup) method.invoke(null, new Object[] { obj,
257: info });
258: } catch (Exception ex) {
259: Exceptions.printStackTrace(ex);
260: return null;
261: }
262: }
263:
264: /** Converts the publicID into filesystem friendly name.
265: * <p>
266: * It expects that PUBLIC has at maximum three "//" parts
267: * (standard // vendor // entity name // language). It is basically
268: * converted to "vendor/entity_name" resource name.
269: *
270: * @see EntityCatalog
271: */
272: @SuppressWarnings("fallthrough")
273: private static String convertPublicId(String publicID) {
274: char[] arr = publicID.toCharArray();
275:
276: int numberofslashes = 0;
277: int state = 0;
278: int write = 0;
279: OUT: for (int i = 0; i < arr.length; i++) {
280: char ch = arr[i];
281:
282: switch (state) {
283: case 0:
284: // initial state
285: if (ch == '+' || ch == '-' || ch == 'I' || ch == 'S'
286: || ch == 'O') {
287: // do not write that char
288: continue;
289: }
290: // switch to regular state
291: state = 1;
292: // fallthru
293: case 1:
294: // regular state expecting any character
295: if (ch == '/') {
296: state = 2;
297: if (++numberofslashes == 3) {
298: // last part of the ID, exit
299: break OUT;
300: }
301: arr[write++] = '/';
302: continue;
303: }
304: break;
305: case 2:
306: // previous character was /
307: if (ch == '/') {
308: // ignore second / and write nothing
309: continue;
310: }
311: state = 1;
312: break;
313: }
314:
315: // write the char into the array
316: if (ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z'
317: || ch >= '0' && ch <= '9') {
318: arr[write++] = ch;
319: } else {
320: arr[write++] = '_';
321: }
322: }
323:
324: return new String(arr, 0, write);
325: }
326:
327: /** Finds a fileobject for given ID.
328: * @param id string id
329: * @param last[0] will be filled with last file object we should listen on
330: * @return file object that should represent it
331: */
332: private static FileObject findObject(String id, FileObject[] last) {
333: StringBuffer sb = new StringBuffer(200);
334: sb.append(LOOKUP_PREFIX);
335: sb.append(id);
336: int len = sb.length();
337: // at least for now
338: sb.append(".instance"); // NOI18N
339:
340: FileObject root = Repository.getDefault()
341: .getDefaultFileSystem().getRoot();
342:
343: String toSearch1 = sb.toString();
344: int indx = searchFolder(root, toSearch1, last);
345: if (indx == -1) {
346: // not possible to find folders
347: return null;
348: }
349:
350: FileObject fo = last[0]
351: .getFileObject(toSearch1.substring(indx));
352:
353: if (fo == null) {
354: // try to find a file with xml extension
355: sb.setLength(len);
356: sb.append(".xml"); // NOI18N
357:
358: fo = last[0].getFileObject(sb.toString().substring(indx));
359: }
360:
361: return fo;
362: }
363:
364: /** Find last folder for resourceName.
365: * @param fo file object to search from
366: * @param resourceName name of file to find
367: * @param last last[0] will be filled with the last found name
368: * @return position of last / if everything has been searched, or -1 if some files are missing
369: */
370: private static int searchFolder(FileObject fo, String resourceName,
371: FileObject[] last) {
372: int pos = 0;
373:
374: for (;;) {
375: int next = resourceName.indexOf('/', pos);
376: if (next == -1) {
377: // end of the search
378: last[0] = fo;
379: return pos;
380: }
381:
382: if (next == pos) {
383: pos++;
384: continue;
385: }
386:
387: FileObject nf = fo.getFileObject(resourceName.substring(
388: pos, next));
389: if (nf == null) {
390: // not found a continuation
391: last[0] = fo;
392: return -1;
393: }
394:
395: // proceed to next one
396: pos = next + 1;
397: fo = nf;
398: }
399: }
400:
401: // internally stops documet parsing when looking for public id
402: private static class StopSaxException extends SAXException {
403: public StopSaxException() {
404: super ("STOP");
405: } //NOI18N
406: }
407:
408: private static final StopSaxException STOP = new StopSaxException();
409:
410: ////////////////////////////////////////////////////////////////////////
411: // DTDParser
412: ////////////////////////////////////////////////////////////////////////
413: /** resolve the PUBLIC item from the xml header of .settings file */
414: private static class DTDParser extends
415: org.xml.sax.helpers.DefaultHandler implements
416: org.xml.sax.ext.LexicalHandler {
417:
418: private String publicId = null;
419: private FileObject src;
420:
421: public DTDParser(FileObject src) {
422: this .src = src;
423: }
424:
425: public String getPublicId() {
426: return publicId;
427: }
428:
429: public void parse() {
430: InputStream in = null;
431: try {
432: org.xml.sax.XMLReader reader = org.openide.xml.XMLUtil
433: .createXMLReader(false, false);
434: reader.setContentHandler(this );
435: reader.setEntityResolver(this );
436: in = new BufferedInputStream(src.getInputStream());
437: InputSource is = new InputSource(in);
438: try {
439: reader.setFeature(
440: "http://xml.org/sax/features/validation",
441: false); //NOI18N
442: } catch (SAXException sex) {
443: ERR
444: .warning("XML parser does not support validation feature."); //NOI18N
445: }
446: try {
447: reader
448: .setProperty(
449: "http://xml.org/sax/properties/lexical-handler",
450: this ); //NOI18N
451: } catch (SAXException sex) {
452: ERR
453: .warning("XML parser does not support lexical-handler feature."); //NOI18N
454: }
455: reader.parse(is);
456: } catch (StopSaxException ex) {
457: ERR.log(Level.FINE, null, ex);
458: } catch (Exception ex) { // SAXException, FileNotFoundException, IOException
459: if ("org.openide.util.lookup.AbstractLookup$ISE"
460: .equals(ex.getClass().getName())) { // NOI18N
461: // this is covered by the FileEntityResolverDeadlock54971Test
462: throw (IllegalStateException) ex;
463: }
464:
465: try {
466: // #25082: do not notify an exception if the file comes
467: // from other filesystem than the system filesystem
468: if (src.getFileSystem() == Repository.getDefault()
469: .getDefaultFileSystem()) {
470: ERR.log(Level.WARNING, "Parsing " + src, ex); // NOI18N
471: }
472: } catch (org.openide.filesystems.FileStateInvalidException fie) {
473: // ignore
474: }
475: } finally {
476: try {
477: if (in != null) {
478: in.close();
479: }
480: } catch (IOException exc) {
481: ERR.log(Level.WARNING, "Closing stream for " + src,
482: exc);
483: }
484: }
485: }
486:
487: @Override
488: public InputSource resolveEntity(String publicId,
489: String systemID) {
490: InputSource ret = new InputSource(new java.io.StringReader(
491: "")); // NOI18N
492: ret.setSystemId("StringReader"); //NOI18N
493: return ret;
494: }
495:
496: public void endDTD() throws org.xml.sax.SAXException {
497: throw STOP;
498: }
499:
500: public void startDTD(String name, String publicId,
501: String systemId) throws org.xml.sax.SAXException {
502: this .publicId = publicId;
503: }
504:
505: public void startEntity(String str)
506: throws org.xml.sax.SAXException {
507: }
508:
509: public void endEntity(String str)
510: throws org.xml.sax.SAXException {
511: }
512:
513: public void comment(char[] values, int param, int param2)
514: throws org.xml.sax.SAXException {
515: }
516:
517: public void startCDATA() throws org.xml.sax.SAXException {
518: }
519:
520: public void endCDATA() throws org.xml.sax.SAXException {
521: }
522:
523: }
524:
525: /** A special lookup associated with id.
526: */
527: private static final class Lkp extends ProxyLookup implements
528: PropertyChangeListener, FileChangeListener {
529: /** converted ID we are associated with */
530: private String id;
531: /** for this data object we initialized this lookup */
532: private Reference<DataObject> xml;
533:
534: /** last file folder we are listening on. Initialized lazily */
535: private volatile FileObject folder;
536: /** a data object that produces values Initialized lazily */
537: private volatile DataObject obj;
538:
539: /** @param id the id to work on */
540: public Lkp(String id, DataObject xml) {
541: super (new Lookup[0]);
542: this .id = id;
543: this .xml = new WeakReference<DataObject>(xml);
544: }
545:
546: /** Check whether all necessary values are updated.
547: */
548: @Override
549: protected void beforeLookup(Template t) {
550: if (ERR.isLoggable(Level.FINE)) {
551: ERR.fine("beforeLookup: " + t.getType() + " for "
552: + getXml()); // NOI18N
553: }
554:
555: if (folder == null && obj == null) {
556: update();
557: }
558: }
559:
560: /** Updates current state of the lookup.
561: */
562: private void update() {
563: if (ERR.isLoggable(Level.FINE))
564: ERR.fine("update: " + id + " for " + getXml()); // NOI18N
565: FileObject[] last = new FileObject[1];
566: FileObject fo = findObject(id, last);
567: if (ERR.isLoggable(Level.FINE))
568: ERR.fine("fo: " + fo + " for " + getXml()); // NOI18N
569: DataObject o = null;
570:
571: if (fo != null) {
572: try {
573: o = DataObject.find(fo);
574: if (ERR.isLoggable(Level.FINE))
575: ERR.fine("object found: " + o + " for "
576: + getXml()); // NOI18N
577: } catch (org.openide.loaders.DataObjectNotFoundException ex) {
578: Exceptions.printStackTrace(ex);
579: }
580: }
581:
582: if (o == obj) {
583: if (ERR.isLoggable(Level.FINE))
584: ERR.fine("same data object" + " for " + getXml()); // NOI18N
585: // the data object is still the same as used to be
586: //
587: Lookup l = findLookup(getXml(), o);
588: if (o != null && l != null) {
589: if (ERR.isLoggable(Level.FINE))
590: ERR.fine("updating lookups" + " for "
591: + getXml()); // NOI18N
592: // just update the lookups
593: setLookups(new Lookup[] { l });
594: if (ERR.isLoggable(Level.FINE))
595: ERR.fine("updating lookups done" + " for "
596: + getXml()); // NOI18N
597: // and exit
598: return;
599: }
600: } else {
601: // data object changed
602: Lookup l = findLookup(getXml(), o);
603:
604: if (o != null && l != null) {
605: if (ERR.isLoggable(Level.FINE))
606: ERR.fine("change the lookup"); // NOI18N
607: // add listener to changes of the data object
608: o
609: .addPropertyChangeListener(org.openide.util.WeakListeners
610: .propertyChange(this , o));
611: // update the lookups
612: setLookups(new Lookup[] { l });
613: if (ERR.isLoggable(Level.FINE))
614: ERR.fine("change in lookup done" + " for "
615: + getXml()); // NOI18N
616: // and exit
617: obj = o;
618: if (ERR.isLoggable(Level.FINE))
619: ERR.fine("data object updated to " + obj
620: + " for " + getXml()); // NOI18N
621: return;
622: } else {
623: obj = o;
624: if (ERR.isLoggable(Level.FINE))
625: ERR.fine("data object updated to " + obj
626: + " for " + getXml()); // NOI18N
627: }
628: }
629:
630: if (ERR.isLoggable(Level.FINE))
631: ERR.fine("delegating to nobody for " + obj + " for "
632: + getXml()); // NOI18N
633: // object is null => there are no lookups
634: setLookups(new Lookup[0]);
635:
636: // and start listening on latest existing folder
637: // if we did not do it yet
638: if (folder != last[0]) {
639: folder = last[0];
640: last[0]
641: .addFileChangeListener(org.openide.filesystems.FileUtil
642: .weakFileChangeListener(this , last[0]));
643: }
644: }
645:
646: /** Fired when a file is deleted.
647: * @param fe the event describing context where action has taken place
648: */
649: public void fileDeleted(FileEvent fe) {
650: update();
651: }
652:
653: /** Fired when a new folder is created. This action can only be
654: * listened to in folders containing the created folder up to the root of
655: * file system.
656: *
657: * @param fe the event describing context where action has taken place
658: */
659: public void fileFolderCreated(FileEvent fe) {
660: update();
661: }
662:
663: /** Fired when a new file is created. This action can only be
664: * listened in folders containing the created file up to the root of
665: * file system.
666: *
667: * @param fe the event describing context where action has taken place
668: */
669: public void fileDataCreated(FileEvent fe) {
670: update();
671: }
672:
673: /** Fired when a file attribute is changed.
674: * @param fe the event describing context where action has taken place,
675: * the name of attribute and the old and new values.
676: */
677: public void fileAttributeChanged(FileAttributeEvent fe) {
678: }
679:
680: public void propertyChange(java.beans.PropertyChangeEvent ev) {
681: String name = ev.getPropertyName();
682:
683: if (DataObject.PROP_COOKIE.equals(name)
684: || DataObject.PROP_NAME.equals(name)
685: || DataObject.PROP_VALID.equals(name)
686: || DataObject.PROP_PRIMARY_FILE.equals(name)) {
687: update();
688: }
689: }
690:
691: /** Fired when a file is renamed.
692: * @param fe the event describing context where action has taken place
693: * and the original name and extension.
694: */
695: public void fileRenamed(FileRenameEvent fe) {
696: update();
697: }
698:
699: /** Fired when a file is changed.
700: * @param fe the event describing context where action has taken place
701: */
702: public void fileChanged(FileEvent fe) {
703: }
704:
705: private DataObject getXml() {
706: return xml.get();
707: }
708:
709: }
710: }
|