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.versioning.system.cvss.util;
043:
044: import java.awt.Dialog;
045: import java.awt.Frame;
046: import java.awt.KeyboardFocusManager;
047: import java.awt.Window;
048: import java.io.*;
049: import java.util.*;
050: import java.util.regex.Pattern;
051:
052: import org.netbeans.api.project.FileOwnerQuery;
053: import org.netbeans.api.project.Project;
054: import org.netbeans.api.project.ProjectUtils;
055: import org.netbeans.api.project.SourceGroup;
056: import org.netbeans.api.project.Sources;
057: import org.netbeans.api.queries.SharabilityQuery;
058: import org.netbeans.lib.cvsclient.admin.Entry;
059: import org.netbeans.lib.cvsclient.command.log.LogInformation;
060: import org.netbeans.modules.versioning.system.cvss.CvsVersioningSystem;
061: import org.netbeans.modules.versioning.system.cvss.FileInformation;
062: import org.netbeans.modules.versioning.system.cvss.FileStatusCache;
063: import org.netbeans.modules.versioning.system.cvss.ClientRuntime;
064: import org.netbeans.modules.versioning.spi.VCSContext;
065: import org.openide.filesystems.FileObject;
066: import org.openide.filesystems.FileUtil;
067: import org.openide.nodes.Node;
068: import org.openide.util.Lookup;
069: import org.openide.util.NbBundle;
070: import org.openide.windows.TopComponent;
071: import org.openide.windows.WindowManager;
072:
073: /**
074: * Provides static utility methods for CVS module.
075: *
076: * @author Maros Sandor
077: */
078: public class Utils {
079:
080: private static final Pattern metadataPattern = Pattern
081: .compile(".*\\" + File.separatorChar + "CVS(\\"
082: + File.separatorChar + ".*|$)");
083:
084: private static final FileFilter cvsFileFilter = new FileFilter() {
085: public boolean accept(File pathname) {
086: if (CvsVersioningSystem.FILENAME_CVS.equals(pathname
087: .getName()))
088: return false;
089: if (CvsVersioningSystem.FILENAME_CVSIGNORE.equals(pathname
090: .getName()))
091: return true;
092: return SharabilityQuery.getSharability(pathname) != SharabilityQuery.NOT_SHARABLE;
093: }
094: };
095:
096: /**
097: * Semantics is similar to {@link org.openide.windows.TopComponent#getActivatedNodes()} except that this
098: * method returns File objects instead od Nodes. Every node is examined for Files it represents. File and Folder
099: * nodes represent their underlying files or folders. Project nodes are represented by their source groups. Other
100: * logical nodes must provide FileObjects in their Lookup.
101: *
102: * @return File [] array of activated files
103: * @param nodes or null (then taken from windowsystem, it may be wrong on editor tabs #66700).
104: */
105: public static Context getCurrentContext(Node[] nodes) {
106: if (nodes == null) {
107: nodes = TopComponent.getRegistry().getActivatedNodes();
108: }
109: VCSContext ctx = VCSContext.forNodes(nodes);
110: return new Context(
111: new HashSet(ctx.computeFiles(cvsFileFilter)),
112: new HashSet(ctx.getRootFiles()), new HashSet(ctx
113: .getExclusions()));
114: }
115:
116: /**
117: * Semantics is similar to {@link org.openide.windows.TopComponent#getActivatedNodes()} except that this
118: * method returns File objects instead od Nodes. Every node is examined for Files it represents. File and Folder
119: * nodes represent their underlying files or folders. Project nodes are represented by their source groups. Other
120: * logical nodes must provide FileObjects in their Lookup.
121: *
122: * @param nodes null (then taken from windowsystem, it may be wrong on editor tabs #66700).
123: * @param includingFileStatus if any activated file does not have this CVS status, an empty array is returned
124: * @param includingFolderStatus if any activated folder does not have this CVS status, an empty array is returned
125: * @return File [] array of activated files, or an empty array if any of examined files/folders does not have given status
126: */
127: public static Context getCurrentContext(Node[] nodes,
128: int includingFileStatus, int includingFolderStatus) {
129: Context context = getCurrentContext(nodes);
130: FileStatusCache cache = CvsVersioningSystem.getInstance()
131: .getStatusCache();
132: File[] files = context.getRootFiles();
133: for (int i = 0; i < files.length; i++) {
134: File file = files[i];
135: FileInformation fi = cache.getStatus(file);
136: if (file.isDirectory()) {
137: if ((fi.getStatus() & includingFolderStatus) == 0)
138: return Context.Empty;
139: } else {
140: if ((fi.getStatus() & includingFileStatus) == 0)
141: return Context.Empty;
142: }
143: }
144: // if there are no exclusions, we may safely return this context because filtered files == root files
145: if (context.getExclusions().size() == 0)
146: return context;
147:
148: // in this code we remove files from filteredFiles to NOT include any files that do not have required status
149: // consider a freeform project that has 'build' in filteredFiles, the Branch action would try to branch it
150: // so, it is OK to have BranchAction enabled but the context must be a bit adjusted here
151: Set<File> filteredFiles = new HashSet<File>(Arrays
152: .asList(context.getFiles()));
153: Set<File> rootFiles = new HashSet<File>(Arrays.asList(context
154: .getRootFiles()));
155: Set<File> rootFileExclusions = new HashSet<File>(context
156: .getExclusions());
157:
158: for (Iterator<File> i = filteredFiles.iterator(); i.hasNext();) {
159: File file = i.next();
160: if (file.isDirectory()) {
161: if ((cache.getStatus(file).getStatus() & includingFolderStatus) == 0)
162: i.remove();
163: } else {
164: if ((cache.getStatus(file).getStatus() & includingFileStatus) == 0)
165: i.remove();
166: }
167: }
168: return new Context(filteredFiles, rootFiles, rootFileExclusions);
169: }
170:
171: /**
172: * @return <code>true</code> if
173: * <ul>
174: * <li> the node contains a project in its lookup and
175: * <li> the project contains at least one CVS versioned source group
176: * </ul>
177: * otherwise <code>false</code>.
178: */
179: public static boolean isVersionedProject(Node node) {
180: Lookup lookup = node.getLookup();
181: Project project = lookup.lookup(Project.class);
182: return isVersionedProject(project);
183: }
184:
185: /**
186: * @return <code>true</code> if
187: * <ul>
188: * <li> the project != null and
189: * <li> the project contains at least one CVS versioned source group
190: * </ul>
191: * otherwise <code>false</code>.
192: */
193: public static boolean isVersionedProject(Project project) {
194: if (project != null) {
195: FileStatusCache cache = CvsVersioningSystem.getInstance()
196: .getStatusCache();
197: Sources sources = ProjectUtils.getSources(project);
198: SourceGroup[] sourceGroups = sources
199: .getSourceGroups(Sources.TYPE_GENERIC);
200: for (int j = 0; j < sourceGroups.length; j++) {
201: SourceGroup sourceGroup = sourceGroups[j];
202: File f = FileUtil.toFile(sourceGroup.getRootFolder());
203: if (f != null) {
204: if ((cache.getStatus(f).getStatus() & FileInformation.STATUS_MANAGED) != 0)
205: return true;
206: }
207: }
208: }
209: return false;
210: }
211:
212: /**
213: * Determines all files and folders that belong to a given project and adds them to the supplied Collection.
214: *
215: * @param filteredFiles destination collection of Files
216: * @param project project to examine
217: */
218: public static void addProjectFiles(Collection filteredFiles,
219: Collection rootFiles, Collection rootFilesExclusions,
220: Project project) {
221: FileStatusCache cache = CvsVersioningSystem.getInstance()
222: .getStatusCache();
223: Sources sources = ProjectUtils.getSources(project);
224: SourceGroup[] sourceGroups = sources
225: .getSourceGroups(Sources.TYPE_GENERIC);
226: for (int j = 0; j < sourceGroups.length; j++) {
227: SourceGroup sourceGroup = sourceGroups[j];
228: FileObject srcRootFo = sourceGroup.getRootFolder();
229: File rootFile = FileUtil.toFile(srcRootFo);
230: try {
231: getCVSRootFor(rootFile);
232: } catch (IOException e) {
233: // the folder is not under a versioned root
234: continue;
235: }
236: rootFiles.add(rootFile);
237: boolean containsSubprojects = false;
238: FileObject[] rootChildren = srcRootFo.getChildren();
239: Set projectFiles = new HashSet(rootChildren.length);
240: for (int i = 0; i < rootChildren.length; i++) {
241: FileObject rootChildFo = rootChildren[i];
242: if (CvsVersioningSystem.FILENAME_CVS.equals(rootChildFo
243: .getNameExt()))
244: continue;
245: File child = FileUtil.toFile(rootChildFo);
246: // #67900 Added special treatment for .cvsignore files
247: if (sourceGroup.contains(rootChildFo)
248: || CvsVersioningSystem.FILENAME_CVSIGNORE
249: .equals(rootChildFo.getNameExt())) {
250: // TODO: #60516 deep scan is required here but not performed due to performace reasons
251: projectFiles.add(child);
252: } else {
253: int status = cache.getStatus(child).getStatus();
254: if (status != FileInformation.STATUS_NOTVERSIONED_EXCLUDED) {
255: rootFilesExclusions.add(child);
256: containsSubprojects = true;
257: }
258: }
259: }
260: if (containsSubprojects) {
261: filteredFiles.addAll(projectFiles);
262: } else {
263: filteredFiles.add(rootFile);
264: }
265: }
266: }
267:
268: /**
269: * May take a long time for many projects, consider making the call from worker threads.
270: *
271: * @param projects projects to examine
272: * @return Context context that defines list of supplied projects
273: */
274: public static Context getProjectsContext(Project[] projects) {
275: Set filtered = new HashSet();
276: Set roots = new HashSet();
277: Set exclusions = new HashSet();
278: for (int i = 0; i < projects.length; i++) {
279: addProjectFiles(filtered, roots, exclusions, projects[i]);
280: }
281: return new Context(filtered, roots, exclusions);
282: }
283:
284: /**
285: * Returns the widest possible versioned context for the given file, the outter boundary is the file's Project.
286: *
287: * @param file a file
288: * @return Context a context
289: */
290: public static Context getProjectContext(Project project, File file) {
291: Context context = Utils
292: .getProjectsContext(new Project[] { project });
293: if (context.getRootFiles().length == 0) {
294: // the project itself is not versioned, try to search in the broadest context possible
295: FileStatusCache cache = CvsVersioningSystem.getInstance()
296: .getStatusCache();
297: for (;;) {
298: File parent = file.getParentFile();
299: assert parent != null;
300: if ((cache.getStatus(parent).getStatus() & FileInformation.STATUS_IN_REPOSITORY) == 0) {
301: Set<File> files = new HashSet<File>(1);
302: files.add(file);
303: context = new Context(files, files, Collections
304: .emptySet());
305: break;
306: }
307: file = parent;
308: }
309: }
310: return context;
311: }
312:
313: public static File[] toFileArray(Collection fileObjects) {
314: Set files = new HashSet(fileObjects.size() * 4 / 3 + 1);
315: for (Iterator i = fileObjects.iterator(); i.hasNext();) {
316: files.add(FileUtil.toFile((FileObject) i.next()));
317: }
318: files.remove(null);
319: return (File[]) files.toArray(new File[files.size()]);
320: }
321:
322: /**
323: * Determines CVS repository root for the given file. It does that by reading the CVS/Root file from
324: * its parent directory, its parent and so on until CVS/Root is found.
325: *
326: * @param file the file in question
327: * @return CVS root for the given file
328: * @throws IOException if CVS/Root file is unreadable
329: */
330: public static String getCVSRootFor(File file) throws IOException {
331: if (file.isFile())
332: file = file.getParentFile();
333: for (; file != null; file = file.getParentFile()) {
334: File rootFile = new File(file, "CVS/Root"); // NOI18N
335: BufferedReader br = null;
336: try {
337: br = new BufferedReader(new FileReader(rootFile));
338: return br.readLine();
339: } catch (FileNotFoundException e) {
340: continue;
341: } finally {
342: if (br != null)
343: br.close();
344: }
345: }
346: throw new IOException("CVS/Root not found"); // NOI18N
347: }
348:
349: public static Window getCurrentWindow() {
350: Window wnd = KeyboardFocusManager
351: .getCurrentKeyboardFocusManager().getActiveWindow();
352: if (wnd instanceof Dialog || wnd instanceof Frame) {
353: return wnd;
354: } else {
355: return WindowManager.getDefault().getMainWindow();
356: }
357: }
358:
359: /**
360: * Tests parent/child relationship of files.
361: *
362: * @param parent file to be parent of the second parameter
363: * @param file file to be a child of the first parameter
364: * @return true if the second parameter represents the same file as the first parameter OR is its descendant (child)
365: */
366: public static boolean isParentOrEqual(File parent, File file) {
367: for (; file != null; file = file.getParentFile()) {
368: if (file.equals(parent))
369: return true;
370: }
371: return false;
372: }
373:
374: /**
375: * Computes path of this file to repository root.
376: *
377: * @param file a file
378: * @return String path of this file in repsitory. If this path does not describe a
379: * versioned file, this method returns an empty string
380: */
381: public static String getRelativePath(File file) {
382: String postfix = "";
383: for (file = file.getParentFile(); file != null
384: && !file.exists(); file = file.getParentFile()) {
385: postfix = "/" + file.getName() + postfix;
386: }
387: if (file == null)
388: return "";
389: try {
390: return CvsVersioningSystem.getInstance().getAdminHandler()
391: .getRepositoryForDirectory(file.getAbsolutePath(),
392: "").substring(1)
393: + postfix; // NOI18N
394: } catch (IOException e) {
395: return ""; // NOI18N
396: }
397: }
398:
399: /**
400: * Determines the sticky information for a given file. If the file is new then it
401: * returns its parent directory's sticky info, if any.
402: *
403: * @param file file to examine
404: * @return String sticky information for a file (without leading N, D or T specifier) or null
405: */
406: public static String getSticky(File file) {
407: if (file == null)
408: return null;
409: FileInformation info = CvsVersioningSystem.getInstance()
410: .getStatusCache().getStatus(file);
411: if (info.getStatus() == FileInformation.STATUS_NOTVERSIONED_NEWLOCALLY) {
412: return getSticky(file.getParentFile());
413: } else if (info.getStatus() == FileInformation.STATUS_NOTVERSIONED_EXCLUDED) {
414: return null;
415: }
416: if (file.isDirectory()) {
417: String std = CvsVersioningSystem.getInstance()
418: .getAdminHandler().getStickyTagForDirectory(file);
419: if (std != null) {
420: std = std.substring(1);
421: }
422: return std;
423: }
424: Entry entry = info.getEntry(file);
425: if (entry != null) {
426: String stickyInfo = null;
427: if (entry.getTag() != null)
428: stickyInfo = entry.getTag(); // NOI18N
429: else if (entry.getDate() != null)
430: stickyInfo = entry.getDateFormatted(); // NOI18N
431: return stickyInfo;
432: }
433: return null;
434: }
435:
436: /**
437: * Computes previous revision or <code>null</code>
438: * for initial.
439: *
440: * @param revision num.dot revision or <code>null</code>
441: */
442: public static String previousRevision(String revision) {
443: if (revision == null)
444: return null;
445: String[] nums = revision.split("\\."); // NOI18N
446: assert (nums.length % 2) == 0 : "File revisions must consist from even tokens: "
447: + revision; // NOI18N
448:
449: // eliminate branches
450: int lastIndex = nums.length - 1;
451: boolean cutoff = false;
452: while (lastIndex > 1 && "1".equals(nums[lastIndex])) { // NOI18N
453: lastIndex -= 2;
454: cutoff = true;
455: }
456: if (lastIndex <= 0) {
457: return null;
458: } else if (lastIndex == 1 && "1".equals(nums[lastIndex])) { // NOI18N
459: return null;
460: } else {
461: int rev = Integer.parseInt(nums[lastIndex]);
462: if (!cutoff)
463: rev--;
464: StringBuffer sb = new StringBuffer(nums[0]);
465: for (int i = 1; i < lastIndex; i++) {
466: sb.append('.').append(nums[i]); // NOI18N
467: }
468: sb.append('.').append(rev); // NOI18N
469: return sb.toString();
470: }
471: }
472:
473: /**
474: * Determines parent project for a file.
475: *
476: * @param file file to examine
477: * @return Project owner of the file or null if the file does not belong to a project
478: */
479: public static Project getProject(File file) {
480: if (file == null)
481: return null;
482: FileObject fo = FileUtil.toFileObject(file);
483: if (fo == null)
484: return getProject(file.getParentFile());
485: return FileOwnerQuery.getOwner(fo);
486: }
487:
488: public static String createBranchRevisionNumber(String branchNumber) {
489: StringBuilder sb = new StringBuilder();
490: int idx = branchNumber.lastIndexOf('.'); // NOI18N
491: sb.append(branchNumber.substring(0, idx));
492: sb.append(".0"); // NOI18N
493: sb.append(branchNumber.substring(idx));
494: return sb.toString();
495: }
496:
497: public static String formatBranches(
498: LogInformation.Revision revision,
499: boolean useNumbersIfNamesNotAvailable) {
500: String branches = revision.getBranches();
501: if (branches == null)
502: return ""; // NOI18N
503:
504: boolean branchNamesAvailable = true;
505: StringBuilder branchNames = new StringBuilder();
506: StringTokenizer st = new StringTokenizer(branches, ";"); // NOI18N
507: while (st.hasMoreTokens()) {
508: String branchNumber = st.nextToken().trim();
509: List<LogInformation.SymName> names = revision
510: .getLogInfoHeader().getSymNamesForRevision(
511: createBranchRevisionNumber(branchNumber));
512: if (names.size() > 0) {
513: branchNames.append(names.get(0).getName());
514: } else {
515: branchNamesAvailable = false;
516: if (useNumbersIfNamesNotAvailable) {
517: branchNames.append(branchNumber);
518: } else {
519: break;
520: }
521: }
522: branchNames.append("; "); // NOI18N
523: }
524: if (branchNamesAvailable || useNumbersIfNamesNotAvailable) {
525: branchNames.delete(branchNames.length() - 2, branchNames
526: .length());
527: } else {
528: branchNames.delete(0, branchNames.length());
529: }
530: return branchNames.toString();
531: }
532:
533: public static boolean containsMetadata(File folder) {
534: File repository = new File(folder,
535: CvsVersioningSystem.FILENAME_CVS_REPOSITORY);
536: File entries = new File(folder,
537: CvsVersioningSystem.FILENAME_CVS_ENTRIES);
538: return repository.canRead() && entries.canRead();
539: }
540:
541: /**
542: * Compares two {@link FileInformation} objects by importance of statuses they represent.
543: */
544: public static class ByImportanceComparator implements Comparator {
545: public int compare(Object o1, Object o2) {
546: FileInformation i1 = (FileInformation) o1;
547: FileInformation i2 = (FileInformation) o2;
548: return getComparableStatus(i1.getStatus())
549: - getComparableStatus(i2.getStatus());
550: }
551: }
552:
553: /**
554: * Gets integer status that can be used in comparators. The more important the status is for the user,
555: * the lower value it has. Conflict is 0, unknown status is 100.
556: *
557: * @return status constant suitable for 'by importance' comparators
558: */
559: public static int getComparableStatus(int status) {
560: switch (status) {
561: case FileInformation.STATUS_VERSIONED_CONFLICT:
562: return 0;
563: case FileInformation.STATUS_VERSIONED_MERGE:
564: return 1;
565: case FileInformation.STATUS_VERSIONED_DELETEDLOCALLY:
566: return 10;
567: case FileInformation.STATUS_VERSIONED_REMOVEDLOCALLY:
568: return 11;
569: case FileInformation.STATUS_NOTVERSIONED_NEWLOCALLY:
570: return 12;
571: case FileInformation.STATUS_VERSIONED_ADDEDLOCALLY:
572: return 13;
573: case FileInformation.STATUS_VERSIONED_MODIFIEDLOCALLY:
574: return 14;
575: case FileInformation.STATUS_VERSIONED_REMOVEDINREPOSITORY:
576: return 30;
577: case FileInformation.STATUS_VERSIONED_NEWINREPOSITORY:
578: return 31;
579: case FileInformation.STATUS_VERSIONED_MODIFIEDINREPOSITORY:
580: return 32;
581: case FileInformation.STATUS_VERSIONED_UPTODATE:
582: return 50;
583: case FileInformation.STATUS_NOTVERSIONED_EXCLUDED:
584: return 100;
585: case FileInformation.STATUS_NOTVERSIONED_NOTMANAGED:
586: return 101;
587: case FileInformation.STATUS_UNKNOWN:
588: return 102;
589: default:
590: throw new IllegalArgumentException("Unknown status: "
591: + status); // NOI18N
592: }
593: }
594:
595: public static boolean isPartOfCVSMetadata(File file) {
596: return metadataPattern.matcher(file.getAbsolutePath())
597: .matches();
598: }
599:
600: /** Like mkdirs but but using openide filesystems (firing events) */
601: public static FileObject mkfolders(File file) throws IOException {
602: if (file.isDirectory())
603: return FileUtil.toFileObject(file);
604:
605: File parent = file.getParentFile();
606:
607: String path = file.getName();
608: while (parent.isDirectory() == false) {
609: path = parent.getName() + "/" + path; // NOI18N
610: parent = parent.getParentFile();
611: }
612:
613: FileObject fo = FileUtil.toFileObject(parent);
614: return FileUtil.createFolder(fo, path);
615: }
616:
617: /**
618: * 1) A tag must start with a letter
619: * 2) A tag must not contain characters: $,.:;@ SPACE TAB NEWLINE
620: * 3) Reserved tag names: HEAD BASE
621: *
622: * @param name
623: * @return true if the name of the tag is valid, false otherwise
624: */
625: public static boolean isTagValid(String name) {
626: if (name == null || name.length() == 0)
627: return false;
628: char c = name.charAt(0);
629: if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z'))
630: return false;
631: for (int i = 1; i < name.length(); i++) {
632: c = name.charAt(i);
633: if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z')
634: && (c < '0' || c > '9') && c != '-' && c != '_')
635: return false;
636: }
637: if (name.equals("HEAD") || name.equals("BASE"))
638: return false;
639: return true;
640: }
641: }
|