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.subversion;
043:
044: import org.netbeans.modules.subversion.config.SvnConfigFiles;
045: import org.netbeans.modules.subversion.util.Context;
046: import org.netbeans.modules.subversion.client.*;
047: import org.netbeans.modules.subversion.util.SvnUtils;
048: import org.tigris.subversion.svnclientadapter.*;
049: import org.openide.util.RequestProcessor;
050: import java.io.*;
051: import java.util.*;
052: import java.util.logging.Logger;
053: import java.util.logging.Level;
054: import java.beans.PropertyChangeListener;
055: import java.beans.PropertyChangeSupport;
056: import org.netbeans.modules.subversion.ui.diff.Setup;
057: import org.netbeans.modules.subversion.ui.ignore.IgnoreAction;
058: import org.netbeans.modules.versioning.spi.VCSInterceptor;
059: import org.netbeans.modules.versioning.spi.VersioningSupport;
060: import org.netbeans.api.queries.SharabilityQuery;
061: import org.netbeans.modules.subversion.ui.repository.RepositoryConnection;
062:
063: /**
064: * A singleton Subversion manager class, center of Subversion module. Use {@link #getInstance()} to get access
065: * to Subversion module functionality.
066: *
067: * @author Maros Sandor
068: */
069: public class Subversion {
070:
071: /**
072: * Fired when textual annotations and badges have changed. The NEW value is Set<File> of files that changed or NULL
073: * if all annotaions changed.
074: */
075: static final String PROP_ANNOTATIONS_CHANGED = "annotationsChanged";
076:
077: static final String PROP_VERSIONED_FILES_CHANGED = "versionedFilesChanged";
078:
079: static final String INVALID_METADATA_MARKER = "invalid-metadata"; // NOI18N
080:
081: private static final int STATUS_DIFFABLE = FileInformation.STATUS_VERSIONED_UPTODATE
082: | FileInformation.STATUS_VERSIONED_MODIFIEDLOCALLY
083: | FileInformation.STATUS_VERSIONED_MODIFIEDINREPOSITORY
084: | FileInformation.STATUS_VERSIONED_CONFLICT
085: | FileInformation.STATUS_VERSIONED_MERGE
086: | FileInformation.STATUS_VERSIONED_REMOVEDINREPOSITORY
087: | FileInformation.STATUS_VERSIONED_MODIFIEDINREPOSITORY
088: | FileInformation.STATUS_VERSIONED_MODIFIEDINREPOSITORY;
089:
090: private static Subversion instance;
091:
092: private FileStatusCache fileStatusCache;
093: private FilesystemHandler filesystemHandler;
094: private FileStatusProvider fileStatusProvider;
095: private Annotator annotator;
096: private HashMap<String, RequestProcessor> processorsToUrl;
097:
098: private SvnClient noUrlClientWithoutListeners;
099: private SvnClient noUrlClientWithListeners;
100: private List<ISVNNotifyListener> svnNotifyListeners;
101:
102: private final PropertyChangeSupport support = new PropertyChangeSupport(
103: this );
104:
105: public static final Logger LOG = Logger
106: .getLogger("org.netbeans.modules.subversion");
107:
108: public static synchronized Subversion getInstance() {
109: if (instance == null) {
110: instance = new Subversion();
111: instance.init();
112: }
113: return instance;
114: }
115:
116: private Subversion() {
117: }
118:
119: private void init() {
120: loadIniParserClassesWorkaround();
121: SvnClientFactory.init();
122:
123: fileStatusCache = new FileStatusCache();
124: annotator = new Annotator(this );
125: fileStatusProvider = new FileStatusProvider();
126: filesystemHandler = new FilesystemHandler(this );
127: cleanup();
128: }
129:
130: /**
131: * Ini4j uses context classloader to load classes, use this as a workaround.
132: */
133: private void loadIniParserClassesWorkaround() {
134: ClassLoader cl = Thread.currentThread().getContextClassLoader();
135: Thread.currentThread().setContextClassLoader(
136: this .getClass().getClassLoader());
137: try {
138: SvnConfigFiles.getInstance(); // triggers ini4j initialization
139: } finally {
140: Thread.currentThread().setContextClassLoader(cl);
141: }
142: }
143:
144: private void cleanup() {
145: getRequestProcessor().post(new Runnable() {
146: public void run() {
147: try {
148: LOG.fine("Cleaning up cache"); // NOI18N
149: fileStatusCache.cleanUp();
150: // TODO: refresh all annotations
151: } finally {
152: Subversion.LOG.fine("END Cleaning up cache"); // NOI18N
153: }
154: }
155: }, 3000);
156: }
157:
158: public void shutdown() {
159: fileStatusProvider.shutdown();
160: // TODO: refresh all annotations
161: }
162:
163: public SvnFileNode[] getNodes(Context context, int includeStatus) {
164: File[] files = fileStatusCache
165: .listFiles(context, includeStatus);
166: SvnFileNode[] nodes = new SvnFileNode[files.length];
167: for (int i = 0; i < files.length; i++) {
168: nodes[i] = new SvnFileNode(files[i]);
169: }
170: return nodes;
171: }
172:
173: /**
174: * Tests <tt>.svn</tt> directory itself.
175: */
176: public boolean isAdministrative(File file) {
177: String name = file.getName();
178: boolean administrative = isAdministrative(name);
179: return (administrative && !file.exists())
180: || (administrative && file.exists() && file
181: .isDirectory()); // lets suppose it's administrative if file doesnt exist
182: }
183:
184: public boolean isAdministrative(String fileName) {
185: return fileName.equals(".svn") || fileName.equals("_svn"); // NOI18N
186: }
187:
188: public FileStatusCache getStatusCache() {
189: return fileStatusCache;
190: }
191:
192: public Annotator getAnnotator() {
193: return annotator;
194: }
195:
196: public boolean checkClientAvailable() {
197: try {
198: SvnClientFactory.checkClientAvailable();
199: } catch (SVNClientException ex) {
200: SvnClientExceptionHandler.notifyException(ex, true, true);
201: return false;
202: }
203: return true;
204: }
205:
206: public SvnClient getClient(SVNUrl repositoryUrl, String username,
207: String password) throws SVNClientException {
208: return getClient(repositoryUrl, username, password,
209: SvnClientExceptionHandler.EX_DEFAULT_HANDLED_EXCEPTIONS);
210: }
211:
212: public SvnClient getClient(SVNUrl repositoryUrl, String username,
213: String password, int handledExceptions)
214: throws SVNClientException {
215: SvnClient client = SvnClientFactory.getInstance()
216: .createSvnClient(repositoryUrl, null, username,
217: password, handledExceptions);
218: attachListeners(client);
219: return client;
220: }
221:
222: public SvnClient getClient(SVNUrl repositoryUrl,
223: SvnProgressSupport support) throws SVNClientException {
224: String username = ""; // NOI18N
225: String password = ""; // NOI18N
226: RepositoryConnection rc = SvnModuleConfig.getDefault()
227: .getRepositoryConnection(repositoryUrl.toString());
228: if (rc != null) {
229: username = rc.getUsername();
230: password = rc.getPassword();
231: }
232: SvnClient client = SvnClientFactory
233: .getInstance()
234: .createSvnClient(
235: repositoryUrl,
236: support, /*null, */
237: username,
238: password,
239: SvnClientExceptionHandler.EX_DEFAULT_HANDLED_EXCEPTIONS);
240: attachListeners(client);
241: return client;
242: }
243:
244: public SvnClient getClient(File file) throws SVNClientException {
245: return getClient(file, null);
246: }
247:
248: public SvnClient getClient(File file, SvnProgressSupport support)
249: throws SVNClientException {
250: SVNUrl repositoryUrl = SvnUtils.getRepositoryRootUrl(file);
251: assert repositoryUrl != null : "Unable to get repository: "
252: + file.getAbsolutePath() + " is probably unmanaged."; // NOI18N
253:
254: return getClient(repositoryUrl, support);
255: }
256:
257: public SvnClient getClient(Context ctx, SvnProgressSupport support)
258: throws SVNClientException {
259: File[] roots = ctx.getRootFiles();
260: SVNUrl repositoryUrl = null;
261: for (int i = 0; i < roots.length; i++) {
262: repositoryUrl = SvnUtils.getRepositoryRootUrl(roots[0]);
263: if (repositoryUrl != null) {
264: break;
265: }
266: }
267:
268: assert repositoryUrl != null : "Unable to get repository, context contains only unmanaged files!"; // NOI18N
269:
270: return getClient(repositoryUrl, support);
271: }
272:
273: public SvnClient getClient(SVNUrl repositoryUrl)
274: throws SVNClientException {
275: return getClient(repositoryUrl, null);
276: }
277:
278: /**
279: * <b>Creates</b> ClientAtapter implementation that already handles:
280: * <ul>
281: * <li>prompts user for password if necessary,
282: * <li>let user specify proxy setting on network errors or
283: * <li>let user cancel operation
284: * <li>logs command execuion into output tab
285: * <li>posts notification events in status cache
286: * </ul>
287: *
288: * <p>It hanldes cancellability
289: */
290: public SvnClient getClient(boolean attachListeners)
291: throws SVNClientException {
292: cleanupFilesystem();
293: if (attachListeners) {
294: if (noUrlClientWithListeners == null) {
295: noUrlClientWithListeners = SvnClientFactory
296: .getInstance().createSvnClient();
297: attachListeners(noUrlClientWithListeners);
298: }
299: return noUrlClientWithListeners;
300: } else {
301: if (noUrlClientWithoutListeners == null) {
302: noUrlClientWithoutListeners = SvnClientFactory
303: .getInstance().createSvnClient();
304: }
305: return noUrlClientWithoutListeners;
306: }
307: }
308:
309: public FilesystemHandler getFileSystemHandler() {
310: return filesystemHandler;
311: }
312:
313: /**
314: * Tests whether a file or directory should receive the STATUS_NOTVERSIONED_NOTMANAGED status.
315: * All files and folders that have a parent with either .svn/entries or _svn/entries file are
316: * considered versioned.
317: *
318: * @param file a file or directory
319: * @return false if the file should receive the STATUS_NOTVERSIONED_NOTMANAGED status, true otherwise
320: */
321: public boolean isManaged(File file) {
322: return VersioningSupport.getOwner(file) instanceof SubversionVCS
323: && !SvnUtils.isPartOfSubversionMetadata(file);
324: }
325:
326: public void versionedFilesChanged() {
327: support.firePropertyChange(PROP_VERSIONED_FILES_CHANGED, null,
328: null);
329: }
330:
331: /**
332: * Tests whether the file is managed by this versioning system. If it is, the method should return the topmost
333: * parent of the file that is still versioned.
334: *
335: * @param file a file
336: * @return File the file itself or one of its parents or null if the supplied file is NOT managed by this versioning system
337: */
338: File getTopmostManagedParent(File file) {
339: try {
340: SvnClientFactory.checkClientAvailable();
341: } catch (SVNClientException ex) {
342: return null;
343: }
344: if (SvnUtils.isPartOfSubversionMetadata(file)) {
345: for (; file != null; file = file.getParentFile()) {
346: if (isAdministrative(file)) {
347: file = file.getParentFile();
348: break;
349: }
350: }
351: }
352: File topmost = null;
353: for (; file != null; file = file.getParentFile()) {
354: if (org.netbeans.modules.versioning.util.Utils
355: .isScanForbidden(file))
356: break;
357: if (new File(file, ".svn/entries").canRead()
358: || new File(file, "_svn/entries").canRead()) { // NOI18N
359: topmost = file;
360: }
361: }
362: return topmost;
363: }
364:
365: /**
366: * TODO: Backdoor for SvnClientFactory
367: */
368: public void cleanupFilesystem() {
369: filesystemHandler.removeInvalidMetadata();
370: }
371:
372: private void attachListeners(SvnClient client) {
373: // XXX let the cache and logger register by themself (addXXXListener)
374: client.addNotifyListener(getLogger(client.getSvnUrl()));
375: client.addNotifyListener(fileStatusCache);
376:
377: List<ISVNNotifyListener> l = getSVNNotifyListeners();
378:
379: ISVNNotifyListener[] listeners = null;
380: synchronized (l) {
381: listeners = l.toArray(new ISVNNotifyListener[l.size()]);
382: }
383: for (ISVNNotifyListener listener : listeners) {
384: client.addNotifyListener(listener);
385: }
386: }
387:
388: /**
389: *
390: * @param repositoryRoot URL of Subversion repository so that logger writes to correct output tab. Can be null
391: * in which case the logger will not print anything
392: * @return OutputLogger logger to write to
393: */
394: public OutputLogger getLogger(SVNUrl repositoryRoot) {
395: return OutputLogger.getLogger(repositoryRoot);
396: }
397:
398: /**
399: * Non-recursive ignore check.
400: *
401: * <p>Side effect: if under SVN version control
402: * it sets svn:ignore property
403: *
404: * @return true if file is listed in parent's ignore list
405: * or IDE thinks it should be.
406: */
407: boolean isIgnored(final File file) {
408: String name = file.getName();
409:
410: // ask SVN
411:
412: final File parent = file.getParentFile();
413: if (parent != null) {
414: int pstatus = fileStatusCache.getStatus(parent).getStatus();
415: if ((pstatus & FileInformation.STATUS_VERSIONED) != 0) {
416: try {
417: SvnClient client = getClient(false);
418:
419: List<String> gignores = SvnConfigFiles
420: .getInstance().getGlobalIgnores();
421: if (SvnUtils.getMatchinIgnoreParterns(gignores,
422: name, true).size() > 0) {
423: // no need to read the ignored property -> its already set in ignore patterns
424: return true;
425: }
426: List<String> patterns = client
427: .getIgnoredPatterns(parent);
428: if (SvnUtils.getMatchinIgnoreParterns(patterns,
429: name, true).size() > 0) {
430: return true;
431: }
432:
433: } catch (SVNClientException ex) {
434: SvnClientExceptionHandler.notifyException(ex,
435: false, false);
436: }
437: }
438: }
439:
440: if (SharabilityQuery.getSharability(file) == SharabilityQuery.NOT_SHARABLE) {
441: try {
442: // BEWARE: In NetBeans VISIBILTY == SHARABILITY ... and we hide Locally Removed folders => we must not Ignore them by mistake
443: FileInformation info = fileStatusCache
444: .getCachedStatus(file); // getStatus may cause stack overflow
445: if (SubversionVisibilityQuery
446: .isHiddenFolder(info, file)) {
447: return false;
448: }
449: // if IDE-ignore-root then propagate IDE opinion to Subversion svn:ignore
450: if (SharabilityQuery.getSharability(parent) != SharabilityQuery.NOT_SHARABLE) {
451: if ((fileStatusCache.getStatus(parent).getStatus() & FileInformation.STATUS_VERSIONED) != 0) {
452: IgnoreAction.ignore(file);
453: }
454: }
455: } catch (SVNClientException ex) {
456: SvnClientExceptionHandler.notifyException(ex, false,
457: false);
458: }
459: return true;
460: } else {
461: // backward compatability #68124
462: if (".nbintdb".equals(name)) { // NOI18N
463: return true;
464: }
465:
466: return false;
467: }
468: }
469:
470: /**
471: * Serializes all SVN requests (moves them out of AWT).
472: */
473: public RequestProcessor getRequestProcessor() {
474: return getRequestProcessor(null);
475: }
476:
477: /**
478: * Serializes all SVN requests (moves them out of AWT).
479: */
480: public RequestProcessor getRequestProcessor(SVNUrl url) {
481: if (processorsToUrl == null) {
482: processorsToUrl = new HashMap<String, RequestProcessor>();
483: }
484:
485: String key;
486: if (url != null) {
487: key = url.toString();
488: } else {
489: key = "ANY_URL"; // NOI18N
490: }
491:
492: RequestProcessor rp = processorsToUrl.get(key);
493: if (rp == null) {
494: rp = new RequestProcessor("Subversion - " + key, 1, true); // NOI18N
495: processorsToUrl.put(key, rp);
496: }
497: return rp;
498: }
499:
500: FileStatusProvider getVCSAnnotator() {
501: return fileStatusProvider;
502: }
503:
504: VCSInterceptor getVCSInterceptor() {
505: return filesystemHandler;
506: }
507:
508: private List<ISVNNotifyListener> getSVNNotifyListeners() {
509: if (svnNotifyListeners == null) {
510: svnNotifyListeners = new ArrayList<ISVNNotifyListener>();
511: }
512: return svnNotifyListeners;
513: }
514:
515: /**
516: * Refreshes all textual annotations and badges.
517: */
518: public void refreshAllAnnotations() {
519: support
520: .firePropertyChange(PROP_ANNOTATIONS_CHANGED, null,
521: null);
522: }
523:
524: void addPropertyChangeListener(PropertyChangeListener listener) {
525: support.addPropertyChangeListener(listener);
526: }
527:
528: void removePropertyChangeListener(PropertyChangeListener listener) {
529: support.removePropertyChangeListener(listener);
530: }
531:
532: public void addSVNNotifyListener(ISVNNotifyListener listener) {
533: List<ISVNNotifyListener> listeners = getSVNNotifyListeners();
534: synchronized (listeners) {
535: listeners.add(listener);
536: }
537: }
538:
539: public void removeSVNNotifyListener(ISVNNotifyListener listener) {
540: List<ISVNNotifyListener> listeners = getSVNNotifyListeners();
541: synchronized (listeners) {
542: listeners.remove(listener);
543: }
544: }
545:
546: public void getOriginalFile(File workingCopy, File originalFile) {
547: FileInformation info = fileStatusCache.getStatus(workingCopy);
548: if ((info.getStatus() & STATUS_DIFFABLE) == 0)
549: return;
550:
551: try {
552: File original = VersionsCache.getInstance()
553: .getFileRevision(workingCopy, Setup.REVISION_BASE);
554: if (original == null)
555: throw new IOException("Unable to get BASE revision of "
556: + workingCopy);
557: org.netbeans.modules.versioning.util.Utils
558: .copyStreamsCloseAll(new FileOutputStream(
559: originalFile),
560: new FileInputStream(original));
561: } catch (IOException e) {
562: LOG.log(Level.INFO, "Unable to get original file", e);
563: }
564: }
565: }
|