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

(-)bin/system.properties (+25 lines)
Lines 111-113 Link Here
111
# This property can be defined if JMeter cannot find the application automatically
111
# This property can be defined if JMeter cannot find the application automatically
112
# It should not be necessary in most cases.
112
# It should not be necessary in most cases.
113
#keytool.directory=<Java Home Directory>/bin
113
#keytool.directory=<Java Home Directory>/bin
114
115
#---------------------------------------------------------------------------
116
# JMX Backup configuration
117
#---------------------------------------------------------------------------
118
#Enable auto backups of the .jmx file when a test plan is saved.
119
#When enabled, before the .jmx is saved, it will be backed up to the directory pointed
120
#by the jmeter.gui.action.save.backup_directory property (see below). Backup file names are built
121
#after the jmx file being saved. For example, saving test-plan.jmx will create a test-plan-000012.jmx
122
#in the backup directory provided that the last created backup file is test-plan-000011.jmx.
123
#Default value is true indicating that auto backups are enabled
124
#jmeter.gui.action.save.backup_on_save=true
125
126
#Set the backup directory path where JMX backups will be created upon save in the GUI.
127
#If not set (what it defaults to) then backup files will be created in
128
#a sub-directory of the JMeter base installation. The default directory is ${JMETER_HOME}/backups
129
#If set and the directory does not exist, it will be created.
130
#jmeter.gui.action.save.backup_directory=
131
132
#Set the maximum time (in hours) that backup files should be preserved since the save time.
133
#By default no expiration time is set which means we keep backups for ever.
134
#jmeter.gui.action.save.keep_backup_max_hours=0
135
136
#Set the maximum number of backup files that should be preserved. By default 10 backups will be preserved.
137
#Setting this to zero will cause the backups to not being deleted (unless keep_backup_max_hours is set to a non zero value)
138
#jmeter.gui.action.save.keep_backup_max_count=10
(-)src/core/org/apache/jmeter/gui/action/Save.java (-1 / +221 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.DecimalFormat;
26
import java.util.ArrayList;
27
import java.util.Calendar;
28
import java.util.Collections;
29
import java.util.Comparator;
24
import java.util.HashSet;
30
import java.util.HashSet;
25
import java.util.Iterator;
31
import java.util.Iterator;
26
import java.util.LinkedList;
32
import java.util.LinkedList;
33
import java.util.List;
27
import java.util.Set;
34
import java.util.Set;
35
import java.util.regex.Matcher;
36
import java.util.regex.Pattern;
28
37
29
import javax.swing.JFileChooser;
38
import javax.swing.JFileChooser;
30
import javax.swing.JOptionPane;
39
import javax.swing.JOptionPane;
31
40
41
import org.apache.commons.io.FileUtils;
32
import org.apache.commons.io.FilenameUtils;
42
import org.apache.commons.io.FilenameUtils;
43
import org.apache.commons.io.filefilter.FileFilterUtils;
44
import org.apache.commons.io.filefilter.IOFileFilter;
33
import org.apache.jmeter.control.gui.TestFragmentControllerGui;
45
import org.apache.jmeter.control.gui.TestFragmentControllerGui;
34
import org.apache.jmeter.engine.TreeCloner;
46
import org.apache.jmeter.engine.TreeCloner;
35
import org.apache.jmeter.exceptions.IllegalUserActionException;
47
import org.apache.jmeter.exceptions.IllegalUserActionException;
Lines 56-63 Link Here
56
public class Save implements Command {
68
public class Save implements Command {
57
    private static final Logger log = LoggingManager.getLoggerForClass();
69
    private static final Logger log = LoggingManager.getLoggerForClass();
58
70
71
    private static final List<File> EMPTY_FILE_LIST = Collections.emptyList();
72
    
73
    private static final String JMX_BACKUP_ON_SAVE = "jmeter.gui.action.save.backup_on_save"; // $NON-NLS-1$
74
75
    private static final String JMX_BACKUP_DIRECTORY = "jmeter.gui.action.save.backup_directory"; // $NON-NLS-1$
76
    
77
    private static final String JMX_BACKUP_MAX_HOURS = "jmeter.gui.action.save.keep_backup_max_hours"; // $NON-NLS-1$
78
    
79
    private static final String JMX_BACKUP_MAX_COUNT = "jmeter.gui.action.save.keep_backup_max_count"; // $NON-NLS-1$
80
    
59
    public static final String JMX_FILE_EXTENSION = ".jmx"; // $NON-NLS-1$
81
    public static final String JMX_FILE_EXTENSION = ".jmx"; // $NON-NLS-1$
60
82
83
    private static final String DEFAULT_BACKUP_DIRECTORY = JMeterUtils.getJMeterHome() + "/backups"; //$NON-NLS-1$
84
    
85
    // Whether we should keep backups for save JMX files. Default is to enable backup
86
    private static final boolean BACKUP_ENABLED = JMeterUtils.getPropDefault(JMX_BACKUP_ON_SAVE, true);
87
    
88
    // Path to the backup directory
89
    private static final String BACKUP_DIRECTORY = JMeterUtils.getPropDefault(JMX_BACKUP_DIRECTORY, DEFAULT_BACKUP_DIRECTORY);
90
    
91
    // Backup files expiration in hours. Default is to never expire (zero value).
92
    private static final int BACKUP_MAX_HOURS = JMeterUtils.getPropDefault(JMX_BACKUP_MAX_HOURS, 0);
93
    
94
    // Max number of backup files. Default is to limit to 10 backups max.
95
    private static final int BACKUP_MAX_COUNT = JMeterUtils.getPropDefault(JMX_BACKUP_MAX_COUNT, 10);
96
97
    // NumberFormat to format version number in backup file names
98
    private static final DecimalFormat BACKUP_VERSION_FORMATER = new DecimalFormat("000000"); //$NON-NLS-1$
99
    
61
    private static final Set<String> commands = new HashSet<String>();
100
    private static final Set<String> commands = new HashSet<String>();
62
101
63
    static {
102
    static {
Lines 166-173 Link Here
166
                GuiPackage.getInstance().setTestPlanFile(updateFile);
205
                GuiPackage.getInstance().setTestPlanFile(updateFile);
167
            }
206
            }
168
        }
207
        }
169
208
        
209
        // backup existing file according to jmeter/user.properties settings
210
        List<File> expiredBackupFiles = EMPTY_FILE_LIST;
211
        File fileToBackup = new File(updateFile);
170
        try {
212
        try {
213
            expiredBackupFiles = createBackupFile(fileToBackup);
214
        } catch (Exception ex) {
215
            log.error("Failed to create a backup for " + fileToBackup.getName(), ex); //$NON-NLS-1$
216
        }
217
        
218
        try {
171
            convertSubTree(subTree);
219
            convertSubTree(subTree);
172
        } catch (Exception err) {
220
        } catch (Exception err) {
173
            log.warn("Error converting subtree "+err);
221
            log.warn("Error converting subtree "+err);
Lines 181-186 Link Here
181
                subTree = GuiPackage.getInstance().getTreeModel().getTestPlan(); // refetch, because convertSubTree affects it
229
                subTree = GuiPackage.getInstance().getTreeModel().getTestPlan(); // refetch, because convertSubTree affects it
182
                ActionRouter.getInstance().doActionNow(new ActionEvent(subTree, e.getID(), ActionNames.SUB_TREE_SAVED));
230
                ActionRouter.getInstance().doActionNow(new ActionEvent(subTree, e.getID(), ActionNames.SUB_TREE_SAVED));
183
            }
231
            }
232
            
233
            // delete expired backups : here everything went right so we can
234
            // proceed to deletion
235
            for (File expiredBackupFile : expiredBackupFiles) {
236
                try {
237
                    FileUtils.deleteQuietly(expiredBackupFile);
238
                } catch (Exception ex) {
239
                    log.warn("Failed to delete backup file " + expiredBackupFile.getName()); //$NON-NLS-1$
240
                }
241
            }
184
        } catch (Throwable ex) {
242
        } catch (Throwable ex) {
185
            log.error("Error saving tree:", ex);
243
            log.error("Error saving tree:", ex);
186
            if (ex instanceof Error){
244
            if (ex instanceof Error){
Lines 195-201 Link Here
195
        }
253
        }
196
        GuiPackage.getInstance().updateCurrentGui();
254
        GuiPackage.getInstance().updateCurrentGui();
197
    }
