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.apache.tools.ant.module.xml;
043:
044: import java.beans.PropertyChangeEvent;
045: import java.beans.PropertyChangeListener;
046: import java.io.File;
047: import java.io.FileNotFoundException;
048: import java.io.IOException;
049: import java.io.InputStream;
050: import java.io.StringReader;
051: import java.lang.ref.Reference;
052: import java.lang.ref.WeakReference;
053: import javax.swing.event.ChangeListener;
054: import javax.swing.event.DocumentEvent;
055: import javax.swing.event.DocumentListener;
056: import javax.swing.text.BadLocationException;
057: import javax.swing.text.StyledDocument;
058: import javax.xml.parsers.DocumentBuilder;
059: import javax.xml.parsers.DocumentBuilderFactory;
060: import org.apache.tools.ant.module.AntModule;
061: import org.apache.tools.ant.module.api.AntProjectCookie;
062: import org.openide.ErrorManager;
063: import org.openide.cookies.EditorCookie;
064: import org.openide.filesystems.FileAttributeEvent;
065: import org.openide.filesystems.FileEvent;
066: import org.openide.filesystems.FileObject;
067: import org.openide.filesystems.FileRenameEvent;
068: import org.openide.filesystems.FileStateInvalidException;
069: import org.openide.filesystems.FileUtil;
070: import org.openide.loaders.DataObject;
071: import org.openide.loaders.DataObjectNotFoundException;
072: import org.openide.util.ChangeSupport;
073: import org.openide.util.RequestProcessor;
074: import org.openide.util.WeakListeners;
075: import org.w3c.dom.Document;
076: import org.w3c.dom.Element;
077: import org.xml.sax.ErrorHandler;
078: import org.xml.sax.InputSource;
079: import org.xml.sax.SAXException;
080: import org.xml.sax.SAXParseException;
081:
082: public class AntProjectSupport implements AntProjectCookie.ParseStatus,
083: DocumentListener,
084: /*FileChangeListener,*/PropertyChangeListener {
085:
086: private FileObject fo;
087:
088: private Document projDoc = null; // [PENDING] SoftReference
089: private Throwable exception = null;
090: private boolean parsed = false;
091: private Reference<StyledDocument> styledDocRef = null;
092: private Object parseLock; // see init()
093:
094: private final ChangeSupport cs = new ChangeSupport(this );
095: private EditorCookie.Observable editor = null;
096:
097: private DocumentBuilder documentBuilder;
098:
099: // milliseconds of quiet time after a textual document change after which
100: // changes will be fired and the XML may be reparsed
101: private static final int REPARSE_DELAY = 3000;
102:
103: public AntProjectSupport(FileObject fo) {
104: this .fo = fo;
105: parseLock = new Object();
106: rp = new RequestProcessor("AntProjectSupport[" + fo + "]"); // NOI18N
107: }
108:
109: private synchronized EditorCookie.Observable getEditor() {
110: FileObject file = getFileObject();
111: if (file == null)
112: return null;
113: if (editor == null) {
114: try {
115: editor = DataObject.find(file).getCookie(
116: EditorCookie.Observable.class);
117: if (editor != null) {
118: editor.addPropertyChangeListener(WeakListeners
119: .propertyChange(this , editor));
120: }
121: } catch (DataObjectNotFoundException donfe) {
122: AntModule.err.notify(ErrorManager.INFORMATIONAL, donfe);
123: }
124: }
125: return editor;
126: }
127:
128: public File getFile() {
129: FileObject file = getFileObject();
130: if (file != null) {
131: return FileUtil.toFile(file);
132: } else {
133: return null;
134: }
135: }
136:
137: public FileObject getFileObject() {
138: if (fo != null && !fo.isValid()) { // #11065
139: return null;
140: }
141: return fo;
142: }
143:
144: public void setFile(File f) { // #11979
145: fo = FileUtil.toFileObject(f);
146: invalidate();
147: }
148:
149: public void setFileObject(FileObject fo) { // #11979
150: this .fo = fo;
151: invalidate();
152: }
153:
154: public boolean isParsed() {
155: return parsed;
156: }
157:
158: public Document getDocument() {
159: synchronized (parseLock) {
160: if (!parsed) {
161: parseDocument();
162: }
163: return projDoc != null ? (Document) projDoc
164: ./* #111862 */cloneNode(true) : null;
165: }
166: }
167:
168: public Throwable getParseException() {
169: synchronized (parseLock) {
170: if (!parsed) {
171: parseDocument();
172: }
173: return exception;
174: }
175: }
176:
177: /**
178: * Make a DocumentBuilder object for use in this support.
179: * Thread-safe, but of course the result is not.
180: * @throws Exception for various reasons of configuration
181: */
182: private static synchronized DocumentBuilder createDocumentBuilder()
183: throws Exception {
184: //DocumentBuilderFactory factory = (DocumentBuilderFactory)Class.forName(XERCES_DOCUMENT_BUILDER_FACTORY).newInstance();
185: DocumentBuilderFactory factory = DocumentBuilderFactory
186: .newInstance();
187: factory.setNamespaceAware(true);
188: DocumentBuilder documentBuilder = factory.newDocumentBuilder();
189: documentBuilder.setErrorHandler(ErrHandler.DEFAULT);
190: return documentBuilder;
191: }
192:
193: /**
194: * XML parser error handler; passes on all errors.
195: */
196: private static final class ErrHandler implements ErrorHandler {
197: static final ErrorHandler DEFAULT = new ErrHandler();
198:
199: private ErrHandler() {
200: }
201:
202: public void error(SAXParseException exception)
203: throws SAXException {
204: throw exception;
205: }
206:
207: public void fatalError(SAXParseException exception)
208: throws SAXException {
209: throw exception;
210: }
211:
212: public void warning(SAXParseException exception)
213: throws SAXException {
214: throw exception;
215: }
216: }
217:
218: /**
219: * Utility method to get a properly configured XML input source for a script.
220: */
221: public static InputSource createInputSource(final FileObject fo,
222: final StyledDocument document) throws IOException {
223: if (fo != null) {
224: DataObject d = DataObject.find(fo);
225: if (!d.isModified()) {
226: // #58194: no need to parse the live document.
227: try {
228: return new InputSource(fo.getURL().toExternalForm());
229: } catch (FileStateInvalidException e) {
230: assert false : e;
231: }
232: }
233: }
234: final String[] contents = new String[1];
235: document.render(new Runnable() {
236: public void run() {
237: try {
238: contents[0] = document.getText(0, document
239: .getLength());
240: } catch (BadLocationException e) {
241: throw new AssertionError(e);
242: }
243: }
244: });
245: InputSource in = new InputSource(new StringReader(contents[0]));
246: if (fo != null) { // #10348
247: try {
248: in.setSystemId(fo.getURL().toExternalForm());
249: } catch (FileStateInvalidException e) {
250: assert false : e;
251: }
252: // [PENDING] Ant's ProjectHelper has an elaborate set of work-
253: // arounds for inconsistent parser behavior, e.g. file:foo.xml
254: // works in Ant but not with Xerces parser. You must use just foo.xml
255: // as the system ID. If necessary, Ant's algorithm could be copied
256: // here to make the behavior match perfectly, but it ought not be necessary.
257: }
258: return in;
259: }
260:
261: private void parseDocument() {
262: assert Thread.holdsLock(parseLock); // so it is OK to use documentBuilder
263: FileObject file = getFileObject();
264: AntModule.err
265: .log("AntProjectSupport.parseDocument: fo=" + file);
266: try {
267: if (documentBuilder == null) {
268: documentBuilder = createDocumentBuilder();
269: }
270: EditorCookie ed = getEditor();
271: Document doc;
272: if (ed != null) {
273: final StyledDocument document = ed.openDocument();
274: // add only one Listener (listeners for doc are hold in a List!)
275: if ((styledDocRef != null && styledDocRef.get() != document)
276: || styledDocRef == null) {
277: document.addDocumentListener(this );
278: styledDocRef = new WeakReference<StyledDocument>(
279: document);
280: }
281: InputSource in = createInputSource(file, document);
282: doc = documentBuilder.parse(in);
283: } else if (file != null) {
284: InputStream is = file.getInputStream();
285: try {
286: InputSource in = new InputSource(is);
287: try {
288: in.setSystemId(file.getURL().toExternalForm());
289: } catch (FileStateInvalidException e) {
290: assert false : e;
291: }
292: doc = documentBuilder.parse(is);
293: } finally {
294: is.close();
295: }
296: } else {
297: exception = new FileNotFoundException(
298: "Ant script probably deleted"); // NOI18N
299: return;
300: }
301: projDoc = doc;
302: exception = null;
303: } catch (Exception e) {
304: // leave projDoc the way it is...
305: exception = e;
306: if (!(exception instanceof SAXParseException)) {
307: AntModule.err.annotate(exception, ErrorManager.UNKNOWN,
308: "Strange parse error in " + this , null, null,
309: null); // NOI18N
310: AntModule.err.notify(ErrorManager.INFORMATIONAL,
311: exception);
312: }
313: }
314: fireChangeEvent(false);
315: parsed = true;
316: }
317:
318: public Element getProjectElement() {
319: Document doc = getDocument();
320: if (doc != null) {
321: return doc.getDocumentElement();
322: } else {
323: return null;
324: }
325: }
326:
327: @Override
328: public boolean equals(Object o) {
329: if (!(o instanceof AntProjectSupport))
330: return false;
331: AntProjectSupport other = (AntProjectSupport) o;
332: if (fo != null) {
333: return fo.equals(other.fo);
334: } else {
335: return false;
336: }
337: }
338:
339: @Override
340: public int hashCode() {
341: return 27825 ^ (fo != null ? fo.hashCode() : 0);
342: }
343:
344: @Override
345: public String toString() {
346: FileObject file = getFileObject();
347: if (file != null) {
348: return file.toString();
349: } else {
350: return "<missing Ant script>"; // NOI18N
351: }
352: }
353:
354: public void addChangeListener(ChangeListener l) {
355: cs.addChangeListener(l);
356: }
357:
358: public void removeChangeListener(ChangeListener l) {
359: cs.removeChangeListener(l);
360: }
361:
362: private final RequestProcessor rp;
363: private RequestProcessor.Task task = null;
364:
365: protected void fireChangeEvent(boolean delay) {
366: AntModule.err
367: .log("AntProjectSupport.fireChangeEvent: fo=" + fo);
368: ChangeFirer f = new ChangeFirer();
369: synchronized (this ) {
370: if (task == null) {
371: task = rp.post(f, delay ? REPARSE_DELAY : 0);
372: } else if (!delay) {
373: task.schedule(0);
374: }
375: }
376: }
377:
378: private final class ChangeFirer implements Runnable {
379: public ChangeFirer() {
380: }
381:
382: public void run() {
383: AntModule.err.log("AntProjectSupport.ChangeFirer.run");
384: synchronized (AntProjectSupport.this ) {
385: if (task == null) {
386: return;
387: }
388: task = null;
389: }
390: cs.fireChange();
391: }
392: }
393:
394: public void removeUpdate(DocumentEvent ev) {
395: invalidate();
396: }
397:
398: public void changedUpdate(DocumentEvent ev) {
399: // Not to worry, just text attributes or something...
400: }
401:
402: public void insertUpdate(DocumentEvent ev) {
403: invalidate();
404: }
405:
406: // Called when editor support changes state: #11616
407: public void propertyChange(PropertyChangeEvent e) {
408: if (EditorCookie.Observable.PROP_DOCUMENT.equals(e
409: .getPropertyName())) {
410: invalidate();
411: }
412: }
413:
414: public void fileDeleted(FileEvent p1) {
415: // Hmm, not our problem.
416: }
417:
418: public void fileDataCreated(FileEvent p1) {
419: // ignore
420: }
421:
422: public void fileFolderCreated(FileEvent p1) {
423: // ignore
424: }
425:
426: public void fileRenamed(FileRenameEvent p1) {
427: // ignore
428: }
429:
430: public void fileAttributeChanged(FileAttributeEvent p1) {
431: // ignore
432: }
433:
434: public void fileChanged(FileEvent p1) {
435: invalidate();
436: }
437:
438: protected final void invalidate() {
439: AntModule.err.log("AntProjectSupport.invalidate: fo=" + fo);
440: parsed = false;
441: fireChangeEvent(true);
442: }
443:
444: }
|