View | Details | Raw Unified | Return to bug 57913
Collapse All | Expand All

(-)bin/user.properties (+17 lines)
Lines 62-64 Link Here
62
62
63
# Enable Proxy request debug
63
# Enable Proxy request debug
64
#log_level.jmeter.protocol.http.proxy.HttpRequestHdr=DEBUG
64
#log_level.jmeter.protocol.http.proxy.HttpRequestHdr=DEBUG
65
66
#---------------------------------------------------------------------------
67
# JMX Backup configuration
68
#---------------------------------------------------------------------------
69
#Enable backups of the .jmx file when the test plan is saved.
70
#When enabled, before the .jmx is saved, it will be backed up to a subfolder whose name is built
71
#from the .jmx file and suffixed by .backups, ex. test-plan.jmx.backups
72
#Backup file names are suffixed by their save date, ex: test-plan.jmx.20150501.150027
73
#jmeter.gui.action.save.backup_when_saving=true
74
75
#Set the maximum time (in hours) the backup files should be preserved since the most recent save time.
76
#By default no expiration time is set which means we keep backups for ever.
77
#jmeter.gui.action.save.keep_backup_max_hours=0
78
79
#Set the maximum number of backup files that should be preserved. By default 10 backups will be kept.
80
#Setting this to zero will cause the backups to not being deleted (unless keep_backup_max_hours is set to a non nul value)
81
#jmeter.gui.action.save.keep_backup_max_count=10
(-)src/core/org/apache/jmeter/gui/action/Save.java (-1 / +211 lines)
Lines 21-35 Link Here
21
import java.awt.event.ActionEvent;
21
import java.awt.event.ActionEvent;
22
import java.io.File;
22
import java.io.File;
23
import java.io.FileOutputStream;
23
import java.io.FileOutputStream;
24
import java.io.IOException;
25
import java.text.DateFormat;
26
import java.text.ParseException;
27
import java.text.SimpleDateFormat;
28
import java.util.ArrayList;
29
import java.util.Calendar;
30
import java.util.Collections;
31
import java.util.Comparator;
32
import java.util.Date;
24
import java.util.HashSet;
33
import java.util.HashSet;
25
import java.util.Iterator;
34
import java.util.Iterator;
26
import java.util.LinkedList;
35
import java.util.LinkedList;
36
import java.util.List;
27
import java.util.Set;
37
import java.util.Set;
28
38
29
import javax.swing.JFileChooser;
39
import javax.swing.JFileChooser;
30
import javax.swing.JOptionPane;
40
import javax.swing.JOptionPane;
31
41
42
import org.apache.commons.io.FileUtils;
32
import org.apache.commons.io.FilenameUtils;
43
import org.apache.commons.io.FilenameUtils;
44
import org.apache.commons.io.filefilter.IOFileFilter;
45
import org.apache.commons.lang3.StringUtils;
33
import org.apache.jmeter.control.gui.TestFragmentControllerGui;
46
import org.apache.jmeter.control.gui.TestFragmentControllerGui;
34
import org.apache.jmeter.engine.TreeCloner;
47
import org.apache.jmeter.engine.TreeCloner;
35
import org.apache.jmeter.exceptions.IllegalUserActionException;
48
import org.apache.jmeter.exceptions.IllegalUserActionException;
Lines 56-61 Link Here
56
public class Save implements Command {
69
public class Save implements Command {
57
    private static final Logger log = LoggingManager.getLoggerForClass();
70
    private static final Logger log = LoggingManager.getLoggerForClass();
58
71
72
    private static final String JMX_BACKUP_WHEN_SAVING = "jmeter.gui.action.save.backup_when_saving";
73
    
74
    private static final String JMX_BACKUP_MAX_HOURS = "jmeter.gui.action.save.keep_backup_max_hours";
75
    
76
    private static final String JMX_BACKUP_MAX_COUNT = "jmeter.gui.action.save.keep_backup_max_count";
77
    
59
    public static final String JMX_FILE_EXTENSION = ".jmx"; // $NON-NLS-1$
78
    public static final String JMX_FILE_EXTENSION = ".jmx"; // $NON-NLS-1$
60
79
61
    private static final Set<String> commands = new HashSet<String>();
80
    private static final Set<String> commands = new HashSet<String>();
Lines 166-172 Link Here
166
                GuiPackage.getInstance().setTestPlanFile(updateFile);
185
                GuiPackage.getInstance().setTestPlanFile(updateFile);
167
            }
186
            }
168
        }
187
        }
169
188
        
189
        // backup existing file according to user.properties settings
190
        List<File> backupFilesToBeDeleted = Collections.emptyList();
191
        File fileToBackup = new File(updateFile);
192
        try {
193
            backupFilesToBeDeleted = createBackupFile(fileToBackup);
194
        } catch (Exception ex) {
195
            log.error("Failed to create a backup for " + fileToBackup.getName(), ex);
196
        }