255
    }
256
    
257
    /**
258
     * <p>
259
     * Create a backup copy of the specified file whose name will be
260
     * <code>{baseName}-{version}.jmx</code><br>
261
     * Where :<br>
262
     * <code>{baseName}</code> is the name of the file to backup without its
263
     * <code>.jmx</code> extension. For a file named <code>testplan.jmx</code>
264
     * it would then be <code>testplan</code><br>
265
     * <code>{version}</code> is the version number automatically incremented
266
     * after the higher version number of pre-existing backup files. <br>
267
     * <br>
268
     * Example: <code>testplan-000028.jmx</code> <br>
269
     * <br>
270
     * If <code>jmeter.gui.action.save.backup_directory</code> is <b>not</b>
271
     * set, then backup files will be created in
272
     * <code>${JMETER_HOME}/backups</code>
273
     * </p>
274
     * <p>
275
     * Backup process is controlled by the following jmeter/user properties :<br>
276
     * <table border=1>
277
     * <tr>
278
     * <th align=left>Property</th>
279
     * <th align=left>Type/Value</th>
280
     * <th align=left>Description</th>
281
     * </tr>
282
     * <tr>
283
     * <td><code>jmeter.gui.action.save.backup_on_save</code></td>
284
     * <td><code>true|false</code></td>
285
     * <td>Enables / Disables backup</td>
286
     * </tr>
287
     * <tr>
288
     * <td><code>jmeter.gui.action.save.backup_directory</code></td>
289
     * <td><code>/path/to/backup/directory</code></td>
290
     * <td>Set the directory path where backups will be stored upon save. If not
291
     * set then backups will be created in <code>${JMETER_HOME}/backups</code><br>
292
     * If that directory does not exist, it will be created</td>
293
     * </tr>
294
     * <tr>
295
     * <td><code>jmeter.gui.action.save.keep_backup_max_hours</code></td>
296
     * <td><code>integer</code></td>
297
     * <td>Maximum number of hours to preserve backup files. Backup files whose
298
     * age exceeds that limit should be deleted and will be added to this method
299
     * returned list</td>
300
     * </tr>
301
     * <tr>
302
     * <td><code>jmeter.gui.action.save.keep_backup_max_count</code></td>
303
     * <td><code>integer</code></td>
304
     * <td>Max number of backup files to be preserved. Exceeding backup files
305
     * should be deleted and will be added to this method returned list. Only
306
     * the most recent files will be preserved.</td>
307
     * </tr>
308
     * </table>
309
     * </p>
310
     * 
311
     * @param fileToBackup
312
     *            The file to create a backup from
313
     * @return A list of expired backup files selected according to the above
314
     *         properties and that should be deleted after the save operation
315
     *         has performed successfully
316
     */
