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: package org.netbeans.modules.mercurial;
042:
043: import javax.swing.SwingUtilities;
044: import org.netbeans.modules.versioning.spi.VCSInterceptor;
045: import org.netbeans.modules.versioning.util.Utils;
046: import org.netbeans.modules.mercurial.HgException;
047: import java.io.File;
048: import java.io.IOException;
049: import org.netbeans.modules.mercurial.util.HgCommand;
050: import org.openide.util.RequestProcessor;
051: import java.util.logging.Level;
052: import java.util.List;
053: import java.util.ArrayList;
054: import java.util.Iterator;
055: import java.util.Map;
056: import java.util.Collection;
057: import java.util.Calendar;
058: import org.netbeans.modules.mercurial.util.HgUtils;
059: import org.netbeans.api.queries.SharabilityQuery;
060: import java.util.concurrent.ConcurrentLinkedQueue;
061: import java.util.concurrent.ConcurrentHashMap;
062:
063: /**
064: * Listens on file system changes and reacts appropriately, mainly refreshing affected files' status.
065: *
066: * @author Maros Sandor
067: */
068: public class MercurialInterceptor extends VCSInterceptor {
069:
070: private final FileStatusCache cache;
071:
072: private ConcurrentHashMap<File, File> dirsToDelete = new ConcurrentHashMap<File, File>();
073:
074: private ConcurrentLinkedQueue<File> filesToRefresh = new ConcurrentLinkedQueue<File>();
075:
076: private RequestProcessor.Task refreshTask;
077:
078: private static final RequestProcessor rp = new RequestProcessor(
079: "MercurialRefresh", 1, true);
080:
081: public MercurialInterceptor() {
082: cache = Mercurial.getInstance().getFileStatusCache();
083: refreshTask = rp.create(new RefreshTask());
084: }
085:
086: public boolean beforeDelete(File file) {
087: if (file == null)
088: return true;
089: if (HgUtils.isPartOfMercurialMetadata(file))
090: return false;
091:
092: // We track the deletion of top level directories
093: if (file.isDirectory()) {
094: for (File dir : dirsToDelete.keySet()) {
095: if (file.equals(dir.getParentFile())) {
096: dirsToDelete.remove(dir);
097: }
098: }
099: if (SharabilityQuery.getSharability(file) != SharabilityQuery.NOT_SHARABLE) {
100: dirsToDelete.put(file, file);
101: }
102: }
103: return true;
104: }
105:
106: public void doDelete(File file) throws IOException {
107: return;
108: }
109:
110: public void afterDelete(final File file) {
111: Utils.post(new Runnable() {
112: public void run() {
113: fileDeletedImpl(file);
114: }
115: });
116: }
117:
118: private void fileDeletedImpl(final File file) {
119: if (file == null)
120: return;
121: Mercurial hg = Mercurial.getInstance();
122: final File root = hg.getTopmostManagedParent(file);
123: RequestProcessor rp = null;
124: if (root != null) {
125: rp = hg.getRequestProcessor(root.getAbsolutePath());
126: }
127: if (file.exists()) {
128: if (file.isDirectory()) {
129: file.delete();
130: if (!dirsToDelete.remove(file, file))
131: return;
132: if (root == null)
133: return;
134: HgProgressSupport support = new HgProgressSupport() {
135: public void perform() {
136: try {
137: HgCommand.doRemove(root, file, this
138: .getLogger());
139: // We need to cache the status of all deleted files
140: Map<File, FileInformation> interestingFiles = HgCommand
141: .getInterestingStatus(root, file);
142: if (!interestingFiles.isEmpty()) {
143: Collection<File> files = interestingFiles
144: .keySet();
145:
146: Map<File, Map<File, FileInformation>> interestingDirs = HgUtils
147: .getInterestingDirs(
148: interestingFiles, files);
149:
150: Calendar start = Calendar.getInstance();
151: for (File tmpFile : files) {
152: if (this .isCanceled()) {
153: return;
154: }
155: FileInformation fi = interestingFiles
156: .get(tmpFile);
157:
158: cache
159: .refreshFileStatus(
160: tmpFile,
161: fi,
162: interestingDirs
163: .get(tmpFile
164: .isDirectory() ? tmpFile
165: : tmpFile
166: .getParentFile()),
167: true);
168: }
169: Calendar end = Calendar.getInstance();
170: }
171: } catch (HgException ex) {
172: Mercurial.LOG.log(Level.FINE,
173: "fileDeletedImpl(): File: {0} {1}",
174: new Object[] {
175: file.getAbsolutePath(),
176: ex.toString() }); // NOI18N
177: }
178: }
179: };
180:
181: support.start(rp, root.getAbsolutePath(),
182: org.openide.util.NbBundle.getMessage(
183: MercurialInterceptor.class,
184: "MSG_Remove_Progress")); // NOI18N
185: } else {
186: // If we are deleting a parent directory of this file
187: // skip the call to hg remove as we will do it for the directory
188: file.delete();
189: if (root == null)
190: return;
191: for (File dir : dirsToDelete.keySet()) {
192: File tmpFile = file.getParentFile();
193: while (tmpFile != null) {
194: if (tmpFile.equals(dir))
195: return;
196: tmpFile = tmpFile.getParentFile();
197: }
198: }
199: HgProgressSupport support = new HgProgressSupport() {
200: public void perform() {
201: try {
202: HgCommand.doRemove(root, file, this
203: .getLogger());
204: cache
205: .refresh(
206: file,
207: FileStatusCache.REPOSITORY_STATUS_UNKNOWN);
208: } catch (HgException ex) {
209: Mercurial.LOG.log(Level.FINE,
210: "fileDeletedImpl(): File: {0} {1}",
211: new Object[] {
212: file.getAbsolutePath(),
213: ex.toString() }); // NOI18N
214: }
215: }
216: };
217: support.start(rp, root.getAbsolutePath(),
218: org.openide.util.NbBundle.getMessage(
219: MercurialInterceptor.class,
220: "MSG_Remove_Progress")); // NOI18N
221: }
222: }
223: }
224:
225: public boolean beforeMove(File from, File to) {
226: if (from == null || to == null || to.exists())
227: return true;
228:
229: Mercurial hg = Mercurial.getInstance();
230: if (hg.isManaged(from)) {
231: return hg.isManaged(to);
232: }
233: return super .beforeMove(from, to);
234: }
235:
236: public void doMove(final File from, final File to)
237: throws IOException {
238: if (from == null || to == null || to.exists())
239: return;
240:
241: if (SwingUtilities.isEventDispatchThread()) {
242:
243: Mercurial.LOG.log(Level.INFO,
244: "Warning: launching external process in AWT",
245: new Exception().fillInStackTrace()); // NOI18N
246: final Throwable innerT[] = new Throwable[1];
247: Runnable outOfAwt = new Runnable() {
248: public void run() {
249: try {
250: hgMoveImplementation(from, to);
251: } catch (Throwable t) {
252: innerT[0] = t;
253: }
254: }
255: };
256:
257: Mercurial.getInstance().getRequestProcessor()
258: .post(outOfAwt).waitFinished();
259: if (innerT[0] != null) {
260: if (innerT[0] instanceof IOException) {
261: throw (IOException) innerT[0];
262: } else if (innerT[0] instanceof RuntimeException) {
263: throw (RuntimeException) innerT[0];
264: } else if (innerT[0] instanceof Error) {
265: throw (Error) innerT[0];
266: } else {
267: throw new IllegalStateException(
268: "Unexpected exception class: " + innerT[0]); // NOI18N
269: }
270: }
271:
272: // end of hack
273:
274: } else {
275: hgMoveImplementation(from, to);
276: }
277: }
278:
279: private void hgMoveImplementation(final File srcFile,
280: final File dstFile) throws IOException {
281: final Mercurial hg = Mercurial.getInstance();
282: final File root = hg.getTopmostManagedParent(srcFile);
283: final File dstRoot = hg.getTopmostManagedParent(dstFile);
284: if (root == null)
285: return;
286:
287: RequestProcessor rp = hg.getRequestProcessor(root
288: .getAbsolutePath());
289:
290: Mercurial.LOG.log(Level.FINE,
291: "hgMoveImplementation(): File: {0} {1}", new Object[] {
292: srcFile, dstFile }); // NOI18N
293:
294: srcFile.renameTo(dstFile);
295: Runnable moveImpl = new Runnable() {
296: public void run() {
297: OutputLogger logger = OutputLogger.getLogger(root
298: .getAbsolutePath());
299: try {
300: if (dstFile.isDirectory() && root.equals(dstRoot)) {
301: HgCommand.doRenameAfter(root, srcFile, dstFile,
302: logger);
303: return;
304: }
305: int status = HgCommand.getSingleStatus(root,
306: srcFile.getParent(), srcFile.getName())
307: .getStatus();
308: Mercurial.LOG.log(Level.FINE,
309: "hgMoveImplementation(): Status: {0} {1}",
310: new Object[] { srcFile, status }); // NOI18N
311: if (status == FileInformation.STATUS_NOTVERSIONED_NEWLOCALLY
312: || status == FileInformation.STATUS_NOTVERSIONED_EXCLUDED) {
313: } else if (status == FileInformation.STATUS_VERSIONED_ADDEDLOCALLY) {
314: HgCommand.doRemove(root, srcFile, logger);
315: if (dstRoot != null) {
316: HgCommand.doAdd(dstRoot, dstFile, logger);
317: }
318: } else {
319: if (root.equals(dstRoot)) {
320: HgCommand.doRenameAfter(root, srcFile,
321: dstFile, logger);
322: }
323: }
324: } catch (HgException e) {
325: Mercurial.LOG
326: .log(
327: Level.FINE,
328: "Mercurial failed to rename: File: {0} {1}",
329: new Object[] {
330: srcFile.getAbsolutePath(),
331: dstFile.getAbsolutePath() }); // NOI18N
332: } finally {
333: logger.closeLog();
334: }
335: }
336: };
337:
338: rp.post(moveImpl);
339: }
340:
341: public void afterMove(final File from, final File to) {
342: Utils.post(new Runnable() {
343: public void run() {
344: fileMovedImpl(from, to);
345: }
346: });
347: }
348:
349: private void fileMovedImpl(final File from, final File to) {
350: if (from == null || to == null || !to.exists())
351: return;
352: if (to.isDirectory())
353: return;
354: Mercurial hg = Mercurial.getInstance();
355: final File root = hg.getTopmostManagedParent(from);
356: if (root == null)
357: return;
358:
359: RequestProcessor rp = hg.getRequestProcessor(root
360: .getAbsolutePath());
361:
362: HgProgressSupport supportCreate = new HgProgressSupport() {
363: public void perform() {
364: cache.refresh(from,
365: FileStatusCache.REPOSITORY_STATUS_UNKNOWN);
366: cache.refresh(to,
367: FileStatusCache.REPOSITORY_STATUS_UNKNOWN);
368: }
369: };
370:
371: supportCreate.start(rp, root.getAbsolutePath(),
372: org.openide.util.NbBundle
373: .getMessage(MercurialInterceptor.class,
374: "MSG_Move_Progress")); // NOI18N
375: }
376:
377: public boolean beforeCreate(File file, boolean isDirectory) {
378: return super .beforeCreate(file, isDirectory);
379: }
380:
381: public void doCreate(File file, boolean isDirectory)
382: throws IOException {
383: super .doCreate(file, isDirectory);
384: }
385:
386: public void afterCreate(final File file) {
387: Utils.post(new Runnable() {
388: public void run() {
389: fileCreatedImpl(file);
390: }
391: });
392: }
393:
394: private void fileCreatedImpl(final File file) {
395: if (file.isDirectory())
396: return;
397: Mercurial hg = Mercurial.getInstance();
398: final File root = hg.getTopmostManagedParent(file);
399: if (root == null)
400: return;
401:
402: RequestProcessor rp = hg.getRequestProcessor(root
403: .getAbsolutePath());
404:
405: HgProgressSupport supportCreate = new HgProgressSupport() {
406: public void perform() {
407: // There is no point in refreshing the cache for ignored files.
408: if (!HgUtils.isIgnored(file, false)) {
409: reScheduleRefresh(1000, file);
410: }
411: }
412: };
413:
414: supportCreate.start(rp, root.getAbsolutePath(),
415: org.openide.util.NbBundle.getMessage(
416: MercurialInterceptor.class,
417: "MSG_Create_Progress")); // NOI18N
418: }
419:
420: public void afterChange(final File file) {
421: Utils.post(new Runnable() {
422: public void run() {
423: fileChangedImpl(file);
424: }
425: });
426: }
427:
428: private void fileChangedImpl(final File file) {
429: if (file.isDirectory())
430: return;
431: Mercurial hg = Mercurial.getInstance();
432: final File root = hg.getTopmostManagedParent(file);
433: if (root == null)
434: return;
435:
436: RequestProcessor rp = hg.getRequestProcessor(root
437: .getAbsolutePath());
438:
439: HgProgressSupport supportCreate = new HgProgressSupport() {
440: public void perform() {
441: Mercurial.LOG.log(Level.FINE,
442: "fileChangedImpl(): File: {0}", file); // NOI18N
443: // There is no point in refreshing the cache for ignored files.
444: if (!HgUtils.isIgnored(file, false)) {
445: reScheduleRefresh(1000, file);
446: }
447: }
448: };
449:
450: supportCreate.start(rp, root.getAbsolutePath(),
451: org.openide.util.NbBundle.getMessage(
452: MercurialInterceptor.class,
453: "MSG_Change_Progress")); // NOI18N
454: }
455:
456: private void reScheduleRefresh(int delayMillis, File fileToRefresh) {
457: if (!filesToRefresh.contains(fileToRefresh)) {
458: if (!filesToRefresh.offer(fileToRefresh)) {
459: Mercurial.LOG
460: .log(
461: Level.FINE,
462: "reScheduleRefresh failed to add to filesToRefresh queue {0}",
463: fileToRefresh);
464: }
465: }
466: refreshTask.schedule(delayMillis);
467: }
468:
469: private class RefreshTask implements Runnable {
470: public void run() {
471: Thread.interrupted();
472: File fileToRefresh = filesToRefresh.poll();
473: if (fileToRefresh != null) {
474: cache.refresh(fileToRefresh,
475: FileStatusCache.REPOSITORY_STATUS_UNKNOWN);
476: fileToRefresh = filesToRefresh.peek();
477: if (fileToRefresh != null) {
478: refreshTask.schedule(0);
479: }
480: }
481: }
482: }
483:
484: }
|