-
Notifications
You must be signed in to change notification settings - Fork 461
/
RemoteFolderTraversalTask.java
356 lines (312 loc) · 16.5 KB
/
RemoteFolderTraversalTask.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
package com.dotcms.api.client.files.traversal.task;
import com.dotcms.api.client.files.traversal.data.Retriever;
import com.dotcms.api.client.files.traversal.exception.TraversalTaskException;
import com.dotcms.api.client.task.TaskProcessor;
import com.dotcms.api.traversal.TreeNode;
import com.dotcms.model.asset.FolderView;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import javax.enterprise.context.Dependent;
import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.microprofile.context.ManagedExecutor;
import org.jboss.logging.Logger;
/**
* Recursive task for traversing a dotCMS remote location and building a hierarchical tree
* representation of its contents. This task is used to split the traversal into smaller sub-tasks
* that can be executed in parallel, allowing for faster traversal of large directory structures.
*/
@Dependent
public class RemoteFolderTraversalTask extends
TaskProcessor<RemoteFolderTraversalTaskParams, CompletableFuture<TraverseTaskResult>> {
private final ManagedExecutor executor;
private final Logger logger;
private final Retriever retriever;
private RemoteFolderTraversalTaskParams traversalTaskParams;
/**
* Constructs a new RemoteFolderTraversalTask instance.
*
* @param executor the executor for parallel execution of traversal tasks
* @param logger the logger for logging debug information
* @param retriever The retriever used for REST calls and other operations.
*/
public RemoteFolderTraversalTask(
final Logger logger,
final ManagedExecutor executor,
final Retriever retriever) {
this.executor = executor;
this.logger = logger;
this.retriever = retriever;
}
/**
* Sets the traversal parameters for the RemoteFolderTraversalTask. This method provides a way
* to inject necessary configuration after the instance of RemoteFolderTraversalTask has been
* created by the container, which is a common pattern when working with frameworks like Quarkus
* that manage object creation and dependency injection in a specific manner.
* <p>
* This method is used as an alternative to constructor injection, which is not feasible due to
* the limitations or constraints of the framework's dependency injection mechanism. It allows
* for the explicit setting of traversal parameters after the object's instantiation, ensuring
* that the executor is properly configured before use.
*
* @param params The traversal parameters
*/
@Override
public void setTaskParams(final RemoteFolderTraversalTaskParams params) {
this.traversalTaskParams = params;
}
/**
* Executes the folder traversal task and returns a TreeNode representing the directory tree
* rooted at the folder specified in the constructor.
*
* @return A Pair object containing a list of exceptions encountered during traversal and the
* resulting TreeNode representing the directory tree at the specified folder.
*/
@Override
public CompletableFuture<TraverseTaskResult> compute() {
var errors = new ArrayList<Exception>();
TreeNode currentNode = null;
FolderView currentFolder = null;
try {
// Processing the current folder
var processResult = processCurrentFolder();
currentNode = processResult.getLeft();
currentFolder = processResult.getRight();
} catch (Exception e) {
if (traversalTaskParams.failFast()) {
return CompletableFuture.failedFuture(e);
} else {
errors.add(e);
}
}
// And now its sub-folders
return processSubFolders(
currentNode,
currentFolder,
errors
);
}
/**
* Processes the current folder and returns a TreeNode representing it.
*
* @return A Pair object containing the TreeNode representing the processed folder and the
* FolderView object representing the current folder.
*/
private Pair<TreeNode, FolderView> processCurrentFolder() {
var currentNode = new TreeNode(traversalTaskParams.folder());
var currentFolder = traversalTaskParams.folder();
// Processing the very first level
if (traversalTaskParams.isRoot()) {
try {
// Make a REST call to fetch the root folder
currentFolder = this.retrieveFolderInformation(
traversalTaskParams.siteName(),
traversalTaskParams.folder().path(),
traversalTaskParams.folder().level(),
traversalTaskParams.folder().implicitGlobInclude(),
traversalTaskParams.folder().explicitGlobInclude(),
traversalTaskParams.folder().explicitGlobExclude()
);
// Using the values set by the filter in the root folder
var detailedFolder = traversalTaskParams.folder().withImplicitGlobInclude(
currentFolder.implicitGlobInclude());
detailedFolder = detailedFolder.withExplicitGlobInclude(
currentFolder.explicitGlobInclude());
detailedFolder = detailedFolder.withExplicitGlobExclude(
currentFolder.explicitGlobExclude());
currentNode = new TreeNode(detailedFolder);
// Add the fetched files to the root folder
if (currentFolder.assets() != null) {
currentNode.assets(currentFolder.assets().versions());
}
} catch (TraversalTaskException e) {
throw e;
} catch (Exception e) {
throw new TraversalTaskException(e.getMessage(), e);
}
}
return Pair.of(currentNode, currentFolder);
}
/**
* Processes the sub-folders of the current folder and returns a CompletableFuture containing a
* pair of a list of exceptions encountered during the traversal and a TreeNode representing the
* current folder.
*
* @param currentNode The current TreeNode representing the current folder.
* @param currentFolder The FolderView object representing the current folder to process.
* @param errors The list of exceptions to which any error should be added.
* @return a CompletableFuture containing a list of exceptions encountered during the traversal
* and a TreeNode representing the processed folder.
*/
private CompletableFuture<TraverseTaskResult> processSubFolders(
TreeNode currentNode, FolderView currentFolder, List<Exception> errors) {
// Traverse all sub-folders of the current folder
if (currentFolder != null && currentFolder.subFolders() != null) {
List<CompletableFuture<TraverseTaskResult>> futures = new ArrayList<>();
for (FolderView subFolder : currentFolder.subFolders()) {
if (traversalTaskParams.depth() == -1
|| subFolder.level() <= traversalTaskParams.depth()) {
try {
// Create a new task to traverse the sub-folder and add it to the list of sub-tasks
var task = searchForFolder(
traversalTaskParams.siteName(),
subFolder.path(),
subFolder.level(),
subFolder.implicitGlobInclude(),
subFolder.explicitGlobInclude(),
subFolder.explicitGlobExclude()
);
CompletableFuture<TraverseTaskResult> future =
CompletableFuture.supplyAsync(
task::compute, executor
).thenCompose(Function.identity());
futures.add(future);
} catch (Exception e) {
handleException(errors, e);
}
} else {
// Add the sub-folder to the current node if we've reached the maximum traversal depth
currentNode.addChild(new TreeNode(subFolder));
}
}
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(ignored -> {
for (CompletableFuture<TraverseTaskResult> future : futures) {
var taskResult = future.join();
errors.addAll(taskResult.exceptions());
taskResult.treeNode().ifPresent(currentNode::addChild);
}
return TraverseTaskResult.builder()
.exceptions(errors)
.treeNode(Optional.ofNullable(currentNode))
.build();
});
}
return CompletableFuture.completedFuture(TraverseTaskResult.builder()
.exceptions(errors)
.treeNode(Optional.ofNullable(currentNode))
.build());
}
/**
* Creates a new FolderTraversalTask instance to search for a folder with the specified
* parameters.
*
* @param siteName The name of the site containing the folder to search for.
* @param folderPath The path of the folder to search for.
* @param level The level of the folder to search for.
* @param implicitGlobInclude This property represents whether a folder should be implicitly
* included based on the absence of any include patterns. When
* implicitGlobInclude is set to true, it means that there are no
* include patterns specified, so all folders should be included by
* default. In other words, if there are no specific include patterns
* defined, the filter assumes that all folders should be included
* unless explicitly excluded.
* @param explicitGlobInclude This property represents whether a folder should be explicitly
* included based on the configured includes patterns for folders.
* When explicitGlobInclude is set to true, it means that the folder
* has matched at least one of the include patterns and should be
* included in the filtered result. The explicit inclusion takes
* precedence over other rules. If a folder is explicitly included,
* it will be included regardless of any other rules or patterns.
* @param explicitGlobExclude This property represents whether a folder should be explicitly
* excluded based on the configured excludes patterns for folders.
* When explicitGlobExclude is set to true, it means that the folder
* has matched at least one of the exclude patterns and should be
* excluded from the filtered result. The explicit exclusion takes
* precedence over other rules. If a folder is explicitly excluded,
* it will be excluded regardless of any other rules or patterns.
* @return A new FolderTraversalTask instance to search for the specified folder.
*/
private RemoteFolderTraversalTask searchForFolder(
final String siteName,
final String folderPath,
final int level,
final boolean implicitGlobInclude,
final Boolean explicitGlobInclude,
final Boolean explicitGlobExclude
) {
final var folder = this.retrieveFolderInformation(siteName, folderPath, level,
implicitGlobInclude, explicitGlobInclude, explicitGlobExclude);
var task = new RemoteFolderTraversalTask(
this.logger,
this.executor,
this.retriever
);
task.setTaskParams(RemoteFolderTraversalTaskParams.builder()
.filter(traversalTaskParams.filter())
.siteName(siteName)
.folder(folder)
.isRoot(false)
.depth(traversalTaskParams.depth())
.failFast(traversalTaskParams.failFast())
.build()
);
return task;
}
/**
* Retrieves the contents of a folder
*
* @param siteName The name of the site containing the folder
* @param folderPath The path of the folder to search for.
* @param level The hierarchical level of the folder
* @param implicitGlobInclude This property represents whether a folder should be implicitly
* included based on the absence of any include patterns. When
* implicitGlobInclude is set to true, it means that there are no
* include patterns specified, so all folders should be included by
* default. In other words, if there are no specific include patterns
* defined, the filter assumes that all folders should be included
* unless explicitly excluded.
* @param explicitGlobInclude This property represents whether a folder should be explicitly
* included based on the configured includes patterns for folders.
* When explicitGlobInclude is set to true, it means that the folder
* has matched at least one of the include patterns and should be
* included in the filtered result. The explicit inclusion takes
* precedence over other rules. If a folder is explicitly included,
* it will be included regardless of any other rules or patterns.
* @param explicitGlobExclude This property represents whether a folder should be explicitly
* excluded based on the configured excludes patterns for folders.
* When explicitGlobExclude is set to true, it means that the folder
* has matched at least one of the exclude patterns and should be
* excluded from the filtered result. The explicit exclusion takes
* precedence over other rules. If a folder is explicitly excluded,
* it will be excluded regardless of any other rules or patterns.
* @return an {@code FolderView} object containing the metadata for the requested folder
*/
private FolderView retrieveFolderInformation(final String siteName, final String folderPath,
final int level, final boolean implicitGlobInclude, final Boolean explicitGlobInclude,
final Boolean explicitGlobExclude) {
try {
var foundFolder = this.retriever.retrieveFolderInformation(
siteName,
folderPath,
level,
implicitGlobInclude,
explicitGlobInclude,
explicitGlobExclude
);
return traversalTaskParams.filter().apply(foundFolder);
} catch (Exception e) {
var message = String.format("Error retrieving folder information [%s]", folderPath);
logger.error(message, e);
throw new TraversalTaskException(message, e);
}
}
/**
* Handles an exception that occurred during the execution of a traversal task.
*
* @param errors The list of exceptions to which the error should be added.
* @param e The exception that occurred.
*/
private void handleException(List<Exception> errors, Exception e) {
if (traversalTaskParams.failFast()) {
if (e instanceof TraversalTaskException) {
throw (TraversalTaskException) e;
}
throw new TraversalTaskException(e.getMessage(), e);
} else {
errors.add(e);
}
}
}