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.modules.ant.freeform.spi.support;
043:
044: import java.io.File;
045: import java.io.IOException;
046: import java.util.ArrayList;
047: import java.util.Arrays;
048: import java.util.HashMap;
049: import java.util.Iterator;
050: import java.util.List;
051: import java.util.Map;
052: import javax.xml.XMLConstants;
053: import javax.xml.transform.dom.DOMSource;
054: import javax.xml.validation.Schema;
055: import javax.xml.validation.SchemaFactory;
056: import javax.xml.validation.Validator;
057: import org.netbeans.api.project.Project;
058: import org.netbeans.api.project.ProjectManager;
059: import org.netbeans.api.queries.CollocationQuery;
060: import org.netbeans.modules.ant.freeform.FreeformProject;
061: import org.netbeans.modules.ant.freeform.FreeformProjectGenerator;
062: import org.netbeans.modules.ant.freeform.FreeformProjectType;
063: import org.netbeans.modules.ant.freeform.spi.ProjectAccessor;
064: import org.netbeans.modules.ant.freeform.spi.ProjectConstants;
065: import org.netbeans.spi.project.AuxiliaryConfiguration;
066: import org.netbeans.spi.project.support.ant.AntProjectHelper;
067: import org.netbeans.spi.project.support.ant.PropertyEvaluator;
068: import org.netbeans.spi.project.support.ant.PropertyUtils;
069: import org.openide.ErrorManager;
070: import org.openide.filesystems.FileObject;
071: import org.openide.filesystems.FileUtil;
072: import org.openide.util.Mutex;
073: import org.w3c.dom.Attr;
074: import org.w3c.dom.DOMException;
075: import org.w3c.dom.Document;
076: import org.w3c.dom.Element;
077: import org.w3c.dom.NamedNodeMap;
078: import org.w3c.dom.Node;
079: import org.w3c.dom.NodeList;
080: import org.w3c.dom.Text;
081: import org.w3c.dom.ls.DOMImplementationLS;
082: import org.w3c.dom.ls.LSSerializer;
083: import org.xml.sax.ErrorHandler;
084: import org.xml.sax.SAXException;
085: import org.xml.sax.SAXParseException;
086:
087: /**
088: * Miscellaneous helper methods.
089: * @author Jesse Glick, David Konecny
090: */
091: public class Util {
092:
093: private Util() {
094: }
095:
096: // XXX XML methods copied from ant/project... make a general API of these instead?
097:
098: /**
099: * Search for an XML element in the direct children of a parent.
100: * DOM provides a similar method but it does a recursive search
101: * which we do not want. It also gives a node list and we want
102: * only one result.
103: * @param parent a parent element
104: * @param name the intended local name
105: * @param namespace the intended namespace
106: * @return the one child element with that name, or null if none or more than one
107: */
108: public static Element findElement(Element parent, String name,
109: String namespace) {
110: Element result = null;
111: NodeList l = parent.getChildNodes();
112: for (int i = 0; i < l.getLength(); i++) {
113: if (l.item(i).getNodeType() == Node.ELEMENT_NODE) {
114: Element el = (Element) l.item(i);
115: if (name.equals(el.getLocalName())
116: && namespace.equals(el.getNamespaceURI())) {
117: if (result == null) {
118: result = el;
119: } else {
120: return null;
121: }
122: }
123: }
124: }
125: return result;
126: }
127:
128: /**
129: * Extract nested text from an element.
130: * Currently does not handle coalescing text nodes, CDATA sections, etc.
131: * @param parent a parent element
132: * @return the nested text, or null if none was found
133: */
134: public static String findText(Element parent) {
135: NodeList l = parent.getChildNodes();
136: for (int i = 0; i < l.getLength(); i++) {
137: if (l.item(i).getNodeType() == Node.TEXT_NODE) {
138: Text text = (Text) l.item(i);
139: return text.getNodeValue();
140: }
141: }
142: return null;
143: }
144:
145: /**
146: * Find all direct child elements of an element.
147: * More useful than {@link Element#getElementsByTagNameNS} because it does
148: * not recurse into recursive child elements.
149: * Children which are all-whitespace text nodes are ignored; others cause
150: * an exception to be thrown.
151: * @param parent a parent element in a DOM tree
152: * @return a list of direct child elements (may be empty)
153: * @throws IllegalArgumentException if there are non-element children besides whitespace
154: */
155: public static List<Element> findSubElements(Element parent)
156: throws IllegalArgumentException {
157: NodeList l = parent.getChildNodes();
158: List<Element> elements = new ArrayList<Element>(l.getLength());
159: for (int i = 0; i < l.getLength(); i++) {
160: Node n = l.item(i);
161: if (n.getNodeType() == Node.ELEMENT_NODE) {
162: elements.add((Element) n);
163: } else if (n.getNodeType() == Node.TEXT_NODE) {
164: String text = ((Text) n).getNodeValue();
165: if (text.trim().length() > 0) {
166: throw new IllegalArgumentException(
167: "non-ws text encountered in " + parent
168: + ": " + text); // NOI18N
169: }
170: } else if (n.getNodeType() == Node.COMMENT_NODE) {
171: // skip
172: } else {
173: throw new IllegalArgumentException(
174: "unexpected non-element child of " + parent
175: + ": " + n); // NOI18N
176: }
177: }
178: return elements;
179: }
180:
181: /**
182: * Finds AuxiliaryConfiguration for the given project helper. The method
183: * finds project associated with the helper and searches
184: * AuxiliaryConfiguration in project's lookup.
185: *
186: * @param helper instance of project's AntProjectHelper
187: * @return project's AuxiliaryConfiguration
188: */
189: public static AuxiliaryConfiguration getAuxiliaryConfiguration(
190: AntProjectHelper helper) {
191: try {
192: Project p = ProjectManager.getDefault().findProject(
193: helper.getProjectDirectory());
194: AuxiliaryConfiguration aux = p.getLookup().lookup(
195: AuxiliaryConfiguration.class);
196: assert aux != null;
197: return aux;
198: } catch (IOException e) {
199: ErrorManager.getDefault().notify(e);
200: return null;
201: }
202: }
203:
204: /**
205: * Relativize given file against the original project and if needed use
206: * ${project.dir} property as base. If file cannot be relativized
207: * the absolute filepath is returned.
208: * @param projectBase original project base folder
209: * @param freeformBase Freeform project base folder
210: * @param location location to relativize
211: * @return text suitable for storage in project.xml representing given location
212: */
213: public static String relativizeLocation(File projectBase,
214: File freeformBase, File location) {
215: if (CollocationQuery.areCollocated(projectBase, location)) {
216: if (projectBase.equals(freeformBase)) {
217: return PropertyUtils.relativizeFile(projectBase,
218: location);
219: } else if (projectBase.equals(location)
220: && ProjectConstants.PROJECT_LOCATION_PREFIX
221: .endsWith("/")) { // NOI18N
222: return ProjectConstants.PROJECT_LOCATION_PREFIX
223: .substring(
224: 0,
225: ProjectConstants.PROJECT_LOCATION_PREFIX
226: .length() - 1);
227: } else {
228: return ProjectConstants.PROJECT_LOCATION_PREFIX
229: + PropertyUtils.relativizeFile(projectBase,
230: location);
231: }
232: } else {
233: return location.getAbsolutePath();
234: }
235: }
236:
237: /**
238: * Resolve given string value (e.g. "${project.dir}/lib/lib1.jar")
239: * to a File.
240: * @param evaluator evaluator to use for properties resolving
241: * @param freeformProjectBase freeform project base folder
242: * @param val string to be resolved as file
243: * @return resolved File or null if file could not be resolved
244: */
245: public static File resolveFile(PropertyEvaluator evaluator,
246: File freeformProjectBase, String val) {
247: String location = evaluator.evaluate(val);
248: if (location == null) {
249: return null;
250: }
251: return PropertyUtils.resolveFile(freeformProjectBase, location);
252: }
253:
254: /**
255: * Returns location of original project base folder. The location can be dirrerent
256: * from NetBeans metadata project folder.
257: * @param helper AntProjectHelper associated with the project
258: * @param evaluator PropertyEvaluator associated with the project
259: * @return location of original project base folder
260: */
261: public static File getProjectLocation(AntProjectHelper helper,
262: PropertyEvaluator evaluator) {
263: //assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess();
264: String loc = evaluator
265: .getProperty(ProjectConstants.PROP_PROJECT_LOCATION);
266: if (loc != null) {
267: return helper.resolveFile(loc);
268: } else {
269: return FileUtil.toFile(helper.getProjectDirectory());
270: }
271: }
272:
273: /**
274: * Append child element to the correct position according to given
275: * order.
276: * @param parent parent to which the child will be added
277: * @param el element to be added
278: * @param order order of the elements which must be followed
279: */
280: public static void appendChildElement(Element parent, Element el,
281: String[] order) {
282: Element insertBefore = null;
283: List l = Arrays.asList(order);
284: int index = l.indexOf(el.getLocalName());
285: assert index != -1 : el.getLocalName() + " was not found in "
286: + l; // NOI18N
287: Iterator it = Util.findSubElements(parent).iterator();
288: while (it.hasNext()) {
289: Element e = (Element) it.next();
290: int index2 = l.indexOf(e.getLocalName());
291: assert index2 != -1 : e.getLocalName()
292: + " was not found in " + l; // NOI18N
293: if (index2 > index) {
294: insertBefore = e;
295: break;
296: }
297: }
298: parent.insertBefore(el, insertBefore);
299: }
300:
301: /**Get the "default" (user-specified) ant script for the given freeform project.
302: * Please note that this method may return <code>null</code> if there is no such script.
303: *
304: * WARNING: This method is there only for a limited set of usecases like the profiler plugin.
305: * It should not be used by the freeform project natures.
306: *
307: * @param prj the freeform project
308: * @return the "default" ant script or <code>null</code> if there is no such a script
309: * @throws IllegalArgumentException if the passed project is not a freeform project.
310: */
311: public static FileObject getDefaultAntScript(Project prj)
312: throws IllegalArgumentException {
313: ProjectAccessor accessor = prj.getLookup().lookup(
314: ProjectAccessor.class);
315:
316: if (accessor == null) {
317: throw new IllegalArgumentException(
318: "Only FreeformProjects are supported.");
319: }
320:
321: return FreeformProjectGenerator.getAntScript(accessor
322: .getHelper(), accessor.getEvaluator());
323: }
324:
325: /**
326: * Convert an XML fragment from one namespace to another.
327: */
328: private static Element translateXML(Element from, String namespace) {
329: Element to = from.getOwnerDocument().createElementNS(namespace,
330: from.getLocalName());
331: NodeList nl = from.getChildNodes();
332: int length = nl.getLength();
333: for (int i = 0; i < length; i++) {
334: Node node = nl.item(i);
335: Node newNode;
336: if (node.getNodeType() == Node.ELEMENT_NODE) {
337: newNode = translateXML((Element) node, namespace);
338: } else {
339: newNode = node.cloneNode(true);
340: }
341: to.appendChild(newNode);
342: }
343: NamedNodeMap m = from.getAttributes();
344: for (int i = 0; i < m.getLength(); i++) {
345: Node attr = m.item(i);
346: to.setAttribute(attr.getNodeName(), attr.getNodeValue());
347: }
348: return to;
349: }
350:
351: /**
352: * Namespace of data used in {@link #getPrimaryConfigurationData} and {@link #putPrimaryConfigurationData}.
353: * @since org.netbeans.modules.ant.freeform/1 1.15
354: */
355: public static final String NAMESPACE = "http://www.netbeans.org/ns/freeform-project/2"; // NOI18N
356:
357: /**
358: * Replacement for {@link AntProjectHelper#getPrimaryConfigurationData}
359: * taking into account the /1 -> /2 upgrade.
360: * @param helper a project helper
361: * @return data in {@link #NAMESPACE}, converting /1 data if needed
362: * @since org.netbeans.modules.ant.freeform/1 1.15
363: */
364: public static Element getPrimaryConfigurationData(
365: final AntProjectHelper helper) {
366: return ProjectManager.mutex().readAccess(
367: new Mutex.Action<Element>() {
368: public Element run() {
369: AuxiliaryConfiguration ac = helper
370: .createAuxiliaryConfiguration();
371: Element data = ac.getConfigurationFragment(
372: FreeformProjectType.NAME_SHARED,
373: NAMESPACE, true);
374: if (data != null) {
375: return data;
376: } else {
377: return translateXML(helper
378: .getPrimaryConfigurationData(true),
379: NAMESPACE);
380: }
381: }
382: });
383: }
384:
385: /**
386: * Replacement for {@link AntProjectHelper#putPrimaryConfigurationData}
387: * taking into account the /1 -> /2 upgrade.
388: * Always pass the /2 data, which will be converted to /1 where legal.
389: * @param helper a project helper
390: * @param data data in {@link #NAMESPACE}
391: * @throws IllegalArgumentException if the incoming data is not in {@link #NAMESPACE}
392: * @since org.netbeans.modules.ant.freeform/1 1.15
393: */
394: public static void putPrimaryConfigurationData(
395: final AntProjectHelper helper, final Element data) {
396: if (!data.getNamespaceURI().equals(
397: FreeformProjectType.NS_GENERAL)) {
398: throw new IllegalArgumentException("Bad namespace"); // NOI18N
399: }
400: ProjectManager.mutex().writeAccess(new Mutex.Action<Void>() {
401: public Void run() {
402: Element dataAs1 = translateXML(data,
403: FreeformProjectType.NS_GENERAL_1);
404: try {
405: validate(dataAs1, SCHEMA_1);
406: putPrimaryConfigurationDataAs1(helper, dataAs1);
407: } catch (SAXException x1) {
408: try {
409: validate(data, SCHEMA_2);
410: putPrimaryConfigurationDataAs2(helper, data);
411: } catch (SAXException x2) {
412: assert false : x2.getMessage()
413: + "; rejected content: " + format(data);
414: putPrimaryConfigurationDataAs1(helper, dataAs1);
415: }
416: }
417: return null;
418: }
419: });
420: }
421:
422: private static void putPrimaryConfigurationDataAs1(
423: AntProjectHelper helper, Element data) {
424: helper.createAuxiliaryConfiguration()
425: .removeConfigurationFragment(
426: FreeformProjectType.NAME_SHARED, NAMESPACE,
427: true);
428: helper.putPrimaryConfigurationData(data, true);
429: }
430:
431: private static void putPrimaryConfigurationDataAs2(
432: AntProjectHelper helper, Element data) {
433: Document doc = data.getOwnerDocument();
434: Element dummy1 = doc.createElementNS(
435: FreeformProjectType.NS_GENERAL_1,
436: FreeformProjectType.NAME_SHARED);
437: // Make sure it is not invalid.
438: dummy1.appendChild(
439: doc.createElementNS(FreeformProjectType.NS_GENERAL_1,
440: "name")). // NOI18N
441: appendChild(
442: doc.createTextNode(findText(findElement(data,
443: "name", NAMESPACE)))); // NOI18N
444: helper.putPrimaryConfigurationData(dummy1, true);
445: helper.createAuxiliaryConfiguration().putConfigurationFragment(
446: data, true);
447: }
448:
449: private static final Schema SCHEMA_1, SCHEMA_2;
450: static {
451: try {
452: SchemaFactory f = SchemaFactory
453: .newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
454: SCHEMA_1 = f
455: .newSchema(FreeformProject.class
456: .getResource("resources/freeform-project-general.xsd")); // NOI18N
457: SCHEMA_2 = f
458: .newSchema(FreeformProject.class
459: .getResource("resources/freeform-project-general-2.xsd")); // NOI18N
460: } catch (SAXException e) {
461: throw new ExceptionInInitializerError(e);
462: }
463: }
464:
465: private static void validate(Element data, Schema schema)
466: throws SAXException {
467: Validator v = schema.newValidator();
468: final SAXException[] error = { null };
469: v.setErrorHandler(new ErrorHandler() {
470: public void warning(SAXParseException x)
471: throws SAXException {
472: }
473:
474: public void error(SAXParseException x) throws SAXException {
475: // Just rethrowing it is bad because it will also print it to stderr.
476: error[0] = x;
477: }
478:
479: public void fatalError(SAXParseException x)
480: throws SAXException {
481: error[0] = x;
482: }
483: });
484: try {
485: v.validate(new DOMSource(fixupNoNamespaceAttrs(data)));
486: } catch (IOException x) {
487: assert false : x;
488: }
489: if (error[0] != null) {
490: throw error[0];
491: }
492: }
493:
494: private static Element fixupNoNamespaceAttrs(Element root) {
495: // XXX #6529766: some versions of JAXP reject attributes set using setAttribute
496: // (rather than setAttributeNS) even though the schema calls for no-NS attrs!
497: // JDK 5 is fine; JDK 6 broken; JDK 6u2 supposedly will be fixed; current JDK 7 broken
498: Element copy = (Element) root.cloneNode(true);
499: NodeList nl = copy.getElementsByTagName("*");
500: for (int i = 0; i < nl.getLength(); i++) {
501: Element e = (Element) nl.item(i);
502: Map<String, String> replace = new HashMap<String, String>();
503: NamedNodeMap attrs = e.getAttributes();
504: for (int j = 0; j < attrs.getLength(); j++) {
505: Attr attr = (Attr) attrs.item(j);
506: if (attr.getNamespaceURI() == null) {
507: replace.put(attr.getName(), attr.getValue());
508: }
509: }
510: for (Map.Entry<String, String> entry : replace.entrySet()) {
511: e.removeAttribute(entry.getKey());
512: e
513: .setAttributeNS(null, entry.getKey(), entry
514: .getValue());
515: }
516: }
517: return copy;
518: }
519:
520: private static String format(Element data) {
521: LSSerializer ser = ((DOMImplementationLS) data
522: .getOwnerDocument().getImplementation().getFeature(
523: "LS", "3.0")).createLSSerializer();
524: try {
525: ser.getDomConfig()
526: .setParameter("format-pretty-print", true);
527: ser.getDomConfig().setParameter("xml-declaration", false);
528: } catch (DOMException ignore) {
529: }
530: return ser.writeToString(data);
531: }
532:
533: }
|