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.apisupport.project;
043:
044: import java.io.IOException;
045: import java.util.ArrayList;
046: import java.util.Arrays;
047: import java.util.Collections;
048: import java.util.List;
049: import java.util.Map;
050: import java.util.Set;
051: import java.util.SortedSet;
052: import java.util.TreeSet;
053: import org.netbeans.api.project.Project;
054: import org.netbeans.modules.apisupport.project.layers.LayerUtils;
055: import org.openide.filesystems.FileObject;
056: import org.openide.filesystems.FileSystem;
057: import org.openide.filesystems.Repository;
058: import org.openide.modules.SpecificationVersion;
059:
060: /**
061: * Provide general infrastructure for performing miscellaneous operations upon
062: * {@link NbModuleProject}'s files, such as <em>manifest.mf</em>,
063: * <em>bundle.properties</em>, <em>layer.xml</em>, <em>project.xml</em> easily.
064: * See javadoc to individual methods below. After creating a
065: * <code>CreatedModifiedFiles</code> instance client may create {@link
066: * CreatedModifiedFiles.Operation} which then may be added to the
067: * <code>CreatedModifiedFiles</code> instance or just used itself. Both
068: * <code>CreatedModifiedFiles</code> and <code>Operation</code> provide methods
069: * to get sets of relative (to a project's base directory) paths which are
070: * going to be created and/or modified. These sets may be obtained
071: * <strong>before</strong> added operation are run so they can be e.g. shown by
072: * wizard before any files are actually created.
073: *
074: * @author Martin Krauskopf
075: */
076: public final class CreatedModifiedFiles {
077:
078: /**
079: * Operation that may be added to a <code>CreatedModifiedFiles</code>
080: * instance or can just be used alone. See {@link CreatedModifiedFiles} for
081: * more information.
082: */
083: public interface Operation {
084:
085: /** Perform this operation. */
086: void run() throws IOException;
087:
088: /**
089: * Returns sorted array of path which are going to modified after this
090: * {@link CreatedModifiedFiles} instance is run. Paths are relative to
091: * the project's base directory. It is available immediately after an
092: * operation instance is created.
093: * XXX why is this sorted, and not a simple Set<String>?
094: */
095: String[] getModifiedPaths();
096:
097: /**
098: * Returns sorted array of path which are going to created after this
099: * {@link CreatedModifiedFiles} instance is run. Paths are relative to
100: * the project's base directory. It is available immediately after an
101: * operation instance is created.
102: */
103: String[] getCreatedPaths();
104:
105: /**
106: * returns paths that are already existing but the operaton expects to create it.
107: * Is an error condition and should be shown in UI.
108: *
109: */
110: String[] getInvalidPaths();
111:
112: /* XXX should perhaps also have:
113: /**
114: * True if the created or modified path is relevant to the user and should
115: * be selected in the final wizard.
116: * /
117: boolean isRelevant(String path);
118: /**
119: * True if the created or modified path should be opened in the editor.
120: * /
121: boolean isForEditing(String path);
122: */
123:
124: }
125:
126: private final SortedSet<String> createdPaths = new TreeSet<String>();
127: private final SortedSet<String> modifiedPaths = new TreeSet<String>();
128: private final SortedSet<String> invalidPaths = new TreeSet<String>();
129:
130: /** {@link Project} this instance manage. */
131: private final Project project;
132: private final List<CreatedModifiedFiles.Operation> operations = new ArrayList<CreatedModifiedFiles.Operation>();
133:
134: // For use from CreatedModifiedFilesFactory.LayerModifications; XXX would be better to have an operation context or similar
135: // (so that multiple operations could group pre- and post-actions)
136: private LayerUtils.LayerHandle layerHandle;
137:
138: LayerUtils.LayerHandle getLayerHandle() {
139: if (layerHandle == null) {
140: layerHandle = LayerUtils.layerForProject(project);
141: }
142: return layerHandle;
143: }
144:
145: /**
146: * Create instance for managing given {@link NbModuleProject}'s files.
147: * @param project project this instance will operate upon
148: */
149: public CreatedModifiedFiles(Project project) {
150: this .project = project;
151: }
152:
153: /**
154: * Adds given {@link Operation} to a list of operations that will be run
155: * after calling {@link #run()}. Operations are run in the order in which
156: * they have been added. Also files which would be created by a given
157: * operation are added to lists of paths returned by {@link
158: * #getModifiedPaths()} or {@link #getCreatedPaths()} immediately. @param
159: * operation operation to be added
160: */
161: public void add(Operation operation) {
162: operations.add(operation);
163: // XXX should always show isForEditing files at the top of the list, acc. to Jano
164: createdPaths.addAll(Arrays.asList(operation.getCreatedPaths()));
165: modifiedPaths.addAll(Arrays
166: .asList(operation.getModifiedPaths()));
167: invalidPaths.addAll(Arrays.asList(operation.getInvalidPaths()));
168: }
169:
170: /**
171: * Performs in turn {@link Operation#run()} on all operations added to this
172: * instance in order in which operations have been added.
173: */
174: public void run() throws IOException {
175: boolean oldAutosave = false;
176: if (layerHandle != null) {
177: oldAutosave = layerHandle.isAutosave();
178: layerHandle.setAutosave(false);
179: }
180: try {
181: for (Operation op : operations) {
182: op.run();
183: }
184: if (layerHandle != null) {
185: // XXX clumsy, see above
186: layerHandle.save();
187: }
188: } finally {
189: if (layerHandle != null) {
190: layerHandle.setAutosave(oldAutosave);
191: }
192: }
193: // XXX should get EditCookie/OpenCookie for created/modified files for which isForEditing
194: // XXX should return a Set<FileObject> of created/modified files for which isRelevant
195: }
196:
197: public String[] getCreatedPaths() {
198: if (createdPaths == null) {
199: return new String[0];
200: } else {
201: String[] s = new String[createdPaths.size()];
202: return createdPaths.toArray(s);
203: }
204: }
205:
206: public String[] getModifiedPaths() {
207: if (modifiedPaths == null) {
208: return new String[0];
209: } else {
210: String[] s = new String[modifiedPaths.size()];
211: return modifiedPaths.toArray(s);
212: }
213: }
214:
215: public String[] getInvalidPaths() {
216: if (invalidPaths == null) {
217: return new String[0];
218: } else {
219: String[] s = new String[invalidPaths.size()];
220: return invalidPaths.toArray(s);
221: }
222: }
223:
224: /**
225: * Convenience method to load a file template from the standard location.
226: * @param name a simple filename
227: * @return that file from the <code>Templates/NetBeansModuleDevelopment-files</code> layer folder
228: */
229: public static FileObject getTemplate(String name) {
230: FileObject f = Repository.getDefault().getDefaultFileSystem()
231: .findResource(
232: "Templates/NetBeansModuleDevelopment-files/"
233: + name);
234: assert f != null : name;
235: return f;
236: }
237:
238: /**
239: * Returns {@link Operation} for creating custom file in the project file
240: * hierarchy.
241: * @param path relative to a project directory where a file to be created
242: * @param content content for the file being created. Content may address
243: * either text or binary data.
244: */
245: public Operation createFile(String path, FileObject content) {
246: return CreatedModifiedFilesFactory.createFile(project, path,
247: content);
248: }
249:
250: /**
251: * Returns an {@link Operation} for creating custom file in the project
252: * file hierarchy with an option to replace <em>token</em>s from a given
253: * <code>content</code> with custom string. The result will be stored into
254: * a file representing by a given <code>path</code>.
255: *
256: * @param path relative to a project directory where a file to be created
257: * @param content content for the file being created
258: * @param tokens properties with values to be passed to FreeMarker
259: * (in addition to: name, nameAndExt, user, date, time, and project.license)
260: */
261: public Operation createFileWithSubstitutions(String path,
262: FileObject content, Map<String, String> tokens) {
263: if (tokens == null) {
264: throw new NullPointerException();
265: }
266: return CreatedModifiedFilesFactory.createFileWithSubstitutions(
267: project, path, content, tokens);
268: }
269:
270: /**
271: * Provides {@link Operation} that will add given <code>value</code> under
272: * a specified <code>key</code> into the custom <em>bundle</em> which is
273: * specified by the <code>bundlePath</code> parameter.
274: */
275: public Operation bundleKey(String bundlePath, String key,
276: String value) {
277: return CreatedModifiedFilesFactory.bundleKey(project, key,
278: value, bundlePath);
279: }
280:
281: /**
282: * Provides {@link Operation} that will add given <code>value</code> under
283: * a specified <code>key</code> into the project's default <em>localized
284: * bundle</em> which is specified in the project's <em>manifest</em>.
285: */
286: public Operation bundleKeyDefaultBundle(String key, String value) {
287: return CreatedModifiedFilesFactory.bundleKeyDefaultBundle(
288: project, key, value);
289: }
290:
291: /**
292: * Provides {@link Operation} that will create a new section in the
293: * project's <em>manifest</em> registering a given
294: * <code>dataLoaderClass</code>.
295: *
296: * <pre>
297: * Name: org/netbeans/modules/myprops/MyPropsLoader.class
298: * OpenIDE-Module-Class: Loader
299: * </pre>
300: *
301: * @param dataLoaderClass e.g. org/netbeans/modules/myprops/MyPropsLoader
302: * (<strong>without</strong> .class extension)
303: * @param installBefore content of Install-Before attribute, or null if not
304: * specified
305: */
306: public Operation addLoaderSection(String dataLoaderClass,
307: String installBefore) {
308: return CreatedModifiedFilesFactory.addLoaderSection(project,
309: dataLoaderClass, installBefore);
310: }
311:
312: /**
313: * Provides {@link Operation} that will register an <code>implClass</code>
314: * implementation of <code>interfaceClass</code> interface in the lookup.
315: * If a file representing <code>interfaceClass</code> service already
316: * exists in <em>META-INF/services</em> directory
317: * <code>implClass</code> will be appended to the end of the list of
318: * implementations. If it doesn't exist a new file will be created.
319: *
320: * @param interfaceClass e.g. org.example.spi.somemodule.ProvideMe
321: * @param implClass e.g. org.example.module1.ProvideMeImpl
322: * @param inTests if true, add to test/unit/src/META-INF/services/, else to src/META-INF/services/
323: */
324: public Operation addLookupRegistration(String interfaceClass,
325: String implClass, boolean inTests) {
326: return CreatedModifiedFilesFactory.addLookupRegistration(
327: project, interfaceClass, implClass, inTests);
328: }
329:
330: /**
331: * Add a dependency to a list of module dependencies of this project. This
332: * means editing of project's <em>nbproject/project.xml</em>. All
333: * parameters refers to a module this module will depend on. If a project
334: * already has a given dependency it will not be added.
335: *
336: * @param codeNameBase codename base
337: * @param releaseVersion release version, if <code>null</code> will be taken from the
338: * entry found in platform
339: * @param version specification version (see {@link SpecificationVersion}),
340: * if null will be taken from the entry found in platform
341: * @param useInCompiler do this module needs a module beeing added at a
342: * compile time?
343: */
344: public Operation addModuleDependency(String codeNameBase,
345: String releaseVersion, SpecificationVersion version,
346: boolean useInCompiler) {
347: return CreatedModifiedFilesFactory.addModuleDependency(project,
348: codeNameBase, releaseVersion, version, useInCompiler);
349: }
350:
351: /**
352: * Delegates to {@link #addModuleDependency(String, String,
353: * SpecificationVersion, boolean)} passing a given code name base,
354: * <code>null</code> as release version, <code>null</code> as version and
355: * <code>true</code> as useInCompiler arguments.
356: */
357: public CreatedModifiedFiles.Operation addModuleDependency(
358: String codeNameBase) {
359: return addModuleDependency(codeNameBase, null, null, true);
360: }
361:
362: /**
363: * Creates an entry (<em>file</em> element) in the project's layer. Also
364: * may create and/or modify other files as it is needed.
365: *
366: * @param layerPath path in a project's layer. Folders which don't exist
367: * yet will be created. (e.g.
368: * <em>Menu/Tools/org-example-module1-BeepAction.instance</em>).
369: * @param content became content of a file, or null
370: * @param substitutionTokens see {@link #createFileWithSubstitutions} for details;
371: * may be <code>null</code> to not use FreeMarker
372: * @param localizedDisplayName if it is not a <code>null</code>
373: * <em>SystemFileSystem.localizingBundle</em> attribute will be
374: * created with the stringvalue to a default bundle (from manifest).
375: * Also an appropriate entry will be added into the bundle.
376: * @param fileAttributes <String,Object> map. key in the map is the
377: * name of the file attribute value is the actual value, currently
378: * supported types are Boolean and String Generates
379: * <pre>
380: * <attr name="KEY" stringvalue="VALUE"/> or <attr name="KEY" booleanvalue="VALUE"/>
381: * </pre>
382: * @return see {@link Operation}
383: */
384: public Operation createLayerEntry(String layerPath,
385: FileObject content, Map<String, String> substitutionTokens,
386: String localizedDisplayName,
387: Map<String, Object> fileAttributes) {
388: return CreatedModifiedFilesFactory.createLayerEntry(this ,
389: project, layerPath, content, substitutionTokens,
390: localizedDisplayName, fileAttributes);
391: }
392:
393: /**
394: * Adds new attributes into manifest file.
395: * @param section the name of the section or <code>null</code> for the main section.
396: * @param attributes <String,String> map mapping attributes names and values.
397: * @return see {@link Operation}
398: */
399: public Operation manifestModification(String section,
400: Map<String, String> attributes) {
401: return CreatedModifiedFilesFactory.manifestModification(
402: project, section, attributes);
403: }
404:
405: /**
406: * Adds new properties into property file.
407: * @param propertyPath path representing properties file relative to a project directory where all
408: * properties will be put in. If such a file does not exist it is created.
409: * @param properties <String,String> map mapping properties names and values.
410: * @return see {@link Operation}
411: */
412: public Operation propertiesModification(String propertyPath,
413: Map<String, String> properties) {
414: return CreatedModifiedFilesFactory.propertiesModification(
415: project, propertyPath, properties);
416: }
417:
418: /**
419: * Creates a new arbitrary <em><attr></em> element.
420: *
421: * @param parentPath path to a <em>file</em> or a <em>folder</em> in a
422: * project's layer. It <strong>must</strong> exist.
423: * @param attrName value of the name attribute of the <em><attr></em>
424: * element.
425: * @param attrValue value of the attribute (may specially be a string prefixed with "newvalue:" or "methodvalue:")
426: * @return see {@link Operation}
427: */
428: public CreatedModifiedFiles.Operation createLayerAttribute(
429: final String parentPath, final String attrName,
430: final Object attrValue) {
431: return layerModifications(new LayerOperation() {
432: public void run(FileSystem layer) throws IOException {
433: FileObject f = layer.findResource(parentPath);
434: if (f == null) {
435: // XXX sometimes this happens when it should not, during unit tests... why?
436: /*
437: try {
438: // For debugging:
439: getLayerHandle().save();
440: } catch (IOException e) {
441: e.printStackTrace();
442: }
443: */
444: throw new IOException(parentPath);
445: }
446: f.setAttribute(attrName, attrValue);
447: }
448: }, Collections.<String> emptySet());
449: }
450:
451: /**
452: * Order a new entry in a project layer between two others.
453: *
454: * @param layerPath folder path in a project's layer. (e.g. <em>Loaders/text/x-java/Actions</em>).
455: * @param precedingItemName item to be before <em>newItemName</em> (may be null)
456: * @param newItemName the new item (must already exist!)
457: * @param followingItemName item to be after <em>newItemName</em> (may be null)
458: */
459: public Operation orderLayerEntry(final String layerPath,
460: final String precedingItemName, final String newItemName,
461: final String followingItemName) {
462: return layerModifications(new LayerOperation() {
463: public void run(FileSystem layer) throws IOException {
464: FileObject f = layer.findResource(layerPath);
465: if (f == null) {
466: throw new IOException("No such folder " + layerPath);
467: }
468: FileObject merged = LayerUtils
469: .getEffectiveSystemFilesystem(project)
470: .findResource(layerPath);
471: assert merged != null : layerPath;
472: Integer beforePos = getPosition(merged,
473: precedingItemName);
474: Integer afterPos = getPosition(merged,
475: followingItemName);
476: if (beforePos != null && afterPos != null) {
477: // won't work well if afterPos == beforePos + 1, but oh well
478: f.getFileObject(newItemName).setAttribute(
479: "position", (beforePos + afterPos) / 2); // NOI18N
480: } else if (beforePos != null) {
481: f.getFileObject(newItemName).setAttribute(
482: "position", beforePos + 100); // NOI18N
483: } else if (afterPos != null) {
484: f.getFileObject(newItemName).setAttribute(
485: "position", afterPos - 100); // NOI18N
486: } else {
487: // Fallback esp. for old platforms.
488: if (precedingItemName != null) {
489: f.setAttribute(precedingItemName + '/'
490: + newItemName, true);
491: }
492: if (followingItemName != null) {
493: f.setAttribute(newItemName + '/'
494: + followingItemName, true);
495: }
496: }
497: }
498:
499: private Integer getPosition(FileObject folder, String name) {
500: if (name == null) {
501: return null;
502: }
503: FileObject f = folder.getFileObject(name);
504: if (f == null) {
505: return null;
506: }
507: Object pos = f.getAttribute("position"); // NOI18N
508: // ignore floats for now...
509: return pos instanceof Integer ? (Integer) pos : null;
510: }
511: }, Collections.<String> emptySet());
512: }
513:
514: /**
515: * Make structural modifications to the project's XML layer.
516: * The operations may be expressed as filesystem calls.
517: * @param op a callback for the actual changes to make
518: * @param externalFiles a list of <em>simple filenames</em> of new data files which
519: * are to be created in the layer and which will therefore appear
520: * on disk alongside the layer, usually with the same names (unless
521: * they conflict with existing files); you still need to create them
522: * yourself using e.g. {@link FileObject#createData} and {@link FileObject#getOutputStream}
523: * @return the operation handle
524: */
525: public Operation layerModifications(final LayerOperation op,
526: final Set<String> externalFiles) {
527: return CreatedModifiedFilesFactory.layerModifications(project,
528: op, externalFiles, this );
529: }
530:
531: /**
532: * Callback for modifying the project's XML layer.
533: * @see #layerModifications
534: */
535: public interface LayerOperation {
536:
537: /**
538: * Actually change the layer.
539: * @param layer the layer to make changes to using Filesystems API calls
540: * @throws IOException if the changes fail somehow
541: */
542: void run(FileSystem layer) throws IOException;
543:
544: }
545:
546: }
|