317
    private List<File> createBackupFile(File fileToBackup) {
318
        if (!BACKUP_ENABLED) {
319
            return EMPTY_FILE_LIST;
320
        }
321
        char versionSeparator = '-'; //$NON-NLS-1$
322
        String baseName = fileToBackup.getName();
323
        // remove .jmx extension if any
324
        baseName = baseName.endsWith(JMX_FILE_EXTENSION) ? baseName.substring(0, baseName.length() - JMX_FILE_EXTENSION.length()) : baseName;
325
        // get a file to the backup directory
326
        File backupDir = new File(BACKUP_DIRECTORY);
327
        backupDir.mkdirs();
328
        if (!backupDir.isDirectory()) {
329
            log.error("Could not backup file ! Backup directory does not exist, is not a directory or could not be created ! <" + backupDir.getAbsolutePath() + ">"); //$NON-NLS-1$ //$NON-NLS-2$
330
        }
198
331
332
        // select files matching
333
        // {baseName}{versionSeparator}{version}{jmxExtension}
334
        // where {version} is a 6 digits number
335
        String backupPatternRegex = Pattern.quote(baseName + versionSeparator) + "([\\d]{6})" + Pattern.quote(JMX_FILE_EXTENSION); //$NON-NLS-1$
336
        Pattern backupPattern = Pattern.compile(backupPatternRegex);
337
        // create a file filter that select files matching a given regex pattern
338
        IOFileFilter patternFileFilter = new PrivatePatternFileFilter(backupPattern);
339
        // get all backup files in the backup directory
340
        List<File> backupFiles = new ArrayList<File>(FileUtils.listFiles(backupDir, patternFileFilter, null));
341
        // find the highest version number among existing backup files (this
342
        // should be the more recent backup)
343
        int lastVersionNumber = 0;
344
        for (File backupFile : backupFiles) {
345
            Matcher matcher = backupPattern.matcher(backupFile.getName());
346
            if (matcher.find() && matcher.groupCount() > 0) {
347
                // parse version number from the backup file name
348
                // should never fail as it matches the regex
349
                int version = Integer.parseInt(matcher.group(1));
350
                lastVersionNumber = Math.max(lastVersionNumber, version);
351
            }
352
        }
353
        // find expired backup files
354
        List<File> expiredFiles = new ArrayList<File>();
355
        if (BACKUP_MAX_HOURS > 0) {
356
            Calendar cal = Calendar.getInstance();
357
            cal.add(Calendar.HOUR_OF_DAY, -BACKUP_MAX_HOURS);
358
            long expiryDate = cal.getTime().getTime();
359
            // select expired files that should be deleted
360
            IOFileFilter expiredFileFilter = FileFilterUtils.ageFileFilter(expiryDate, true);
361
            expiredFiles.addAll(FileFilterUtils.filterList(expiredFileFilter, backupFiles));
362
        }
363
        // sort backups from by their last modified time
364
        Collections.sort(backupFiles, new Comparator<File>() {
365
            @Override
366
            public int compare(File o1, File o2) {
367
                long diff = o1.lastModified() - o2.lastModified();
368
                // convert the long to an int in order to comply with the method
369
                // contract
370
                return diff < 0 ? -1 : diff > 0 ? 1 : 0;
371
            }
372
        });
373
        // backup name is of the form
374
        // {baseName}{versionSeparator}{version}{jmxExtension}
375
        String backupName = baseName + versionSeparator + BACKUP_VERSION_FORMATER.format(lastVersionNumber + 1) + JMX_FILE_EXTENSION;
376
        File backupFile = new File(backupDir, backupName);
377
        // create file backup
378
        try {
379
            FileUtils.copyFile(fileToBackup, backupFile);
380
        } catch (IOException e) {
381
            log.error("Failed to backup file :" + fileToBackup.getAbsolutePath(), e); //$NON-NLS-1$
382
            return EMPTY_FILE_LIST;
383
        }
384
        // add the fresh new backup file (list is still sorted here)
385
        backupFiles.add(backupFile);
386
        // unless max backups is not set, ensure that we don't keep more backups
387
        // than required
388
        if (BACKUP_MAX_COUNT > 0 && backupFiles.size() > BACKUP_MAX_COUNT) {
389
            // keep the most recent files in the limit of the specified max
390
            // count
391
            expiredFiles.addAll(backupFiles.subList(0, backupFiles.size() - BACKUP_MAX_COUNT));
392
        }
393
        return expiredFiles;
394
    }