197
        
170
        try {
198
        try {
171
            convertSubTree(subTree);
199
            convertSubTree(subTree);
172
        } catch (Exception err) {
200
        } catch (Exception err) {
Lines 181-186 Link Here
181
                subTree = GuiPackage.getInstance().getTreeModel().getTestPlan(); // refetch, because convertSubTree affects it
209
                subTree = GuiPackage.getInstance().getTreeModel().getTestPlan(); // refetch, because convertSubTree affects it
182
                ActionRouter.getInstance().doActionNow(new ActionEvent(subTree, e.getID(), ActionNames.SUB_TREE_SAVED));
210
                ActionRouter.getInstance().doActionNow(new ActionEvent(subTree, e.getID(), ActionNames.SUB_TREE_SAVED));
183
            }
211
            }
212
            
213
            // delete deletable backups : here everything when right so we can
214
            // proceed to deletion
215
            for (File deleteMe : backupFilesToBeDeleted) {
216
                try {
217
                    FileUtils.deleteQuietly(deleteMe);
218
                } catch (Exception ex) {
219
                    log.warn("Failed to delete backup file " + deleteMe.getName());
220
                }
221
            }
184
        } catch (Throwable ex) {
222
        } catch (Throwable ex) {
185
            log.error("Error saving tree:", ex);
223
            log.error("Error saving tree:", ex);
186
            if (ex instanceof Error){
224
            if (ex instanceof Error){
Lines 195-200 Link Here
195
        }
233
        }
196
        GuiPackage.getInstance().updateCurrentGui();
234
        GuiPackage.getInstance().updateCurrentGui();
197
    }
235
    }
236
    
237
    /**
238
     * <p>
239
     * Create a backup copy of the specified file whose name will be formed as
240
     * follows:<br>
241
     * <code>{file_name}.{date_as_yyyyMMdd-HHmmss}</code><br>
242
     * <br>
243
     * The backup copy will reside in a subfolder whose name will be the
244
     * specified file name suffixed by .backups.
245
     * </p>
246
     * <p>
247
     * Backup is controlled by the following user properties :<br>
248
     * <li>
249
     * <code>jmeter.gui.action.save.backup_when_saving : (true|false) Enables or Disables backup</code>
250
     * </li>
251
     * <li>
252
     * <code>jmeter.gui.action.save.keep_backup_max_hours : (int) Number of hours to keep backups since the most recent save time</code>
253
     * </li>
254
     * <li>
255
     * <code>jmeter.gui.action.save.keep_backup_max_count : (int) Max number of backups to keep</code>
256
     * </li>
257
     * </p>
258
     * 
259
     * @param file
260
     *            The file to create a backup from
261
     * @return A list of files that should be deleted after backup has been done
262
     *         according to properties defined in <code>user.properties</code>
263
     */
264
    private List<File> createBackupFile(File file) {
265
        List<File> emptyList = Collections.emptyList();
266
        final char suffixSeparator = '.';
267
        final String suffixDateFormatPattern = "yyyyMMdd-HHmmss";
268
        boolean shouldBackup = JMeterUtils.getPropDefault(JMX_BACKUP_WHEN_SAVING, true);
269
        if (!shouldBackup) {
270
            return emptyList;
271
        }
272
        // No expiration date is set by default
273
        int keepHours = JMeterUtils.getPropDefault(JMX_BACKUP_MAX_HOURS, 0);
274
        // Limit to 10 backups max by default
275
        int keepCount = JMeterUtils.getPropDefault(JMX_BACKUP_MAX_COUNT, 10);
276
        SimpleDateFormat suffixDateFormat = new SimpleDateFormat(suffixDateFormatPattern);
277
        Date now = new Date();
278
        final String baseName = file.getName();
279
        final String backupName = baseName + suffixSeparator + suffixDateFormat.format(now);
280
        File backupDir = new File(file.getParentFile(), baseName + ".backups"); //$NON-NLS-1$
281
        File backupFile = new File(backupDir, backupName);
282
        try {
283
            backupDir.mkdirs();
284
            FileUtils.copyFile(file, backupFile);
285
        } catch (IOException e) {
286
            log.error("Failed to backup file :" + file.getAbsolutePath(), e); //$NON-NLS-1$
287
            return emptyList;
288
        }
289
        PrivateDateSuffixFileFilter dateSuffixFileFilter = new PrivateDateSuffixFileFilter(baseName, suffixSeparator, suffixDateFormat);
290
        List<File> backupFiles = new ArrayList<File>(FileUtils.listFiles(backupDir, dateSuffixFileFilter, null));
291
        List<File> toBeDeleted = new ArrayList<File>();
292
        if (keepHours > 0) {
293
            Calendar cal = Calendar.getInstance();
294
            cal.setTime(now);
295
            cal.add(Calendar.HOUR_OF_DAY, -keepHours);
296
            Date cutoffDate = cal.getTime();
297
            // mark files older than the oldest date to be deleted
298
            for (File f : backupFiles) {
299
                String dateSuffix = f.getName().substring(baseName.length() + 1);
300
                try {
301
                    if (suffixDateFormat.parse(dateSuffix).before(cutoffDate)) {
302
                        toBeDeleted.add(f);
303
                    }
304
                } catch (ParseException pe) {
305
                    // should never happen but ignore and do not mark for
306
                    // deletion
307
                    log.error("Wrong backup file suffix for " + f.getName()); //$NON-NLS-1$
308
                }
309
            }
310
        }
311
        // ensure that we keep at most keepCount backups unless keepCount is not
312
        // set
313
        if (keepCount > 0) {
314
            if (backupFiles.size() > keepCount) {
315
                // we keep only younger backups
316
                Collections.sort(backupFiles, new Comparator<File>() {
317
                    @Override
318
                    public int compare(File o1, File o2) {
319
                        return o1.getName().compareTo(o2.getName());
320
                    }
321
                });
322
                List<File> youngestMaxCountFiles = backupFiles.subList(backupFiles.size() - keepCount, backupFiles.size());
323
                List<File> oldestFiles = new ArrayList<File>(backupFiles);
324
                oldestFiles.removeAll(youngestMaxCountFiles);
325
                toBeDeleted.addAll(oldestFiles);
326
            }
327
        }
328
        return toBeDeleted;
329
    }
