2727 * <b>Example usage of {@code getNextPath}</b>
2828 * <pre>
2929 * ...
30- *
30+ *
3131 * /*
3232 * * This example gets the next path in sequence for base name `artifact`
3333 * * and extension `txt` in the TestNG output directory.
34- * *
35- * * For purposes of this example, the output directory already contains
34+ * *
35+ * * For purposes of this example, the output directory already contains
3636 * * the following files: `artifact.txt`, `artifact-3.txt`
3737 * */
38- *
38+ *
3939 * Path collectionPath = Paths.get(testContext.getOutputDirectory());
4040 * // => C:\git\my-project\test-output\Default suite
41- *
41+ *
4242 * Path artifactPath;
4343 * try {
4444 * artifactPath = PathUtils.getNextPath(collectionPath, "artifact", "txt");
4747 * provider.getLogger().info("Unable to get output path; no artifact was captured", e);
4848 * return;
4949 * }
50- *
50+ *
5151 * ...
5252 * </pre>
5353 */
@@ -56,15 +56,15 @@ public final class PathUtils {
5656 private PathUtils () {
5757 throw new AssertionError ("PathUtils is a static utility class that cannot be instantiated" );
5858 }
59-
59+
6060 private static final String SUREFIRE_PATH = "surefire-reports" ;
6161 private static final String FAILSAFE_PATH = "failsafe-reports" ;
62-
62+
6363 /**
6464 * This enumeration contains methods to help build proxy subclass names and select reports directories.
6565 */
6666 public enum ReportsDirectory {
67-
67+
6868 SUREFIRE_1 ("(Test)(.*)" , SUREFIRE_PATH ),
6969 SUREFIRE_2 ("(.*)(Test)" , SUREFIRE_PATH ),
7070 SUREFIRE_3 ("(.*)(Tests)" , SUREFIRE_PATH ),
@@ -73,45 +73,46 @@ public enum ReportsDirectory {
7373 FAILSAFE_2 ("(.*)(IT)" , FAILSAFE_PATH ),
7474 FAILSAFE_3 ("(.*)(ITCase)" , FAILSAFE_PATH ),
7575 ARTIFACT (".*" , "artifact-capture" );
76-
76+
7777 private String regex ;
7878 private String folder ;
79-
79+
8080 ReportsDirectory (String regex , String folder ) {
8181 this .regex = regex ;
8282 this .folder = folder ;
8383 }
84-
84+
8585 /**
8686 * Get the regular expression that matches class names for this constant.
87- *
87+ *
8888 * @return class-matching regular expression string
8989 */
9090 public String getRegEx () {
9191 return regex ;
9292 }
93-
93+
9494 /**
9595 * Get the name of the folder associated with this constant.
96- *
96+ *
9797 * @return class-related folder name
9898 */
9999 public String getFolder () {
100100 return folder ;
101101 }
102-
102+
103103 /**
104104 * Get the resolved Maven-derived path associated with this constant.
105- *
105+ *
106+ * @param subdirs optional sub-path
106107 * @return Maven folder path
107108 */
108- public Path getPath () {
109- return getTargetPath ().resolve (folder );
109+ public Path getPath (String ... subdirs ) {
110+ return getTargetPath ().resolve (Paths . get ( folder , subdirs ) );
110111 }
111-
112+
112113 /**
113114 * Get the reports directory constant for the specified test class object.
114- *
115+ *
115116 * @param obj test class object
116117 * @return reports directory constant
117118 */
@@ -124,31 +125,35 @@ public static ReportsDirectory fromObject(Object obj) {
124125 }
125126 throw new IllegalStateException ("Someone removed the 'default' pattern from this enumeration" );
126127 }
127-
128+
128129 /**
129130 * Get reports directory path for the specified test class object.
130- *
131+ *
131132 * @param obj test class object
132133 * @return reports directory path
133134 */
134135 public static Path getPathForObject (Object obj ) {
135- ReportsDirectory constant = fromObject (obj );
136- return getTargetPath ().resolve (constant .folder );
136+ String [] subdirs = {};
137+ if (obj instanceof PathModifier ) {
138+ String message = String .format ("Null path modifier returned by: %s" , obj .getClass ().getName ());
139+ subdirs = Objects .requireNonNull (((PathModifier ) obj ).getSubPath (), message );
140+ }
141+ return fromObject (obj ).getPath (subdirs );
137142 }
138-
143+
139144 /**
140145 * Get the path for the 'target' folder of the current project.
141- *
146+ *
142147 * @return path for project 'target' folder
143148 */
144149 private static Path getTargetPath () {
145- return Paths .get (getBaseDir (), "target" );
150+ return Paths .get (getBaseDir ()). resolve ( "target" );
146151 }
147152 }
148153
149154 /**
150155 * Get the next available path in sequence for the specified base name and extension in the specified folder.
151- *
156+ *
152157 * @param targetPath path to target directory for the next available path in sequence
153158 * @param baseName base name for the path sequence
154159 * @param extension extension for the path sequence
@@ -159,7 +164,7 @@ public static Path getNextPath(Path targetPath, String baseName, String extensio
159164 Objects .requireNonNull (targetPath , "[targetPath] must be non-null" );
160165 Objects .requireNonNull (baseName , "[baseName] must be non-null" );
161166 Objects .requireNonNull (extension , "[extension] must be non-null" );
162-
167+
163168 File targetFile = targetPath .toFile ();
164169 if ( ! (targetFile .exists () && targetFile .isDirectory ())) {
165170 throw new IllegalArgumentException ("[targetPath] must specify an existing directory" );
@@ -170,16 +175,16 @@ public static Path getNextPath(Path targetPath, String baseName, String extensio
170175 if (extension .isEmpty ()) {
171176 throw new IllegalArgumentException ("[extension] must specify a non-empty string" );
172177 }
173-
178+
174179 Visitor visitor = new Visitor (baseName , extension );
175180 Files .walkFileTree (targetPath , EnumSet .noneOf (FileVisitOption .class ), 1 , visitor );
176-
181+
177182 return targetPath .resolve (visitor .getNewName ());
178183 }
179184
180185 /**
181186 * Get project base directory.
182- *
187+ *
183188 * @return project base directory
184189 */
185190 public static String getBaseDir () {
@@ -188,7 +193,7 @@ public static String getBaseDir() {
188193 }
189194
190195 private static class Visitor implements FileVisitor <Path > {
191-
196+
192197 private String baseName ;
193198 private String extension ;
194199 private int base , ext ;
@@ -202,7 +207,7 @@ private static class Visitor implements FileVisitor<Path> {
202207 this .ext = extension .length () + 1 ;
203208 this .pathMatcher = FileSystems .getDefault ().getPathMatcher ("regex:\\ Q" + baseName + "\\ E(-\\ d+)?\\ ." + extension );
204209 }
205-
210+
206211 @ Override
207212 public FileVisitResult preVisitDirectory (Path dir , BasicFileAttributes attrs ) throws IOException {
208213 return FileVisitResult .CONTINUE ;
@@ -228,18 +233,65 @@ public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOExce
228233 public FileVisitResult postVisitDirectory (Path dir , IOException exc ) throws IOException {
229234 return FileVisitResult .CONTINUE ;
230235 }
231-
236+
232237 public String getNewName () {
233238 String newName ;
234-
239+
235240 if (intList .isEmpty ()) {
236241 newName = baseName + "." + extension ;
237242 } else {
238243 Collections .sort (intList , Collections .reverseOrder ());
239244 newName = baseName + "-" + intList .get (0 ) + "." + extension ;
240245 }
241-
246+
242247 return newName ;
243248 }
244249 }
250+
251+ /**
252+ * Prepend the specified string to the indicated array.
253+ *
254+ * @param prefix string to be prepended
255+ * @param strings target string array
256+ * @return target array prefixed with the specified string
257+ */
258+ public static String [] prepend (String prefix , String ... strings ) {
259+ int len = strings .length ;
260+ String [] temp = new String [len + 1 ];
261+ if (len > 0 ) System .arraycopy (strings , 0 , temp , 1 , len );
262+ temp [0 ] = prefix ;
263+ return temp ;
264+ }
265+
266+ /**
267+ * Append the specified string to the indicated array.
268+ *
269+ * @param suffix string to be appended
270+ * @param strings target string array
271+ * @return target array with the specified string appended
272+ */
273+ public static String [] append (String suffix , String ... strings ) {
274+ int len = strings .length ;
275+ String [] temp = new String [len + 1 ];
276+ if (len > 0 ) System .arraycopy (strings , 0 , temp , 0 , len );
277+ temp [len ] = suffix ;
278+ return temp ;
279+ }
280+
281+ /**
282+ * Classes that implement this interface are called to supply additional elements for the path returned by
283+ * {@link ReportsDirectory#getPathForObject(Object)}. This enables the implementing class to partition artifacts
284+ * based on scenario-specific criteria.
285+ */
286+ public interface PathModifier {
287+
288+ /**
289+ * Get scenario-specific path modifier for {@link ReportsDirectory#getPathForObject(Object)}.
290+ *
291+ * @return scenario-specific path modifier
292+ */
293+ public String [] getSubPath ();
294+
295+ }
296+
245297}
0 commit comments