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 |
} |