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