395
199
    /**
396
    /**
200
     * Check nodes does not contain a node of type TestPlan or ThreadGroup
397
     * Check nodes does not contain a node of type TestPlan or ThreadGroup
201
     * @param nodes
398
     * @param nodes
Lines 221-224 Link Here
221
            tree.replaceKey(item, testElement);
418
            tree.replaceKey(item, testElement);
222
        }
419
        }
223
    }
420
    }
421
    
422
    private static class PrivatePatternFileFilter implements IOFileFilter {
423
        
424
        private Pattern pattern;
425
        
426
        public PrivatePatternFileFilter(Pattern pattern) {
427
            if(pattern == null) {
428
                throw new IllegalArgumentException("pattern cannot be null !"); //$NON-NLS-1$
429
            }
430
            this.pattern = pattern;
431
        }
432
        
433
        @Override
434
        public boolean accept(File dir, String fileName) {
435
            return pattern.matcher(fileName).matches();
436
        }
437
        
438
        @Override
439
        public boolean accept(File file) {
440
            return accept(file.getParentFile(), file.getName());
441
        }
442
    }
443
    
224
}
444
}
(-)xdocs/usermanual/hints_and_tips.xml (-1 / +24 lines)
Lines 113-118 Link Here
113
</subsection>
113
</subsection>
114
</section>
114
</section>
115
115
116
116
<subsection name="&sect-num;.5 Autosave process configuration" anchor="autosave">
117
<description>
118
    <p>Since JMeter 2.14, JMeter automatically saves up to 10 backups of every saved jmx files. When enabled, just before the .jmx is saved,
119
    it will be backed up to the ${JMETER_HOME}/backups subfolder. Backup files are named after the saved jmx file and assigned a
120
    version number that is automatically incremented, ex: test-plan-000001.jmx, test-plan-000002.jmx, test-plan-000003.jmx, etc.
121
    To control auto-backup, add the following properties to user.properties.
122
    To enable/disable auto-backup, set the following property to true/false (default is true): 
123
    <source>jmeter.gui.action.save.backup_on_save=false</source>
124
    The backup directory can also be set to a different location. Setting the following property to the path of the desired directory
125
    will cause backup files to be stored inside instead of the ${JMETER_HOME}/backups folder. If the specified directory does not exist
126
    it will be created. Leaving this property unset will cause the ${JMETER_HOME}/backups folder to be used.
127
    <source>jmeter.gui.action.save.backup_directory=/path/to/backups/dir</source>
128
    You can also configure the maximum time (in hours) that backup files should be preserved since the most recent save time.
129
    By default a zero expiration time is set which instructs JMeter to preserve backup files for ever.
130
    Use the following property to control max preservation time :
131
    <source>jmeter.gui.action.save.keep_backup_max_hours=0</source>
132
    You can set the maximum number of backup files that should be preserved. By default 10 backups will be kept.
133
    Setting this to zero will cause the backups to never being deleted (unless keep_backup_max_hours is set to a non nul value)
134
    Maximum backup files selection is processed _after_ time expiration selection, so even if you set 1 year as the expiry time, only the max_count
135
    most recent backups files will be kept.
136
    <source>jmeter.gui.action.save.keep_backup_max_count=10</source>
137
    </p>
138
</description>
139
</subsection>
117
</body>
140
</body>
118
</document>
141
</document>

Return to bug 57913