2
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4
* Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
6
* The contents of this file are subject to the terms of either the GNU
7
* General Public License Version 2 only ("GPL") or the Common
8
* Development and Distribution License("CDDL") (collectively, the
9
* "License"). You may not use this file except in compliance with the
10
* License. You can obtain a copy of the License at
11
* http://www.netbeans.org/cddl-gplv2.html
12
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
13
* specific language governing permissions and limitations under the
14
* License. When distributing the software, include this License Header
15
* Notice in each file and include the License file at
16
* nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
17
* particular file as subject to the "Classpath" exception as provided
18
* by Sun in the GPL Version 2 section of the License file that
19
* accompanied this code. If applicable, add the following below the
20
* License Header, with the fields enclosed by brackets [] replaced by
21
* your own identifying information:
22
* "Portions Copyrighted [year] [name of copyright owner]"
26
* The Original Software is NetBeans. The Initial Developer of the Original
27
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
28
* Microsystems, Inc. All Rights Reserved.
30
* If you wish your version of this file to be governed by only the CDDL
31
* or only the GPL Version 2, indicate your decision by adding
32
* "[Contributor] elects to include this software in this distribution
33
* under the [CDDL or GPL Version 2] license." If you do not indicate a
34
* single choice of license, a recipient has the option to distribute
35
* your version of this file under either the CDDL, the GPL Version 2 or
36
* to extend the choice of license to its licensees as provided above.
37
* However, if you add GPL Version 2 code and therefore, elected the GPL
38
* Version 2 license, then the option applies only if the new code is
39
* made subject to such option by the copyright holder.
42
package org.netbeans.modules.subversion.ui.diff;
44
import org.netbeans.modules.diff.builtin.visualizer.TextDiffVisualizer;
45
import org.netbeans.modules.subversion.FileInformation;
46
import org.netbeans.modules.subversion.Subversion;
47
import org.netbeans.modules.subversion.SvnModuleConfig;
48
import org.netbeans.modules.subversion.client.SvnProgressSupport;
49
import org.netbeans.modules.subversion.util.Context;
50
import org.netbeans.modules.subversion.util.SvnUtils;
51
import org.netbeans.modules.subversion.ui.actions.ContextAction;
52
import org.netbeans.modules.versioning.util.AccessibleJFileChooser;
53
import org.netbeans.modules.versioning.util.Utils;
54
import org.netbeans.api.diff.Difference;
55
import org.netbeans.spi.diff.DiffProvider;
56
import org.openide.windows.TopComponent;
57
import org.openide.util.Lookup;
58
import org.openide.util.RequestProcessor;
59
import org.openide.util.NbBundle;
60
import org.openide.ErrorManager;
61
import org.openide.NotifyDescriptor;
62
import org.openide.DialogDisplayer;
63
import org.openide.DialogDescriptor;
64
import org.openide.nodes.Node;
65
import org.openide.awt.StatusDisplayer;
69
import java.util.List;
70
import java.awt.event.ActionListener;
71
import java.awt.event.ActionEvent;
73
import org.netbeans.modules.subversion.FileStatusCache;
74
import org.netbeans.modules.subversion.client.SvnClientExceptionHandler;
75
import org.netbeans.modules.proxy.Base64Encoder;
76
import org.tigris.subversion.svnclientadapter.SVNClientException;
79
* Exports diff to file:
82
* <li>for components that implements {@link DiffSetupSource} interface
83
* exports actually displayed diff.
85
* <li>for DataNodes <b>local</b> differencies between the current
86
* working copy and BASE repository version.
91
public class ExportDiffAction extends ContextAction {
93
private static final int enabledForStatus =
94
FileInformation.STATUS_VERSIONED_MERGE |
95
FileInformation.STATUS_VERSIONED_MODIFIEDLOCALLY |
96
FileInformation.STATUS_VERSIONED_DELETEDLOCALLY |
97
FileInformation.STATUS_VERSIONED_REMOVEDLOCALLY |
98
FileInformation.STATUS_NOTVERSIONED_NEWLOCALLY |
99
FileInformation.STATUS_VERSIONED_ADDEDLOCALLY;
101
protected String getBaseName(Node [] activatedNodes) {
102
return "CTL_MenuItem_ExportDiff"; // NOI18N
106
* First look for DiffSetupSource name then for super (context name).
108
public String getName() {
109
TopComponent activated = TopComponent.getRegistry().getActivated();
110
if (activated instanceof DiffSetupSource) {
111
String setupName = ((DiffSetupSource)activated).getSetupDisplayName();
112
if (setupName != null) {
113
return NbBundle.getMessage(this.getClass(), getBaseName(getActivatedNodes()) + "_Context", // NOI18N
117
return super.getName();
120
public boolean enable(Node[] nodes) {
121
Context ctx = SvnUtils.getCurrentContext(nodes);
122
File[] files = getModifiedFiles(ctx, enabledForStatus);
123
if(files.length < 1) {
126
TopComponent activated = TopComponent.getRegistry().getActivated();
127
if (activated instanceof DiffSetupSource) {
130
return super.enable(nodes) && Lookup.getDefault().lookup(DiffProvider.class) != null;
133
protected void performContextAction(final Node[] nodes) {
135
// reevaluate fast enablement logic guess
137
if(!Subversion.getInstance().checkClientAvailable()) {
142
TopComponent activated = TopComponent.getRegistry().getActivated();
143
if (activated instanceof DiffSetupSource) {
144
noop = ((DiffSetupSource) activated).getSetups().isEmpty();
146
Context context = getContext(nodes);
147
File [] files = SvnUtils.getModifiedFiles(context, FileInformation.STATUS_LOCAL_CHANGE);
148
noop = files.length == 0;
151
NotifyDescriptor msg = new NotifyDescriptor.Message(NbBundle.getMessage(ExportDiffAction.class, "BK3001"), NotifyDescriptor.INFORMATION_MESSAGE);
152
DialogDisplayer.getDefault().notify(msg);
156
final JFileChooser chooser = new AccessibleJFileChooser(NbBundle.getMessage(ExportDiffAction.class, "ACSD_Export"));
157
chooser.setDialogTitle(NbBundle.getMessage(ExportDiffAction.class, "CTL_Export_Title"));
158
chooser.setMultiSelectionEnabled(false);
159
javax.swing.filechooser.FileFilter[] old = chooser.getChoosableFileFilters();
160
for (int i = 0; i < old.length; i++) {
161
javax.swing.filechooser.FileFilter fileFilter = old[i];
162
chooser.removeChoosableFileFilter(fileFilter);
165
chooser.setCurrentDirectory(new File(SvnModuleConfig.getDefault().getPreferences().get("ExportDiff.saveFolder", System.getProperty("user.home")))); // NOI18N
166
chooser.addChoosableFileFilter(new javax.swing.filechooser.FileFilter() {
167
public boolean accept(File f) {
168
return f.getName().endsWith("diff") || f.getName().endsWith("patch") || f.isDirectory(); // NOI18N
170
public String getDescription() {
171
return NbBundle.getMessage(ExportDiffAction.class, "BK3002");
175
chooser.setDialogType(JFileChooser.SAVE_DIALOG);
176
chooser.setApproveButtonMnemonic(NbBundle.getMessage(ExportDiffAction.class, "MNE_Export_ExportAction").charAt(0));
177
chooser.setApproveButtonText(NbBundle.getMessage(ExportDiffAction.class, "CTL_Export_ExportAction"));
178
DialogDescriptor dd = new DialogDescriptor(chooser, NbBundle.getMessage(ExportDiffAction.class, "CTL_Export_Title"));
179
dd.setOptions(new Object[0]);
180
final Dialog dialog = DialogDisplayer.getDefault().createDialog(dd);
182
chooser.addActionListener(new ActionListener() {
183
public void actionPerformed(ActionEvent e) {
184
String state = e.getActionCommand();
185
if (state.equals(JFileChooser.APPROVE_SELECTION)) {
186
File destination = chooser.getSelectedFile();
187
String name = destination.getName();
188
boolean requiredExt = false;
189
requiredExt |= name.endsWith(".diff"); // NOI18N
190
requiredExt |= name.endsWith(".dif"); // NOI18N
191
requiredExt |= name.endsWith(".patch"); // NOI18N
192
if (requiredExt == false) {
193
File parent = destination.getParentFile();
194
destination = new File(parent, name + ".patch"); // NOI18N
197
if (destination.exists()) {
198
NotifyDescriptor nd = new NotifyDescriptor.Confirmation(NbBundle.getMessage(ExportDiffAction.class, "BK3005", destination.getAbsolutePath()));
199
nd.setOptionType(NotifyDescriptor.YES_NO_OPTION);
200
DialogDisplayer.getDefault().notify(nd);
201
if (nd.getValue().equals(NotifyDescriptor.OK_OPTION) == false) {
205
SvnModuleConfig.getDefault().getPreferences().put("ExportDiff.saveFolder", destination.getParent()); // NOI18N
206
final File out = destination;
207
RequestProcessor rp = Subversion.getInstance().getRequestProcessor();
208
SvnProgressSupport ps = new SvnProgressSupport() {
209
protected void perform() {
210
async(this, nodes, out);
213
ps.start(rp, null, getRunningName(nodes));
218
dialog.setVisible(true);
222
protected boolean asynchronous() {
226
private void async(SvnProgressSupport progress, Node[] nodes, File destination) {
227
boolean success = false;
228
OutputStream out = null;
229
int exportedFiles = 0;
232
// prepare setups and common parent - root
237
TopComponent activated = TopComponent.getRegistry().getActivated();
238
if (activated instanceof DiffSetupSource) {
239
setups = new ArrayList<Setup>(((DiffSetupSource) activated).getSetups());
240
List<File> setupFiles = new ArrayList<File>(setups.size());
241
for (Iterator i = setups.iterator(); i.hasNext();) {
242
Setup setup = (Setup) i.next();
243
setupFiles.add(setup.getBaseFile());
245
root = getCommonParent(setupFiles.toArray(new File[setupFiles.size()]));
247
Context context = getContext(nodes);
248
File [] files = SvnUtils.getModifiedFiles(context, FileInformation.STATUS_LOCAL_CHANGE);
249
root = getCommonParent(context.getRootFiles());
250
setups = new ArrayList<Setup>(files.length);
251
for (int i = 0; i < files.length; i++) {
252
File file = files[i];
253
Setup setup = new Setup(file, null, Setup.DIFFTYPE_LOCAL);
258
NotifyDescriptor nd = new NotifyDescriptor(
259
NbBundle.getMessage(ExportDiffAction.class, "MSG_BadSelection_Prompt"),
260
NbBundle.getMessage(ExportDiffAction.class, "MSG_BadSelection_Title"),
261
NotifyDescriptor.DEFAULT_OPTION, NotifyDescriptor.ERROR_MESSAGE, null, null);
262
DialogDisplayer.getDefault().notify(nd);
266
String sep = System.getProperty("line.separator"); // NOI18N
267
out = new BufferedOutputStream(new FileOutputStream(destination));
268
// Used by PatchAction as MAGIC to detect right encoding
269
out.write(("# This patch file was generated by NetBeans IDE" + sep).getBytes("utf8")); // NOI18N
270
out.write(("# Following Index: paths are relative to: " + root.getAbsolutePath() + sep).getBytes("utf8")); // NOI18N
271
out.write(("# This patch can be applied using context Tools: Patch action on respective folder." + sep).getBytes("utf8")); // NOI18N
272
out.write(("# It uses platform neutral UTF-8 encoding and \\n newlines." + sep).getBytes("utf8")); // NOI18N
273
out.write(("# Above lines and this line are ignored by the patching process." + sep).getBytes("utf8")); // NOI18N
276
Collections.sort(setups, new Comparator<Setup>() {
277
public int compare(Setup o1, Setup o2) {
278
return o1.getBaseFile().compareTo(o2.getBaseFile());
281
Iterator<Setup> it = setups.iterator();
283
while (it.hasNext()) {
284
Setup setup = it.next();
285
File file = setup.getBaseFile();
286
if (file.isDirectory()) continue;
288
progress.setRepositoryRoot(SvnUtils.getRepositoryRootUrl(file));
289
} catch (SVNClientException ex) {
290
SvnClientExceptionHandler.notifyException(ex, true, true);
293
progress.setDisplayName(file.getName());
295
String index = "Index: "; // NOI18N
296
String rootPath = root.getAbsolutePath();
297
String filePath = file.getAbsolutePath();
298
String relativePath = filePath;
299
if (filePath.startsWith(rootPath)) {
300
relativePath = filePath.substring(rootPath.length() + 1).replace(File.separatorChar, '/');
301
index += relativePath + sep;
302
out.write(index.getBytes("utf8")); // NOI18N
304
exportDiff(setup, relativePath, out);
310
} catch (IOException ex) {
311
ErrorManager.getDefault().annotate(ex, NbBundle.getMessage(ExportDiffAction.class, "BK3003"));
312
ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, ex); // stack trace to log
313
ErrorManager.getDefault().notify(ErrorManager.USER, ex); // message to user
319
} catch (IOException alreadyClsoed) {
323
StatusDisplayer.getDefault().setStatusText(NbBundle.getMessage(ExportDiffAction.class, "BK3004", new Integer(exportedFiles)));
324
if (exportedFiles == 0) {
325
destination.delete();
327
Utils.openFile(destination);
330
destination.delete();
336
private static File getCommonParent(File [] files) {
337
File root = files[0];
338
if (root.isFile()) root = root.getParentFile();
339
for (int i = 1; i < files.length; i++) {
340
root = Utils.getCommonParent(root, files[i]);
341
if (root == null) return null;
346
/** Writes contextual diff into given stream.*/
347
private void exportDiff(Setup setup, String relativePath, OutputStream out) throws IOException {
349
DiffProvider diff = (DiffProvider) Lookup.getDefault().lookup(DiffProvider.class);
353
Difference[] differences;
356
r1 = setup.getFirstSource().createReader();
357
if (r1 == null) r1 = new StringReader(""); // NOI18N
358
r2 = setup.getSecondSource().createReader();
359
if (r2 == null) r2 = new StringReader(""); // NOI18N
360
differences = diff.computeDiff(r1, r2);
362
if (r1 != null) try { r1.close(); } catch (Exception e) {}
363
if (r2 != null) try { r2.close(); } catch (Exception e) {}
366
File file = setup.getBaseFile();
369
if (!SvnUtils.getMimeType(file).startsWith("text/") && differences.length == 0) {
370
// assume the file is binary
371
is = new ByteArrayInputStream(exportBinaryFile(file).getBytes("utf8")); // NOI18N
373
r1 = setup.getFirstSource().createReader();
374
if (r1 == null) r1 = new StringReader(""); // NOI18N
375
r2 = setup.getSecondSource().createReader();
376
if (r2 == null) r2 = new StringReader(""); // NOI18N
377
TextDiffVisualizer.TextDiffInfo info = new TextDiffVisualizer.TextDiffInfo(
378
relativePath + " " + setup.getFirstSource().getTitle(), // NOI18N
379
relativePath + " " + setup.getSecondSource().getTitle(), // NOI18N
386
info.setContextMode(true, 3);
387
String diffText = TextDiffVisualizer.differenceToUnifiedDiffText(info);
388
is = new ByteArrayInputStream(diffText.getBytes("utf8")); // NOI18N
396
if (r1 != null) try { r1.close(); } catch (Exception e) {}
397
if (r2 != null) try { r2.close(); } catch (Exception e) {}
402
* Utility method that returns all non-excluded modified files that are
403
* under given roots (folders) and have one of specified statuses.
405
* @param context context to search
406
* @param includeStatus bit mask of file statuses to include in result
407
* @return File [] array of Files having specified status
409
public static File [] getModifiedFiles(Context context, int includeStatus) {
410
File[] all = Subversion.getInstance().getStatusCache().listFiles(context, includeStatus);
411
List<File> files = new ArrayList<File>();
412
for (int i = 0; i < all.length; i++) {
417
// ensure that command roots (files that were explicitly selected by user) are included in Diff
418
FileStatusCache cache = Subversion.getInstance().getStatusCache();
419
File [] rootFiles = context.getRootFiles();
420
for (int i = 0; i < rootFiles.length; i++) {
421
File file = rootFiles[i];
422
if (file.isFile() && (cache.getStatus(file).getStatus() & includeStatus) != 0 && !files.contains(file)) {
426
return files.toArray(new File[files.size()]);
429
private String exportBinaryFile(File file) throws IOException {
430
ByteArrayOutputStream baos = new ByteArrayOutputStream();
431
StringBuilder sb = new StringBuilder((int) file.length());
432
if (file.canRead()) {
433
Utils.copyStreamsCloseAll(baos, new FileInputStream(file));
435
sb.append("MIME: application/octet-stream; encoding: Base64; length: " + (file.canRead() ? file.length() : -1)); // NOI18N
436
sb.append(System.getProperty("line.separator")); // NOI18N
437
sb.append(Base64Encoder.encode(baos.toByteArray(), true));
438
sb.append(System.getProperty("line.separator")); // NOI18N
439
return sb.toString();