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.ui.diff;
043:
044: import org.netbeans.modules.diff.builtin.visualizer.TextDiffVisualizer;
045: import org.netbeans.modules.subversion.FileInformation;
046: import org.netbeans.modules.subversion.Subversion;
047: import org.netbeans.modules.subversion.SvnModuleConfig;
048: import org.netbeans.modules.subversion.client.SvnProgressSupport;
049: import org.netbeans.modules.subversion.util.Context;
050: import org.netbeans.modules.subversion.util.SvnUtils;
051: import org.netbeans.modules.subversion.ui.actions.ContextAction;
052: import org.netbeans.modules.versioning.util.AccessibleJFileChooser;
053: import org.netbeans.modules.versioning.util.Utils;
054: import org.netbeans.api.diff.Difference;
055: import org.netbeans.spi.diff.DiffProvider;
056: import org.openide.windows.TopComponent;
057: import org.openide.util.Lookup;
058: import org.openide.util.RequestProcessor;
059: import org.openide.util.NbBundle;
060: import org.openide.NotifyDescriptor;
061: import org.openide.DialogDisplayer;
062: import org.openide.DialogDescriptor;
063: import org.openide.nodes.Node;
064: import org.openide.awt.StatusDisplayer;
065: import javax.swing.*;
066: import java.io.*;
067: import java.util.*;
068: import java.util.List;
069: import java.awt.event.ActionListener;
070: import java.awt.event.ActionEvent;
071: import java.awt.*;
072: import java.util.logging.Level;
073: import org.netbeans.modules.subversion.FileStatusCache;
074: import org.netbeans.modules.subversion.client.SvnClientExceptionHandler;
075: import org.netbeans.modules.proxy.Base64Encoder;
076: import org.tigris.subversion.svnclientadapter.SVNClientException;
077:
078: /**
079: * Exports diff to file:
080: *
081: * <ul>
082: * <li>for components that implements {@link DiffSetupSource} interface
083: * exports actually displayed diff.
084: *
085: * <li>for DataNodes <b>local</b> differencies between the current
086: * working copy and BASE repository version.
087: * </ul>
088: *
089: * @author Petr Kuzel
090: */
091: public class ExportDiffAction extends ContextAction {
092:
093: private static final int enabledForStatus = FileInformation.STATUS_VERSIONED_MERGE
094: | FileInformation.STATUS_VERSIONED_MODIFIEDLOCALLY
095: | FileInformation.STATUS_VERSIONED_DELETEDLOCALLY
096: | FileInformation.STATUS_VERSIONED_REMOVEDLOCALLY
097: | FileInformation.STATUS_NOTVERSIONED_NEWLOCALLY
098: | FileInformation.STATUS_VERSIONED_ADDEDLOCALLY;
099:
100: protected String getBaseName(Node[] activatedNodes) {
101: return "CTL_MenuItem_ExportDiff"; // NOI18N
102: }
103:
104: /**
105: * First look for DiffSetupSource name then for super (context name).
106: */
107: public String getName() {
108: TopComponent activated = TopComponent.getRegistry()
109: .getActivated();
110: if (activated instanceof DiffSetupSource) {
111: String setupName = ((DiffSetupSource) activated)
112: .getSetupDisplayName();
113: if (setupName != null) {
114: return NbBundle.getMessage(this .getClass(),
115: getBaseName(getActivatedNodes()) + "_Context", // NOI18N
116: setupName);
117: }
118: }
119: return super .getName();
120: }
121:
122: public boolean enable(Node[] nodes) {
123: Context ctx = SvnUtils.getCurrentContext(nodes);
124: File[] files = getModifiedFiles(ctx, enabledForStatus);
125: if (files.length < 1) {
126: return false;
127: }
128: TopComponent activated = TopComponent.getRegistry()
129: .getActivated();
130: if (activated instanceof DiffSetupSource) {
131: return true;
132: }
133: return super .enable(nodes)
134: && Lookup.getDefault().lookup(DiffProvider.class) != null;
135: }
136:
137: protected void performContextAction(final Node[] nodes) {
138:
139: // reevaluate fast enablement logic guess
140:
141: if (!Subversion.getInstance().checkClientAvailable()) {
142: return;
143: }
144:
145: boolean noop;
146: TopComponent activated = TopComponent.getRegistry()
147: .getActivated();
148: if (activated instanceof DiffSetupSource) {
149: noop = ((DiffSetupSource) activated).getSetups().isEmpty();
150: } else {
151: Context context = getContext(nodes);
152: File[] files = SvnUtils.getModifiedFiles(context,
153: FileInformation.STATUS_LOCAL_CHANGE);
154: noop = files.length == 0;
155: }
156: if (noop) {
157: NotifyDescriptor msg = new NotifyDescriptor.Message(
158: NbBundle.getMessage(ExportDiffAction.class,
159: "BK3001"),
160: NotifyDescriptor.INFORMATION_MESSAGE);
161: DialogDisplayer.getDefault().notify(msg);
162: return;
163: }
164:
165: final JFileChooser chooser = new AccessibleJFileChooser(
166: NbBundle.getMessage(ExportDiffAction.class,
167: "ACSD_Export"));
168: chooser.setDialogTitle(NbBundle.getMessage(
169: ExportDiffAction.class, "CTL_Export_Title"));
170: chooser.setMultiSelectionEnabled(false);
171: javax.swing.filechooser.FileFilter[] old = chooser
172: .getChoosableFileFilters();
173: for (int i = 0; i < old.length; i++) {
174: javax.swing.filechooser.FileFilter fileFilter = old[i];
175: chooser.removeChoosableFileFilter(fileFilter);
176:
177: }
178: chooser.setCurrentDirectory(new File(SvnModuleConfig
179: .getDefault().getPreferences().get(
180: "ExportDiff.saveFolder",
181: System.getProperty("user.home")))); // NOI18N
182: chooser
183: .addChoosableFileFilter(new javax.swing.filechooser.FileFilter() {
184: public boolean accept(File f) {
185: return f.getName().endsWith("diff")
186: || f.getName().endsWith("patch")
187: || f.isDirectory(); // NOI18N
188: }
189:
190: public String getDescription() {
191: return NbBundle.getMessage(
192: ExportDiffAction.class, "BK3002");
193: }
194: });
195:
196: chooser.setDialogType(JFileChooser.SAVE_DIALOG);
197: chooser.setApproveButtonMnemonic(NbBundle.getMessage(
198: ExportDiffAction.class, "MNE_Export_ExportAction")
199: .charAt(0));
200: chooser.setApproveButtonText(NbBundle.getMessage(
201: ExportDiffAction.class, "CTL_Export_ExportAction"));
202: DialogDescriptor dd = new DialogDescriptor(chooser, NbBundle
203: .getMessage(ExportDiffAction.class, "CTL_Export_Title"));
204: dd.setOptions(new Object[0]);
205: final Dialog dialog = DialogDisplayer.getDefault()
206: .createDialog(dd);
207:
208: chooser.addActionListener(new ActionListener() {
209: public void actionPerformed(ActionEvent e) {
210: String state = e.getActionCommand();
211: if (state.equals(JFileChooser.APPROVE_SELECTION)) {
212: File destination = chooser.getSelectedFile();
213: String name = destination.getName();
214: boolean requiredExt = false;
215: requiredExt |= name.endsWith(".diff"); // NOI18N
216: requiredExt |= name.endsWith(".dif"); // NOI18N
217: requiredExt |= name.endsWith(".patch"); // NOI18N
218: if (requiredExt == false) {
219: File parent = destination.getParentFile();
220: destination = new File(parent, name + ".patch"); // NOI18N
221: }
222:
223: if (destination.exists()) {
224: NotifyDescriptor nd = new NotifyDescriptor.Confirmation(
225: NbBundle.getMessage(
226: ExportDiffAction.class,
227: "BK3005", destination
228: .getAbsolutePath()));
229: nd
230: .setOptionType(NotifyDescriptor.YES_NO_OPTION);
231: DialogDisplayer.getDefault().notify(nd);
232: if (nd.getValue().equals(
233: NotifyDescriptor.OK_OPTION) == false) {
234: return;
235: }
236: }
237: SvnModuleConfig.getDefault().getPreferences().put(
238: "ExportDiff.saveFolder",
239: destination.getParent()); // NOI18N
240: final File out = destination;
241: RequestProcessor rp = Subversion.getInstance()
242: .getRequestProcessor();
243: SvnProgressSupport ps = new SvnProgressSupport() {
244: protected void perform() {
245: async(this , nodes, out);
246: }
247: };
248: ps.start(rp, null, getRunningName(nodes));
249: }
250: dialog.dispose();
251: }
252: });
253: dialog.setVisible(true);
254:
255: }
256:
257: protected boolean asynchronous() {
258: return false;
259: }
260:
261: private void async(SvnProgressSupport progress, Node[] nodes,
262: File destination) {
263: boolean success = false;
264: OutputStream out = null;
265: int exportedFiles = 0;
266: try {
267:
268: // prepare setups and common parent - root
269:
270: File root;
271: List<Setup> setups;
272:
273: TopComponent activated = TopComponent.getRegistry()
274: .getActivated();
275: if (activated instanceof DiffSetupSource) {
276: setups = new ArrayList<Setup>(
277: ((DiffSetupSource) activated).getSetups());
278: List<File> setupFiles = new ArrayList<File>(setups
279: .size());
280: for (Iterator i = setups.iterator(); i.hasNext();) {
281: Setup setup = (Setup) i.next();
282: setupFiles.add(setup.getBaseFile());
283: }
284: root = getCommonParent(setupFiles
285: .toArray(new File[setupFiles.size()]));
286: } else {
287: Context context = getContext(nodes);
288: File[] files = SvnUtils.getModifiedFiles(context,
289: FileInformation.STATUS_LOCAL_CHANGE);
290: root = getCommonParent(context.getRootFiles());
291: setups = new ArrayList<Setup>(files.length);
292: for (int i = 0; i < files.length; i++) {
293: File file = files[i];
294: Setup setup = new Setup(file, null,
295: Setup.DIFFTYPE_LOCAL);
296: setups.add(setup);
297: }
298: }
299: if (root == null) {
300: NotifyDescriptor nd = new NotifyDescriptor(NbBundle
301: .getMessage(ExportDiffAction.class,
302: "MSG_BadSelection_Prompt"), NbBundle
303: .getMessage(ExportDiffAction.class,
304: "MSG_BadSelection_Title"),
305: NotifyDescriptor.DEFAULT_OPTION,
306: NotifyDescriptor.ERROR_MESSAGE, null, null);
307: DialogDisplayer.getDefault().notify(nd);
308: return;
309: }
310:
311: String sep = System.getProperty("line.separator"); // NOI18N
312: out = new BufferedOutputStream(new FileOutputStream(
313: destination));
314: // Used by PatchAction as MAGIC to detect right encoding
315: out
316: .write(("# This patch file was generated by NetBeans IDE" + sep)
317: .getBytes("utf8")); // NOI18N
318: out.write(("# Following Index: paths are relative to: "
319: + root.getAbsolutePath() + sep).getBytes("utf8")); // NOI18N
320: out
321: .write(("# This patch can be applied using context Tools: Patch action on respective folder." + sep)
322: .getBytes("utf8")); // NOI18N
323: out
324: .write(("# It uses platform neutral UTF-8 encoding and \\n newlines." + sep)
325: .getBytes("utf8")); // NOI18N
326: out
327: .write(("# Above lines and this line are ignored by the patching process." + sep)
328: .getBytes("utf8")); // NOI18N
329:
330: Collections.sort(setups, new Comparator<Setup>() {
331: public int compare(Setup o1, Setup o2) {
332: return o1.getBaseFile().compareTo(o2.getBaseFile());
333: }
334: });
335: Iterator<Setup> it = setups.iterator();
336: int i = 0;
337: while (it.hasNext()) {
338: Setup setup = it.next();
339: File file = setup.getBaseFile();
340: if (file.isDirectory())
341: continue;
342: try {
343: progress.setRepositoryRoot(SvnUtils
344: .getRepositoryRootUrl(file));
345: } catch (SVNClientException ex) {
346: SvnClientExceptionHandler.notifyException(ex, true,
347: true);
348: return;
349: }
350: progress.setDisplayName(file.getName());
351:
352: String index = "Index: "; // NOI18N
353: String rootPath = root.getAbsolutePath();
354: String filePath = file.getAbsolutePath();
355: String relativePath = filePath;
356: if (filePath.startsWith(rootPath)) {
357: relativePath = filePath.substring(
358: rootPath.length() + 1).replace(
359: File.separatorChar, '/');
360: index += relativePath + sep;
361: out.write(index.getBytes("utf8")); // NOI18N
362: }
363: exportDiff(setup, relativePath, out);
364: i++;
365: }
366:
367: exportedFiles = i;
368: success = true;
369: } catch (IOException ex) {
370: Subversion.LOG.log(Level.INFO, NbBundle.getMessage(
371: ExportDiffAction.class, "BK3003"), ex);
372: } finally {
373: if (out != null) {
374: try {
375: out.flush();
376: out.close();
377: } catch (IOException alreadyClsoed) {
378: }
379: }
380: if (success) {
381: StatusDisplayer.getDefault().setStatusText(
382: NbBundle.getMessage(ExportDiffAction.class,
383: "BK3004", new Integer(exportedFiles)));
384: if (exportedFiles == 0) {
385: destination.delete();
386: } else {
387: Utils.openFile(destination);
388: }
389: } else {
390: destination.delete();
391: }
392:
393: }
394: }
395:
396: private static File getCommonParent(File[] files) {
397: File root = files[0];
398: if (root.isFile())
399: root = root.getParentFile();
400: for (int i = 1; i < files.length; i++) {
401: root = Utils.getCommonParent(root, files[i]);
402: if (root == null)
403: return null;
404: }
405: return root;
406: }
407:
408: /** Writes contextual diff into given stream.*/
409: private void exportDiff(Setup setup, String relativePath,
410: OutputStream out) throws IOException {
411: setup.initSources();
412: DiffProvider diff = (DiffProvider) Lookup.getDefault().lookup(
413: DiffProvider.class);
414:
415: Reader r1 = null;
416: Reader r2 = null;
417: Difference[] differences;
418:
419: try {
420: r1 = setup.getFirstSource().createReader();
421: if (r1 == null)
422: r1 = new StringReader(""); // NOI18N
423: r2 = setup.getSecondSource().createReader();
424: if (r2 == null)
425: r2 = new StringReader(""); // NOI18N
426: differences = diff.computeDiff(r1, r2);
427: } finally {
428: if (r1 != null)
429: try {
430: r1.close();
431: } catch (Exception e) {
432: }
433: if (r2 != null)
434: try {
435: r2.close();
436: } catch (Exception e) {
437: }
438: }
439:
440: File file = setup.getBaseFile();
441: try {
442: InputStream is;
443: if (!SvnUtils.getMimeType(file).startsWith("text/")
444: && differences.length == 0) {
445: // assume the file is binary
446: is = new ByteArrayInputStream(exportBinaryFile(file)
447: .getBytes("utf8")); // NOI18N
448: } else {
449: r1 = setup.getFirstSource().createReader();
450: if (r1 == null)
451: r1 = new StringReader(""); // NOI18N
452: r2 = setup.getSecondSource().createReader();
453: if (r2 == null)
454: r2 = new StringReader(""); // NOI18N
455: TextDiffVisualizer.TextDiffInfo info = new TextDiffVisualizer.TextDiffInfo(
456: relativePath + " "
457: + setup.getFirstSource().getTitle(), // NOI18N
458: relativePath + " "
459: + setup.getSecondSource().getTitle(), // NOI18N
460: null, null, r1, r2, differences);
461: info.setContextMode(true, 3);
462: String diffText = TextDiffVisualizer
463: .differenceToUnifiedDiffText(info);
464: is = new ByteArrayInputStream(diffText.getBytes("utf8")); // NOI18N
465: }
466: while (true) {
467: int i = is.read();
468: if (i == -1)
469: break;
470: out.write(i);
471: }
472: } finally {
473: if (r1 != null)
474: try {
475: r1.close();
476: } catch (Exception e) {
477: }
478: if (r2 != null)
479: try {
480: r2.close();
481: } catch (Exception e) {
482: }
483: }
484: }
485:
486: /**
487: * Utility method that returns all non-excluded modified files that are
488: * under given roots (folders) and have one of specified statuses.
489: *
490: * @param context context to search
491: * @param includeStatus bit mask of file statuses to include in result
492: * @return File [] array of Files having specified status
493: */
494: public static File[] getModifiedFiles(Context context,
495: int includeStatus) {
496: File[] all = Subversion.getInstance().getStatusCache()
497: .listFiles(context, includeStatus);
498: List<File> files = new ArrayList<File>();
499: for (int i = 0; i < all.length; i++) {
500: File file = all[i];
501: files.add(file);
502: }
503:
504: // ensure that command roots (files that were explicitly selected by user) are included in Diff
505: FileStatusCache cache = Subversion.getInstance()
506: .getStatusCache();
507: File[] rootFiles = context.getRootFiles();
508: for (int i = 0; i < rootFiles.length; i++) {
509: File file = rootFiles[i];
510: if (file.isFile()
511: && (cache.getStatus(file).getStatus() & includeStatus) != 0
512: && !files.contains(file)) {
513: files.add(file);
514: }
515: }
516: return files.toArray(new File[files.size()]);
517: }
518:
519: private String exportBinaryFile(File file) throws IOException {
520: ByteArrayOutputStream baos = new ByteArrayOutputStream();
521: StringBuilder sb = new StringBuilder((int) file.length());
522: if (file.canRead()) {
523: Utils.copyStreamsCloseAll(baos, new FileInputStream(file));
524: }
525: sb
526: .append("MIME: application/octet-stream; encoding: Base64; length: "
527: + (file.canRead() ? file.length() : -1)); // NOI18N
528: sb.append(System.getProperty("line.separator")); // NOI18N
529: sb.append(Base64Encoder.encode(baos.toByteArray(), true));
530: sb.append(System.getProperty("line.separator")); // NOI18N
531: return sb.toString();
532: }
533: }
|