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.api.project.ant;
043:
044: import org.netbeans.spi.project.ant.AntBuildExtenderImplementation;
045: import java.util.ArrayList;
046: import java.util.Collection;
047: import java.util.Collections;
048: import java.util.HashMap;
049: import java.util.HashSet;
050: import java.util.List;
051: import java.util.Map;
052: import java.util.Set;
053: import java.util.TreeMap;
054: import javax.xml.parsers.DocumentBuilder;
055: import javax.xml.parsers.DocumentBuilderFactory;
056: import javax.xml.parsers.ParserConfigurationException;
057: import org.netbeans.api.project.FileOwnerQuery;
058: import org.netbeans.modules.project.ant.AntBuildExtenderAccessor;
059: import org.netbeans.spi.project.AuxiliaryConfiguration;
060: import org.netbeans.spi.project.ant.AntBuildExtenderImplementation;
061: import org.netbeans.spi.project.support.ant.AntProjectHelper;
062: import org.openide.filesystems.FileObject;
063: import org.openide.filesystems.FileUtil;
064: import org.w3c.dom.Document;
065: import org.w3c.dom.Element;
066: import org.w3c.dom.NodeList;
067:
068: /**
069: * Allows extending the project's build script with 3rd party additions.
070: * Check the Project's lookup to see if the feature is supported by a given Ant project type.
071: * Typical usage:
072: * <ul>
073: * <li>Lookup the instance of AntBuildExtender in the project at hand</li>
074: * <li>Create the external build script file with your targets and configuration</li>
075: * <li>Use the AntBuildExtender to wire your script and targets into the main build lifecycle</li>
076: * <li>Call {@link org.netbeans.api.project.ProjectManager#saveProject} to persist the changes and
077: * regenerate the main build script</li>
078: * </ul>
079: *
080: * Please note that it's easy to break the build script functionality and any script extensions
081: * shall be done with care. A few rules to follow:
082: * <ul>
083: * <li>Pick a reasonably unique extension id</li>
084: * <li>Prefix target names and properties you define in your extension with the extension id to prevent clashes.</li>
085: * </ul>
086: * @author mkleint
087: * @since org.netbeans.modules.project.ant 1.16
088: */
089: public final class AntBuildExtender {
090: private HashMap<String, Extension> extensions;
091: private AntBuildExtenderImplementation implementation;
092:
093: static {
094: AntBuildExtenderAccessorImpl.createAccesor();
095: }
096:
097: AntBuildExtender(AntBuildExtenderImplementation implementation) {
098: this .implementation = implementation;
099: }
100:
101: /**
102: * Get a list of target names in the main build script that are allowed to be
103: * extended by adding the "depends" attribute definition to them.
104: * @return list of target names
105: */
106: public List<String> getExtensibleTargets() {
107: List<String> targets = new ArrayList<String>();
108: targets.addAll(implementation.getExtensibleTargets());
109: targets = Collections.unmodifiableList(targets);
110: return targets;
111: }
112:
113: /**
114: * Adds a new build script extension.
115: * @param id identification of the extension
116: * @param extensionXml fileobject referencing the build script for the extension,
117: * needs to be located in nbproject directory or below.
118: * @return the newly created extension.
119: */
120: public synchronized Extension addExtension(String id,
121: FileObject extensionXml) {
122: assert extensionXml != null;
123: assert extensionXml.isValid() && extensionXml.isData();
124: //TODO assert the owner is the same as the owner of this instance of entender.
125: assert FileOwnerQuery.getOwner(extensionXml) == implementation
126: .getOwningProject();
127: FileObject nbproj = implementation.getOwningProject()
128: .getProjectDirectory().getFileObject(
129: AntProjectHelper.PROJECT_XML_PATH).getParent();
130: assert FileUtil.isParentOf(nbproj, extensionXml);
131: if (extensions == null) {
132: readProjectMetadata();
133: }
134: if (extensions.get(id) != null) {
135: throw new IllegalStateException("Extension with id '" + id
136: + "' already exists.");
137: }
138: Extension ex = new Extension(id, extensionXml, FileUtil
139: .getRelativePath(nbproj, extensionXml));
140: extensions.put(id, ex);
141: updateProjectMetadata();
142: return ex;
143: }
144:
145: /**
146: * Remove an existing build script extension. Make sure to remove the extension's script file
147: * before/after removing the extension.
148: * @param id identification of the extension
149: */
150: public synchronized void removeExtension(String id) {
151: if (extensions == null) {
152: readProjectMetadata();
153: }
154: if (extensions.get(id) == null) {
155: // oh well, just ignore.
156: return;
157: }
158: extensions.remove(id);
159: updateProjectMetadata();
160: }
161:
162: /**
163: * Get an extension by the id.
164: * @param id identification token
165: * @return Extention with the given id or null if not found.
166: */
167: public synchronized Extension getExtension(String id) {
168: if (extensions == null) {
169: readProjectMetadata();
170: }
171: return extensions.get(id);
172: }
173:
174: synchronized Set<Extension> getExtensions() {
175: Set<Extension> ext = new HashSet<Extension>();
176: if (extensions == null) {
177: readProjectMetadata();
178: }
179: ext.addAll(extensions.values());
180: return ext;
181: }
182:
183: private static final DocumentBuilder db;
184: static {
185: try {
186: db = DocumentBuilderFactory.newInstance()
187: .newDocumentBuilder();
188: } catch (ParserConfigurationException e) {
189: throw new AssertionError(e);
190: }
191: }
192:
193: private static Document createNewDocument() {
194: // #50198: for thread safety, use a separate document.
195: // Using XMLUtil.createDocument is much too slow.
196: synchronized (db) {
197: return db.newDocument();
198: }
199: }
200:
201: private void updateProjectMetadata() {
202: Document doc = createNewDocument();
203: Element root = doc.createElementNS(
204: AntBuildExtenderAccessor.AUX_NAMESPACE,
205: AntBuildExtenderAccessor.ELEMENT_ROOT);
206: if (extensions != null) {
207: FileObject nbproj = implementation.getOwningProject()
208: .getProjectDirectory().getFileObject(
209: AntProjectHelper.PROJECT_XML_PATH)
210: .getParent();
211: for (Extension ext : extensions.values()) {
212: Element child = doc
213: .createElement(AntBuildExtenderAccessor.ELEMENT_EXTENSION);
214: child.setAttribute(AntBuildExtenderAccessor.ATTR_ID,
215: ext.id);
216: String relPath = FileUtil.getRelativePath(nbproj,
217: ext.file);
218: assert relPath != null;
219: child.setAttribute(AntBuildExtenderAccessor.ATTR_FILE,
220: relPath);
221: root.appendChild(child);
222: for (String target : ext.dependencies.keySet()) {
223: for (String depTarget : ext.dependencies
224: .get(target)) {
225: Element dep = doc
226: .createElement(AntBuildExtenderAccessor.ELEMENT_DEPENDENCY);
227: dep.setAttribute(
228: AntBuildExtenderAccessor.ATTR_TARGET,
229: target);
230: dep
231: .setAttribute(
232: AntBuildExtenderAccessor.ATTR_DEPENDSON,
233: depTarget);
234: child.appendChild(dep);
235: }
236: }
237: }
238: }
239: AuxiliaryConfiguration config = implementation
240: .getOwningProject().getLookup().lookup(
241: AuxiliaryConfiguration.class);
242: config.putConfigurationFragment(root, true);
243: }
244:
245: private void readProjectMetadata() {
246: AuxiliaryConfiguration config = implementation
247: .getOwningProject().getLookup().lookup(
248: AuxiliaryConfiguration.class);
249: Element cfgEl = config.getConfigurationFragment(
250: AntBuildExtenderAccessor.ELEMENT_ROOT,
251: AntBuildExtenderAccessor.AUX_NAMESPACE, true);
252: FileObject nbproj = implementation.getOwningProject()
253: .getProjectDirectory().getFileObject(
254: AntProjectHelper.PROJECT_XML_PATH).getParent();
255: extensions = new HashMap<String, Extension>();
256: if (cfgEl != null) {
257: String namespace = cfgEl.getNamespaceURI();
258: NodeList roots = cfgEl.getElementsByTagNameNS(namespace,
259: AntBuildExtenderAccessor.ELEMENT_EXTENSION);
260: for (int i = 0; i < roots.getLength(); i++) {
261: Element root = (Element) roots.item(i);
262: String id = root
263: .getAttribute(AntBuildExtenderAccessor.ATTR_ID);
264: assert id.length() > 0 : "Illegal project.xml";
265: String value = root
266: .getAttribute(AntBuildExtenderAccessor.ATTR_FILE);
267: FileObject script = nbproj.getFileObject(value);
268: assert script != null : "Missing file " + value
269: + " for extension " + id;
270: Extension ext = new Extension(id, script, value);
271: extensions.put(id, ext);
272: NodeList deps = root.getElementsByTagNameNS(namespace,
273: AntBuildExtenderAccessor.ELEMENT_DEPENDENCY);
274: for (int j = 0; j < deps.getLength(); j++) {
275: Element dep = (Element) deps.item(j);
276: String target = dep
277: .getAttribute(AntBuildExtenderAccessor.ATTR_TARGET);
278: String dependsOn = dep
279: .getAttribute(AntBuildExtenderAccessor.ATTR_DEPENDSON);
280: assert target != null;
281: assert dependsOn != null;
282: ext.loadDependency(target, dependsOn);
283: }
284: }
285: }
286: }
287:
288: /**
289: * Describes and allows to manipulate the build script extension and it's links to the main build script
290: * of the project.
291: */
292: public final class Extension {
293: String id;
294: FileObject file;
295: String path;
296: TreeMap<String, Collection<String>> dependencies;
297:
298: Extension(String id, FileObject script, String relPath) {
299: this .id = id;
300: file = script;
301: path = relPath;
302: dependencies = new TreeMap<String, Collection<String>>();
303: }
304:
305: String getPath() {
306: return path;
307: }
308:
309: /**
310: * Add a dependency of a main build script target on the target in the extension's script.
311: * @param mainBuildTarget name of target in the main build script (see {@link org.netbeans.api.project.ant.AntBuildExtender#getExtensibleTargets})
312: * @param extensionTarget name of target in the extension script
313: */
314: public void addDependency(String mainBuildTarget,
315: String extensionTarget) {
316: assert implementation.getExtensibleTargets().contains(
317: mainBuildTarget) : "The target '"
318: + mainBuildTarget
319: + "' is not designated by the project type as extensible.";
320: synchronized (AntBuildExtender.class) {
321: loadDependency(mainBuildTarget, extensionTarget);
322: updateProjectMetadata();
323: }
324: }
325:
326: private void loadDependency(String mainBuildTarget,
327: String extensionTarget) {
328: synchronized (AntBuildExtender.class) {
329: Collection<String> tars = dependencies
330: .get(mainBuildTarget);
331: if (tars == null) {
332: tars = new ArrayList<String>();
333: dependencies.put(mainBuildTarget, tars);
334: }
335: if (!tars.contains(extensionTarget)) {
336: tars.add(extensionTarget);
337: } else {
338: //log?
339: }
340: }
341: }
342:
343: /**
344: * Remove a dependency of a main build script target on the target in the extension's script.
345: *
346: * @param mainBuildTarget name of target in the main build script (see {@link org.netbeans.api.project.ant.AntBuildExtender#getExtensibleTargets})
347: * @param extensionTarget name of target in the extension script
348: */
349: public void removeDependency(String mainBuildTarget,
350: String extensionTarget) {
351: Collection<String> str = dependencies.get(mainBuildTarget);
352: if (str != null) {
353: str.remove(extensionTarget);
354: updateProjectMetadata();
355: } else {
356: //oh well, just ignore, nothing to update anyway..
357: }
358: }
359:
360: Map<String, Collection<String>> getDependencies() {
361: TreeMap<String, Collection<String>> toRet = new TreeMap<String, Collection<String>>();
362: synchronized (AntBuildExtender.class) {
363: for (String str : dependencies.keySet()) {
364: ArrayList<String> col = new ArrayList<String>();
365: col.addAll(dependencies.get(str));
366: toRet.put(str, col);
367: }
368: }
369: return toRet;
370: }
371: }
372: }
|