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.spi.project.support.ant;
043:
044: import java.io.BufferedInputStream;
045: import java.io.BufferedOutputStream;
046: import java.io.ByteArrayInputStream;
047: import java.io.ByteArrayOutputStream;
048: import java.io.File;
049: import java.io.IOException;
050: import java.io.InputStream;
051: import java.io.OutputStream;
052: import java.net.URI;
053: import java.net.URL;
054: import java.util.Collection;
055: import java.util.HashMap;
056: import java.util.Map;
057: import java.util.Properties;
058: import java.util.zip.CRC32;
059: import java.util.zip.Checksum;
060: import javax.xml.transform.Transformer;
061: import javax.xml.transform.TransformerException;
062: import javax.xml.transform.TransformerFactory;
063: import javax.xml.transform.stream.StreamResult;
064: import javax.xml.transform.stream.StreamSource;
065: import org.netbeans.api.project.ProjectManager;
066: import org.netbeans.api.project.ant.AntBuildExtender;
067: import org.netbeans.api.project.ant.AntBuildExtender.Extension;
068: import org.netbeans.modules.project.ant.AntBuildExtenderAccessor;
069: import org.netbeans.modules.project.ant.UserQuestionHandler;
070: import org.openide.ErrorManager;
071: import org.openide.filesystems.FileLock;
072: import org.openide.filesystems.FileObject;
073: import org.openide.filesystems.FileSystem;
074: import org.openide.filesystems.FileUtil;
075: import org.openide.util.Exceptions;
076: import org.openide.util.Mutex;
077: import org.openide.util.MutexException;
078: import org.openide.util.NbBundle;
079: import org.openide.util.UserQuestionException;
080: import org.openide.util.Utilities;
081: import org.openide.xml.XMLUtil;
082: import org.w3c.dom.Attr;
083: import org.w3c.dom.Document;
084: import org.w3c.dom.Element;
085: import org.w3c.dom.Node;
086: import org.w3c.dom.NodeList;
087: import org.w3c.dom.Text;
088: import org.xml.sax.InputSource;
089: import org.xml.sax.SAXException;
090:
091: /**
092: * Helps a project type (re-)generate, and manage the state and versioning of,
093: * <code>build.xml</code> and <code>build-impl.xml</code>.
094: * @author Jesse Glick
095: */
096: public final class GeneratedFilesHelper {
097:
098: /**
099: * Relative path from project directory to the user build script,
100: * <code>build.xml</code>.
101: */
102: public static final String BUILD_XML_PATH = "build.xml"; // NOI18N
103:
104: /**
105: * Relative path from project directory to the implementation build script,
106: * <code>build-impl.xml</code>.
107: */
108: public static final String BUILD_IMPL_XML_PATH = "nbproject/build-impl.xml"; // NOI18N
109:
110: /**
111: * Path to file storing information about generated files.
112: * It should be kept in version control, since it applies equally after a fresh
113: * project checkout; it does not apply to running Ant, so should not be in
114: * <code>project.properties</code>; and it includes a CRC of <code>project.xml</code>
115: * so it cannot be in that file either. It could be stored in some special
116: * comment at the end of the build script (e.g.) but many users would just
117: * compulsively delete it in this case since it looks weird.
118: */
119: static final String GENFILES_PROPERTIES_PATH = "nbproject/genfiles.properties"; // NOI18N
120:
121: /** Suffix (after script path) of {@link #GENFILES_PROPERTIES_PATH} key for <code>project.xml</code> CRC. */
122: private static final String KEY_SUFFIX_DATA_CRC = ".data.CRC32"; // NOI18N
123:
124: /** Suffix (after script path) of {@link #GENFILES_PROPERTIES_PATH} key for stylesheet CRC. */
125: private static final String KEY_SUFFIX_STYLESHEET_CRC = ".stylesheet.CRC32"; // NOI18N
126:
127: /** Suffix (after script path) of {@link #GENFILES_PROPERTIES_PATH} key for CRC of the script itself. */
128: private static final String KEY_SUFFIX_SCRIPT_CRC = ".script.CRC32"; // NOI18N
129:
130: /**
131: * A build script is missing from disk.
132: * This is mutually exclusive with the other flags.
133: * @see #getBuildScriptState
134: */
135: public static final int FLAG_MISSING = 2 << 0;
136:
137: /**
138: * A build script has been modified since last generated by
139: * {@link #generateBuildScriptFromStylesheet}.
140: * <p class="nonnormative">
141: * Probably this means it was edited by the user.
142: * </p>
143: * @see #getBuildScriptState
144: */
145: public static final int FLAG_MODIFIED = 2 << 1;
146:
147: /**
148: * A build script was generated from an older version of <code>project.xml</code>.
149: * It was last generated using a different version of <code>project.xml</code>,
150: * so using the current <code>project.xml</code> might produce a different
151: * result.
152: * <p class="nonnormative">
153: * This is quite likely in the case of
154: * <code>build.xml</code>; in the case of <code>build-impl.xml</code>, it
155: * probably means that the user edited <code>project.xml</code> manually,
156: * since if that were modified from {@link AntProjectHelper} methods and
157: * the project were saved, the script would have been regenerated
158: * already.
159: * </p>
160: * @see #getBuildScriptState
161: */
162: public static final int FLAG_OLD_PROJECT_XML = 2 << 2;
163:
164: /**
165: * A build script was generated from an older version of a stylesheet.
166: * It was last generated using a different (probably older) version of the
167: * XSLT stylesheet, so using the current stylesheet might produce a different
168: * result.
169: * <p class="nonnormative">
170: * Probably this means the project type
171: * provider module has been upgraded since the project was last saved (in
172: * the case of <code>build-impl.xml</code>) or created (in the case of
173: * <code>build.xml</code>).
174: * </p>
175: * @see #getBuildScriptState
176: */
177: public static final int FLAG_OLD_STYLESHEET = 2 << 3;
178:
179: /**
180: * The build script exists, but nothing else is known about it.
181: * This flag is mutually exclusive with {@link #FLAG_MISSING} but
182: * when set also sets {@link #FLAG_MODIFIED}, {@link #FLAG_OLD_STYLESHEET},
183: * and {@link #FLAG_OLD_PROJECT_XML} - since it is not known whether these
184: * conditions might obtain, it is safest to assume they do.
185: * <p class="nonnormative">
186: * Probably this means that <code>nbproject/genfiles.properties</code> was
187: * deleted by the user.
188: * </p>
189: * @see #getBuildScriptState
190: */
191: public static final int FLAG_UNKNOWN = 2 << 4;
192:
193: /** Associated project helper, or null if using only a directory. */
194: private final AntProjectHelper h;
195:
196: /** Project directory. */
197: private final FileObject dir;
198:
199: private AntBuildExtender extender;
200:
201: /**
202: * Create a helper based on the supplied project helper handle.
203: * @param h an Ant-based project helper supplied to the project type provider
204: */
205: public GeneratedFilesHelper(AntProjectHelper h) {
206: this .h = h;
207: dir = h.getProjectDirectory();
208: }
209:
210: /**
211: * Create a helper based on the supplied project helper handle. The created
212: * instance is capable of extending the generated files with 3rd party content.
213: * @param h an Ant-based project helper supplied to the project type provider
214: * @param ex build extensibility support
215: * @since org.netbeans.modules.project.ant 1.16
216: *
217: */
218: public GeneratedFilesHelper(AntProjectHelper h, AntBuildExtender ex) {
219: this (h);
220: extender = ex;
221: }
222:
223: /**
224: * Create a helper based only on a project directory.
225: * This can be used to perform the basic refresh functionality
226: * without being the owner of the project.
227: * It is only intended for use from the offline Ant task to
228: * refresh a project, and similar special situations.
229: * For normal circumstances please use only
230: * {@link GeneratedFilesHelper#GeneratedFilesHelper(AntProjectHelper)}.
231: * @param d the project directory
232: * @throws IllegalArgumentException if the supplied directory has no <code>project.xml</code>
233: */
234: public GeneratedFilesHelper(FileObject d) {
235: if (d == null
236: || !d.isFolder()
237: || d.getFileObject(AntProjectHelper.PROJECT_XML_PATH) == null) {
238: throw new IllegalArgumentException(
239: "Does not look like an Ant-based project: " + d); // NOI18N
240: }
241: h = null;
242: dir = d;
243: }
244:
245: /**
246: * Create <code>build.xml</code> or <code>nbproject/build-impl.xml</code>
247: * from <code>project.xml</code> plus a supplied XSLT stylesheet.
248: * This is the recommended way to create the build scripts from
249: * project metadata.
250: * <p class="nonnormative">
251: * You may wish to first check {@link #getBuildScriptState} to decide whether
252: * you really want to overwrite an existing build script. Typically if you
253: * find {@link #FLAG_MODIFIED} you should not overwrite it; or ask for confirmation
254: * first; or make a backup. Of course if you find neither of {@link #FLAG_OLD_STYLESHEET}
255: * nor {@link #FLAG_OLD_PROJECT_XML} then there is no reason to overwrite the
256: * script to begin with.
257: * </p>
258: * <p>
259: * Acquires write access.
260: * </p>
261: * @param path a project-relative file path such as {@link #BUILD_XML_PATH} or {@link #BUILD_IMPL_XML_PATH}
262: * @param stylesheet a URL to an XSLT stylesheet accepting <code>project.xml</code>
263: * as input and producing the build script as output
264: * @throws IOException if transforming or writing the output failed
265: * @throws IllegalStateException if the project was modified (and not being saved)
266: */
267: public void generateBuildScriptFromStylesheet(final String path,
268: final URL stylesheet) throws IOException,
269: IllegalStateException {
270: if (path == null) {
271: throw new IllegalArgumentException("Null path"); // NOI18N
272: }
273: if (stylesheet == null) {
274: throw new IllegalArgumentException("Null stylesheet"); // NOI18N
275: }
276: try {
277: ProjectManager.mutex().writeAccess(
278: new Mutex.ExceptionAction<Void>() {
279: public Void run() throws IOException {
280: if (h != null && h.isProjectXmlModified()) {
281: throw new IllegalStateException(
282: "Cannot generate build scripts from a modified project"); // NOI18N
283: }
284: // Need to use an atomic action since otherwise creating new build scripts might
285: // cause them to not be recognized as Ant scripts.
286: dir.getFileSystem().runAtomicAction(
287: new FileSystem.AtomicAction() {
288: public void run()
289: throws IOException {
290: FileObject projectXml = dir
291: .getFileObject(AntProjectHelper.PROJECT_XML_PATH);
292: final FileObject buildScriptXml = FileUtil
293: .createData(dir,
294: path);
295: byte[] projectXmlData;
296: InputStream is = projectXml
297: .getInputStream();
298: try {
299: projectXmlData = load(is);
300: } finally {
301: is.close();
302: }
303: byte[] stylesheetData;
304: is = stylesheet
305: .openStream();
306: try {
307: stylesheetData = load(is);
308: } finally {
309: is.close();
310: }
311: final byte[] resultData;
312: TransformerFactory tf = TransformerFactory
313: .newInstance();
314: try {
315: StreamSource stylesheetSource = new StreamSource(
316: new ByteArrayInputStream(
317: stylesheetData),
318: stylesheet
319: .toExternalForm());
320: Transformer t = tf
321: .newTransformer(stylesheetSource);
322: File projectXmlF = FileUtil
323: .toFile(projectXml);
324: assert projectXmlF != null;
325: StreamSource projectXmlSource = new StreamSource(
326: new ByteArrayInputStream(
327: projectXmlData),
328: projectXmlF
329: .toURI()
330: .toString());
331: ByteArrayOutputStream result = new ByteArrayOutputStream();
332: t
333: .transform(
334: projectXmlSource,
335: new StreamResult(
336: result));
337: if (BUILD_IMPL_XML_PATH
338: .equals(path)) {
339: resultData = applyBuildExtensions(
340: result
341: .toByteArray(),
342: extender);
343: } else {
344: resultData = result
345: .toByteArray();
346: }
347: } catch (TransformerException e) {
348: throw (IOException) new IOException(
349: e.toString())
350: .initCause(e);
351: }
352: // Update genfiles.properties too.
353: final EditableProperties p = new EditableProperties(
354: true);
355: FileObject genfiles = dir
356: .getFileObject(GENFILES_PROPERTIES_PATH);
357: if (genfiles != null
358: && genfiles
359: .isVirtual()) {
360: // #55164: deleted from CVS?
361: genfiles = null;
362: }
363: if (genfiles != null) {
364: is = genfiles
365: .getInputStream();
366: try {
367: p.load(is);
368: } finally {
369: is.close();
370: }
371: }
372: p
373: .setProperty(
374: path
375: + KEY_SUFFIX_DATA_CRC,
376: getCrc32(
377: new ByteArrayInputStream(
378: projectXmlData),
379: projectXml));
380: if (genfiles == null) {
381: // New file, set a comment on it. XXX this puts comment in middle if write build-impl.xml before build.xml
382: p
383: .setComment(
384: path
385: + KEY_SUFFIX_DATA_CRC,
386: new String[] {
387: "# "
388: + NbBundle
389: .getMessage(
390: GeneratedFilesHelper.class,
391: "COMMENT_genfiles.properties_1"), // NOI18N
392: "# "
393: + NbBundle
394: .getMessage(
395: GeneratedFilesHelper.class,
396: "COMMENT_genfiles.properties_2"), // NOI18N
397: },
398: false);
399: }
400: p
401: .setProperty(
402: path
403: + KEY_SUFFIX_STYLESHEET_CRC,
404: getCrc32(
405: new ByteArrayInputStream(
406: stylesheetData),
407: stylesheet));
408: p
409: .setProperty(
410: path
411: + KEY_SUFFIX_SCRIPT_CRC,
412: computeCrc32(new ByteArrayInputStream(
413: resultData)));
414: if (genfiles == null) {
415: genfiles = FileUtil
416: .createData(
417: dir,
418: GENFILES_PROPERTIES_PATH);
419: }
420: final FileObject _genfiles = genfiles;
421: // You get the Spaghetti Code Award if you can follow the control flow in this method!
422: // Now is the time when you wish Java implemented call/cc.
423: // If you didn't understand that last comment, you don't get the Spaghetti Code Award.
424: final FileSystem.AtomicAction body = new FileSystem.AtomicAction() {
425: public void run()
426: throws IOException {
427: // Try to acquire both locks together, since we need them both written.
428: FileLock lock1 = buildScriptXml
429: .lock();
430: try {
431: FileLock lock2 = _genfiles
432: .lock();
433: try {
434: OutputStream os1 = new EolFilterOutputStream(
435: buildScriptXml
436: .getOutputStream(lock1));
437: try {
438: OutputStream os2 = _genfiles
439: .getOutputStream(lock2);
440: try {
441: os1
442: .write(resultData);
443: p
444: .store(os2);
445: } finally {
446: os2
447: .close();
448: }
449: } finally {
450: os1
451: .close();
452: }
453: } finally {
454: lock2
455: .releaseLock();
456: }
457: } finally {
458: lock1
459: .releaseLock();
460: }
461: }
462: };
463: try {
464: body.run();
465: } catch (UserQuestionException uqe) {
466: // #57480: need to regenerate build-impl.xml, really.
467: UserQuestionHandler
468: .handle(
469: uqe,
470: new UserQuestionHandler.Callback() {
471: public void accepted() {
472: // Try again.
473: try {
474: body
475: .run();
476: } catch (UserQuestionException uqe2) {
477: // Need to try one more time - may have locked bSX but not yet gf.
478: UserQuestionHandler
479: .handle(
480: uqe2,
481: new UserQuestionHandler.Callback() {
482: public void accepted() {
483: try {
484: body
485: .run();
486: } catch (IOException e) {
487: ErrorManager
488: .getDefault()
489: .notify(
490: e);
491: }
492: }
493:
494: public void denied() {
495: }
496:
497: public void error(
498: IOException e) {
499: ErrorManager
500: .getDefault()
501: .notify(
502: e);
503: }
504: });
505: } catch (IOException e) {
506: // Oh well.
507: ErrorManager
508: .getDefault()
509: .notify(
510: e);
511: }
512: }
513:
514: public void denied() {
515: // OK, skip it.
516: }
517:
518: public void error(
519: IOException e) {
520: ErrorManager
521: .getDefault()
522: .notify(
523: e);
524: // Never mind.
525: }
526: });
527: }
528: }
529: });
530: return null;
531: }
532: });
533: } catch (MutexException e) {
534: throw (IOException) e.getException();
535: }
536: }
537:
538: private byte[] applyBuildExtensions(byte[] resultData,
539: AntBuildExtender ext) {
540: if (ext == null) {
541: return resultData;
542: }
543: try {
544: ByteArrayInputStream in2 = new ByteArrayInputStream(
545: resultData);
546: InputSource is = new InputSource(in2);
547:
548: Document doc = XMLUtil.parse(is, false, true, null, null);
549: Element el = doc.getDocumentElement();
550: Node firstSubnode = el.getFirstChild();
551: //TODO check if first one is text and use it as indentation..
552: for (Extension extension : AntBuildExtenderAccessor.DEFAULT
553: .getExtensions(ext)) {
554: Text after = doc.createTextNode("\n");
555: el.insertBefore(after, firstSubnode);
556: Element imp = createImportElement(
557: AntBuildExtenderAccessor.DEFAULT
558: .getPath(extension), doc);
559: el.insertBefore(imp, after);
560: Text before = doc.createTextNode(" ");
561: el.insertBefore(before, imp);
562: firstSubnode = before;
563: NodeList nl = el.getElementsByTagName("target"); //NOI18N
564: Map<String, Collection<String>> deps = AntBuildExtenderAccessor.DEFAULT
565: .getDependencies(extension);
566: for (String targetName : deps.keySet()) {
567: Element targetEl = null;
568: for (int i = 0; i < nl.getLength(); i++) {
569: Element elem = (Element) nl.item(i);
570: String at = elem.getAttribute("name"); //NOI18N
571: if (at != null && at.equals(targetName)) {
572: targetEl = elem;
573: break;
574: }
575: }
576: // System.out.println("target name=" + targetName);
577: // System.out.println("target elem=" + targetEl);
578: if (targetEl != null) {
579: Attr depend = targetEl
580: .getAttributeNode("depends"); //NOI18N
581: if (depend == null) {
582: depend = doc.createAttribute("depends"); //NOI18N
583: depend.setValue("");
584: targetEl.setAttributeNode(depend);
585: }
586: String oldVal = depend.getValue();
587: for (String targ : deps.get(targetName)) {
588: oldVal = oldVal + "," + targ; //NOI18N
589: }
590: if (oldVal.startsWith(",")) { //NOI18N
591: oldVal = oldVal.substring(1);
592: }
593: depend.setValue(oldVal);
594: } else {
595: //TODO log??
596: }
597: }
598: }
599:
600: ByteArrayOutputStream out = new ByteArrayOutputStream();
601: XMLUtil.write(doc, out, "UTF-8"); //NOI18N
602: return out.toByteArray();
603: } catch (IOException ex) {
604: ex.printStackTrace();
605: Exceptions.printStackTrace(ex);
606: return resultData;
607: } catch (SAXException ex) {
608: ex.printStackTrace();
609: Exceptions.printStackTrace(ex);
610: return resultData;
611: }
612: }
613:
614: private Element createImportElement(String path, Document doc) {
615: Element el = doc.createElement("import"); //NOI18N
616: el.setAttribute("file", path);
617: return el;
618: }
619:
620: /**
621: * Load data from a stream into a buffer.
622: */
623: private static byte[] load(InputStream is) throws IOException {
624: int size = Math.max(1024, is.available()); // #46235
625: ByteArrayOutputStream baos = new ByteArrayOutputStream(size);
626: byte[] buf = new byte[size];
627: int read;
628: while ((read = is.read(buf)) != -1) {
629: baos.write(buf, 0, read);
630: }
631: return baos.toByteArray();
632: }
633:
634: /**
635: * Find what state a build script is in.
636: * This may be used by a project type provider to decide whether to create
637: * or overwrite it, and whether to produce a backup in the latter case.
638: * Various abnormal conditions are detected:
639: * {@link #FLAG_MISSING}, {@link #FLAG_MODIFIED}, {@link #FLAG_OLD_PROJECT_XML},
640: * {@link #FLAG_OLD_STYLESHEET}, and {@link #FLAG_UNKNOWN}.
641: * <p class="nonnormative">
642: * Currently {@link #FLAG_MODIFIED}, {@link #FLAG_OLD_STYLESHEET}, and
643: * {@link #FLAG_OLD_PROJECT_XML} are detected by computing a CRC-32
644: * of the script when it is created, as well as the CRC-32s of the
645: * stylesheet and <code>project.xml</code>. These CRCs are stored
646: * in a special file <code>nbproject/genfiles.properties</code>.
647: * The CRCs are based on the textual
648: * contents of the files (so even changed to whitespace etc. are considered
649: * changes), but are independent of platform newline conventions (since e.g.
650: * CVS will by default replace \n with \r\n when checking out on Windows).
651: * Changes to external files included into <code>project.xml</code> or the
652: * stylesheet (e.g. using XSLT's import facility) are <em>not</em> detected.
653: * </p>
654: * <p>
655: * If there is some kind of I/O error reading any files, {@link #FLAG_UNKNOWN}
656: * is returned (in conjunction with {@link #FLAG_MODIFIED},
657: * {@link #FLAG_OLD_STYLESHEET}, and {@link #FLAG_OLD_PROJECT_XML} to be safe).
658: * </p>
659: * <p>
660: * Acquires read access.
661: * </p>
662: * @param path a project-relative path such as {@link #BUILD_XML_PATH} or {@link #BUILD_IMPL_XML_PATH}
663: * @param stylesheet a URL to an XSLT stylesheet accepting <code>project.xml</code>
664: * as input and producing the build script as output
665: * (should match that given to {@link #generateBuildScriptFromStylesheet})
666: * @return a bitwise OR of various flags, or <code>0</code> if the script
667: * is present on disk and fully up-to-date
668: * @throws IllegalStateException if the project was modified
669: */
670: public int getBuildScriptState(final String path,
671: final URL stylesheet) throws IllegalStateException {
672: try {
673: return ProjectManager.mutex().readAccess(
674: new Mutex.ExceptionAction<Integer>() {
675: public Integer run() throws IOException {
676: if (h != null && h.isProjectXmlModified()) {
677: throw new IllegalStateException(
678: "Cannot generate build scripts from a modified project"); // NOI18N
679: }
680: FileObject script = dir.getFileObject(path);
681: if (script == null
682: || /* #55164 */script.isVirtual()) {
683: return FLAG_MISSING;
684: }
685: int flags = 0;
686: Properties p = new Properties();
687: FileObject genfiles = dir
688: .getFileObject(GENFILES_PROPERTIES_PATH);
689: if (genfiles == null
690: || /* #55164 */genfiles
691: .isVirtual()) {
692: // Who knows? User deleted it; anything might be wrong. Safest to assume
693: // that everything is.
694: return FLAG_UNKNOWN | FLAG_MODIFIED
695: | FLAG_OLD_PROJECT_XML
696: | FLAG_OLD_STYLESHEET;
697: }
698: InputStream is = new BufferedInputStream(
699: genfiles.getInputStream());
700: try {
701: p.load(is);
702: } finally {
703: is.close();
704: }
705: FileObject projectXml = dir
706: .getFileObject(AntProjectHelper.PROJECT_XML_PATH);
707: if (projectXml != null
708: && /* #55164 */!projectXml
709: .isVirtual()) {
710: String crc = getCrc32(projectXml);
711: if (!crc.equals(p.getProperty(path
712: + KEY_SUFFIX_DATA_CRC))) {
713: flags |= FLAG_OLD_PROJECT_XML;
714: }
715: } else {
716: // Broken project?!
717: flags |= FLAG_OLD_PROJECT_XML;
718: }
719: String crc = getCrc32(stylesheet);
720: if (!crc.equals(p.getProperty(path
721: + KEY_SUFFIX_STYLESHEET_CRC))) {
722: flags |= FLAG_OLD_STYLESHEET;
723: }
724: crc = getCrc32(script);
725: if (!crc.equals(p.getProperty(path
726: + KEY_SUFFIX_SCRIPT_CRC))) {
727: flags |= FLAG_MODIFIED;
728: }
729: return flags;
730: }
731: });
732: } catch (MutexException e) {
733: ErrorManager.getDefault().notify(
734: ErrorManager.INFORMATIONAL,
735: (IOException) e.getException());
736: return FLAG_UNKNOWN | FLAG_MODIFIED | FLAG_OLD_PROJECT_XML
737: | FLAG_OLD_STYLESHEET;
738: }
739: }
740:
741: /**
742: * Compute the CRC-32 of the contents of a stream.
743: * \r\n and \r are both normalized to \n for purposes of the calculation.
744: */
745: static String computeCrc32(InputStream is) throws IOException {
746: Checksum crc = new CRC32();
747: int last = -1;
748: int curr;
749: while ((curr = is.read()) != -1) {
750: if (curr != '\n' && last == '\r') {
751: crc.update('\n');
752: }
753: if (curr != '\r') {
754: crc.update(curr);
755: }
756: last = curr;
757: }
758: if (last == '\r') {
759: crc.update('\n');
760: }
761: int val = (int) crc.getValue();
762: String hex = Integer.toHexString(val);
763: while (hex.length() < 8) {
764: hex = "0" + hex; // NOI18N
765: }
766: return hex;
767: }
768:
769: // #50440 - cache CRC32's for various files to save time esp. during startup.
770:
771: private static final Map<URL, String> crcCache = new HashMap<URL, String>();
772: private static final Map<URL, Long> crcCacheTimestampsXorSizes = new HashMap<URL, Long>();
773:
774: /** Try to find a CRC in the cache according to location of file and last mod time xor size. */
775: private static synchronized String findCachedCrc32(URL u,
776: long footprint) {
777: String crc = crcCache.get(u);
778: if (crc != null) {
779: Long l = crcCacheTimestampsXorSizes.get(u);
780: assert l != null;
781: if (l == footprint) {
782: // Cache hit.
783: return crc;
784: }
785: }
786: // Cache miss - missing or old.
787: return null;
788: }
789:
790: /** Cache a known CRC for a file, using current last mod time xor size. */
791: private static synchronized void cacheCrc32(String crc, URL u,
792: long footprint) {
793: crcCache.put(u, crc);
794: crcCacheTimestampsXorSizes.put(u, footprint);
795: }
796:
797: /** Find (maybe cached) CRC for a file, using a preexisting input stream (not closed by this method). */
798: private static String getCrc32(InputStream is, FileObject fo)
799: throws IOException {
800: URL u = fo.getURL();
801: fo.refresh(); // in case was written on disk and we did not notice yet...
802: long footprint = fo.lastModified().getTime() ^ fo.getSize();
803: String crc = findCachedCrc32(u, footprint);
804: if (crc == null) {
805: crc = computeCrc32(is);
806: cacheCrc32(crc, u, footprint);
807: }
808: return crc;
809: }
810:
811: /** Find the time the file this URL represents was last modified xor its size, if possible. */
812: private static long checkFootprint(URL u) {
813: URL nested = FileUtil.getArchiveFile(u);
814: if (nested != null) {
815: u = nested;
816: }
817: if (u.getProtocol().equals("file")) { // NOI18N
818: File f = new File(URI.create(u.toExternalForm()));
819: return f.lastModified() ^ f.length();
820: } else {
821: return 0L;
822: }
823: }
824:
825: /** Find (maybe cached) CRC for a URL, using a preexisting input stream (not closed by this method). */
826: private static String getCrc32(InputStream is, URL u)
827: throws IOException {
828: long footprint = checkFootprint(u);
829: String crc = null;
830: if (footprint != 0L) {
831: crc = findCachedCrc32(u, footprint);
832: }
833: if (crc == null) {
834: crc = computeCrc32(is);
835: if (footprint != 0L) {
836: cacheCrc32(crc, u, footprint);
837: }
838: }
839: return crc;
840: }
841:
842: /** Find (maybe cached) CRC for a file. Will open its own input stream. */
843: private static String getCrc32(FileObject fo) throws IOException {
844: URL u = fo.getURL();
845: fo.refresh();
846: long footprint = fo.lastModified().getTime() ^ fo.getSize();
847: String crc = findCachedCrc32(u, footprint);
848: if (crc == null) {
849: InputStream is = fo.getInputStream();
850: try {
851: crc = computeCrc32(new BufferedInputStream(is));
852: cacheCrc32(crc, u, footprint);
853: } finally {
854: is.close();
855: }
856: }
857: return crc;
858: }
859:
860: /** Find (maybe cached) CRC for a URL. Will open its own input stream. */
861: private static String getCrc32(URL u) throws IOException {
862: long footprint = checkFootprint(u);
863: String crc = null;
864: if (footprint != 0L) {
865: crc = findCachedCrc32(u, footprint);
866: }
867: if (crc == null) {
868: InputStream is = u.openStream();
869: try {
870: crc = computeCrc32(new BufferedInputStream(is));
871: if (footprint != 0L) {
872: cacheCrc32(crc, u, footprint);
873: }
874: } finally {
875: is.close();
876: }
877: }
878: return crc;
879: }
880:
881: /**
882: * Convenience method to refresh a build script if it can and should be.
883: * <p>
884: * If the script is not modified, and it is either missing, or the flag
885: * <code>checkForProjectXmlModified</code> is false, or it is out of date with
886: * respect to either <code>project.xml</code> or the stylesheet (or both),
887: * it is (re-)generated.
888: * </p>
889: * <p>
890: * Acquires write access.
891: * </p>
892: * <p class="nonnormative">
893: * Typical usage from {@link ProjectXmlSavedHook#projectXmlSaved} is to call
894: * this method for both {@link #BUILD_XML_PATH} and {@link #BUILD_IMPL_XML_PATH}
895: * with the appropriate stylesheets and with <code>checkForProjectXmlModified</code>
896: * false (the script is certainly out of date relative to <code>project.xml</code>).
897: * Typical usage from {@link org.netbeans.spi.project.ui.ProjectOpenedHook#projectOpened} is to call
898: * this method for both scripts with the appropriate stylesheets and with
899: * <code>checkForProjectXmlModified</code> true.
900: * </p>
901: * @param path a project-relative path such as {@link #BUILD_XML_PATH} or {@link #BUILD_IMPL_XML_PATH}
902: * @param stylesheet a URL to an XSLT stylesheet accepting <code>project.xml</code>
903: * as input and producing the build script as output
904: * @param checkForProjectXmlModified true if it is necessary to check whether the
905: * script is out of date with respect to
906: * <code>project.xml</code> and/or the stylesheet
907: * @return true if the script was in fact regenerated
908: * @throws IOException if transforming or writing the output failed
909: * @throws IllegalStateException if the project was modified
910: */
911: public boolean refreshBuildScript(final String path,
912: final URL stylesheet,
913: final boolean checkForProjectXmlModified)
914: throws IOException, IllegalStateException {
915: try {
916: return ProjectManager.mutex().writeAccess(
917: new Mutex.ExceptionAction<Boolean>() {
918: public Boolean run() throws IOException {
919: int flags = getBuildScriptState(path,
920: stylesheet);
921: if (shouldGenerateBuildScript(flags,
922: checkForProjectXmlModified)) {
923: generateBuildScriptFromStylesheet(path,
924: stylesheet);
925: return true;
926: } else {
927: return false;
928: }
929: }
930: });
931: } catch (MutexException e) {
932: throw (IOException) e.getException();
933: }
934: }
935:
936: private static boolean shouldGenerateBuildScript(int flags,
937: boolean checkForProjectXmlModified) {
938: if ((flags & GeneratedFilesHelper.FLAG_MISSING) != 0) {
939: // Yes, need it.
940: return true;
941: }
942: if ((flags & GeneratedFilesHelper.FLAG_MODIFIED) != 0) {
943: // No, don't overwrite a user build script.
944: // XXX modified build-impl.xml probably counts as a serious condition
945: // to warn the user about...
946: // Modified build.xml is no big deal.
947: return false;
948: }
949: if (!checkForProjectXmlModified) {
950: // OK, assume it is out of date.
951: return true;
952: }
953: // Check whether it is in fact out of date.
954: return (flags & (GeneratedFilesHelper.FLAG_OLD_PROJECT_XML | GeneratedFilesHelper.FLAG_OLD_STYLESHEET)) != 0;
955: }
956:
957: // #45373 - workaround: on Windows make sure that all lines end with CRLF.
958: // marcow: Use at least some buffered output!
959: private static class EolFilterOutputStream extends
960: BufferedOutputStream {
961:
962: private boolean isActive = Utilities.isWindows();
963: private int last = -1;
964:
965: public EolFilterOutputStream(OutputStream os) {
966: super (os, 4096);
967: }
968:
969: public void write(byte[] b, int off, int len)
970: throws IOException {
971: if (isActive) {
972: for (int i = off; i < off + len; i++) {
973: write(b[i]);
974: }
975: } else {
976: super .write(b, off, len);
977: }
978: }
979:
980: public void write(int b) throws IOException {
981: if (isActive) {
982: if (b == '\n' && last != '\r') {
983: super .write('\r');
984: }
985: last = b;
986: }
987: super.write(b);
988: }
989:
990: }
991: }
|