198
330
199
    /**
331
    /**
200
     * Check nodes does not contain a node of type TestPlan or ThreadGroup
332
     * Check nodes does not contain a node of type TestPlan or ThreadGroup
Lines 221-224 Link Here
221
            tree.replaceKey(item, testElement);
353
            tree.replaceKey(item, testElement);
222
        }
354
        }
223
    }
355
    }
356
    
357
    /**
358
     * <p>
359
     * Filter to be used with commons-io that select files whose names matches
360
     * the following format:<br>
361
     * <code>{prefix}{separator}{date_format_suffix}</code> <br>
362
     * <br/>
363
     * Example: <code>test-plan.jmx.20150101.152014</code>
364
     * </p>
365
     */
366
    private static class PrivateDateSuffixFileFilter implements IOFileFilter {
367
368
        private String fileNamePrefix;
369
        private DateFormat suffixDateFormat;
370
        private char suffixSeparator;
371
372
        /**
373
         * Create a new IOFileFilter that will accept files whose name matches
374
         * this pattern: <code>{prefix}{separator}{date_suffix}</code>
375
         * 
376
         * @param fileNamePrefix
377
         *            Filename prefix to be searched
378
         * @param suffixSeparator
379
         *            Character used as the suffix separator
380
         * @param suffixDateFormat
381
         *            The {@link DateFormat} to be used to elect files whose
382
         *            suffix are parseable by that {@link DateFormat}
383
         */
384
        public PrivateDateSuffixFileFilter(String fileNamePrefix, char suffixSeparator, DateFormat suffixDateFormat) {
385
            if (StringUtils.isEmpty(fileNamePrefix)) {
386
                throw new IllegalArgumentException("baseFileName cannot be null or empty !"); //$NON-NLS-1$
387
            }
388
            if (suffixDateFormat == null) {
389
                throw new IllegalArgumentException("suffixDateFormat cannot be null !"); //$NON-NLS-1$
390
            }
391
            this.fileNamePrefix = fileNamePrefix;
392
            this.suffixDateFormat = suffixDateFormat;
393
            this.suffixSeparator = suffixSeparator;
394
        }
395
396
        @Override
397
        public boolean accept(File dir, String name) {
398
            if(name.equals(fileNamePrefix)) {
399
                return false;
400
            }
401
            boolean accept = false;
402
            int prefixIdx = name.indexOf(fileNamePrefix);
403
            // ensure file name starts with the good prefix and that it is
404
            // longer
405
            if (prefixIdx == 0 && name.length() > fileNamePrefix.length()) {
406
                // look for suffix
407
                String suffix = name.substring(fileNamePrefix.length());
408
                // first char after prefix must be the suffix separator
409
                if (suffix.charAt(0) == suffixSeparator && suffix.length() > 1) {
410
                    try {
411
                        // check that suffix matches the date format
412
                        suffixDateFormat.parse(suffix.substring(1));
413
                        accept = true;
414
                    } catch (ParseException pe) {
415
                        // suffix is not of the right format
416
                        accept = false;
417
                    }
418
                } else {
419
                    // suffix separator is wrong
420
                    accept = false;
421
                }
422
            } else {
423
                // file name does not start with the baseFileName
424
                accept = false;
425
            }
426
            return accept;
427
        }
428
429
        @Override
430
        public boolean accept(File file) {
431
            return accept(file.getParentFile(), file.getName());
432
        }
433
    }
224
}
434
}

Return to bug 57913