-
Notifications
You must be signed in to change notification settings - Fork 461
/
FilePullHandler.java
313 lines (253 loc) · 11.8 KB
/
FilePullHandler.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
package com.dotcms.api.client.pull.file;
import static com.dotcms.api.client.pull.file.OptionConstants.INCLUDE_EMPTY_FOLDERS;
import static com.dotcms.api.client.pull.file.OptionConstants.PRESERVE;
import com.dotcms.api.LanguageAPI;
import com.dotcms.api.client.model.RestClientFactory;
import com.dotcms.api.client.pull.PullHandler;
import com.dotcms.api.client.pull.exception.PullException;
import com.dotcms.api.traversal.TreeNode;
import com.dotcms.api.traversal.TreeNodeInfo;
import com.dotcms.cli.command.files.TreePrinter;
import com.dotcms.cli.common.ConsoleProgressBar;
import com.dotcms.cli.common.OutputOptionMixin;
import com.dotcms.common.AssetsUtils;
import com.dotcms.model.asset.FolderView;
import com.dotcms.model.language.Language;
import com.dotcms.model.pull.PullOptions;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import javax.enterprise.context.Dependent;
import javax.enterprise.context.control.ActivateRequestContext;
import javax.inject.Inject;
import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.microprofile.context.ManagedExecutor;
import org.jboss.logging.Logger;
/**
* The FilePullHandler class is responsible for pulling files from dotCMS.
* It extends the PullHandler class and handles the pulling of FileTraverseResult objects providing
* its own implementation of the pull method.
*/
@Dependent
public class FilePullHandler extends PullHandler<FileTraverseResult> {
@Inject
Logger logger;
@Inject
RestClientFactory clientFactory;
@Inject
Puller puller;
@Inject
ManagedExecutor executor;
@Override
public String title() {
return "Files";
}
@Override
public String startPullingHeader(final List<FileTraverseResult> contents) {
return String.format("\rPulling %s", title());
}
@Override
public String shortFormat(final FileTraverseResult content, final PullOptions pullOptions) {
boolean includeEmptyFolders = false;
final var customOptions = pullOptions.customOptions();
if (customOptions.isPresent()) {
includeEmptyFolders = (boolean) customOptions.get().
getOrDefault(INCLUDE_EMPTY_FOLDERS, false);
}
var treeInfo = treeInfo(content, pullOptions, includeEmptyFolders);
final TreeNode tree = treeInfo.getLeft();
final TreeNodeInfo treeNodeInfo = treeInfo.getRight();
final StringBuilder sb = new StringBuilder();
sb.append(header(content, treeNodeInfo));
// We need to retrieve the languages
final LanguageAPI languageAPI = clientFactory.getClient(LanguageAPI.class);
final List<Language> languages = languageAPI.list().entity();
// Display the result
TreePrinter.getInstance().filteredFormat(
sb,
tree,
includeEmptyFolders,
languages
);
return sb.toString();
}
@Override
@ActivateRequestContext
public int pull(List<FileTraverseResult> contents,
PullOptions pullOptions,
OutputOptionMixin output) throws ExecutionException, InterruptedException {
//Collect all exceptions from the returned contents
final List<Exception> allExceptions = contents.stream().map(FileTraverseResult::exceptions)
.flatMap(List::stream).collect(Collectors.toList());
//Any failed TreeNode will not be present
//So no need to separate the results
//Save the error code for the traversal process. This will be used to determine the exit code of the command if greater than 0 (zero)
int errorCode = handleExceptions(allExceptions, output);
boolean preserve = false;
boolean includeEmptyFolders = false;
final var customOptions = pullOptions.customOptions();
if (customOptions.isPresent()) {
preserve = (boolean) customOptions.get().getOrDefault(PRESERVE, false);
includeEmptyFolders = (boolean) customOptions.get().
getOrDefault(INCLUDE_EMPTY_FOLDERS, false);
}
output.info(startPullingHeader(contents));
for (final var content : contents) {
var errors = pullTree(
content,
pullOptions,
output,
!preserve,
includeEmptyFolders
);
final int e = handleExceptions(errors, output);
//This should always keep the highest error code
// Meaning that if no errors occurred, the error code will be 0
errorCode = Math.max(e, errorCode);
}
return errorCode;
}
/**
* This method pulls the tree of assets from the given content and options.
*
* @param content The file traverse result.
* @param options The pull options.
* @param output The output option mixin.
* @param overwrite Indicates whether to overwrite existing assets.
* @param generateEmptyFolders Indicates whether to generate empty folders.
* @return A list of exceptions that occurred during the pulling process.
*/
private List<Exception> pullTree(
FileTraverseResult content,
final PullOptions options,
final OutputOptionMixin output,
final boolean overwrite,
final boolean generateEmptyFolders) {
var treeInfo = treeInfo(content, options, generateEmptyFolders);
final TreeNode tree = treeInfo.getLeft();
final TreeNodeInfo treeNodeInfo = treeInfo.getRight();
// If we don't generate empty folders, we need to check if there are assets to process
// in the tree node, if not, we skip the pull process avoiding the creation of site
// folders with no content.
if (!generateEmptyFolders && treeNodeInfo.assetsCount() == 0) {
return List.of();
}
// Display the header
output.info(header(content, treeNodeInfo));
// ConsoleProgressBar instance to handle the download progress bar
ConsoleProgressBar progressBar = new ConsoleProgressBar(output);
CompletableFuture<List<Exception>> treeBuilderFuture = executor.supplyAsync(
() -> puller.pull(
tree,
treeNodeInfo,
options.destination(),
overwrite,
generateEmptyFolders,
options.failFast(),
progressBar
));
progressBar.setFuture(treeBuilderFuture);
CompletableFuture<Void> animationFuture = executor.runAsync(
progressBar
);
final List<Exception> foundErrors;
try {
// Waits for the completion of both the file system tree builder and console progress bar animation tasks.
// This line blocks the current thread until both CompletableFuture instances
// (treeBuilderFuture and animationFuture) have completed.
CompletableFuture.allOf(treeBuilderFuture, animationFuture).join();
foundErrors = treeBuilderFuture.get();
} catch (InterruptedException | ExecutionException e) {
var errorMessage = String.format("Error occurred while pulling assets: [%s].",
e.getMessage());
logger.error(errorMessage, e);
Thread.currentThread().interrupt();
throw new PullException(errorMessage, e);
}
output.info(String.format("%n"));
return foundErrors;
}
/**
* Retrieves the tree node information and generates a tree node object based on the given
* `content`, `options`, and `generateEmptyFolders` parameters.
* <p>
* If the `content` parameter contains a tree, it collects important information about the tree
* and creates a TreeNodeInfo object.
* <p>
* If the `content` parameter contains an asset, it parses and validates the given path, creates
* a simple tree node for the asset, and collects important information about the tree. The
* `generateEmptyFolders` parameter is ignored in this case.
*
* @param content the result of traversing the file system or an asset
* @param options the pull options for retrieving the content
* @param generateEmptyFolders a flag to indicate whether empty folders should be generated
* @return a Pair object containing the generated tree node and its information
* @throws IllegalStateException if both tree and asset are absent in the `content` parameter
*/
private Pair<TreeNode, TreeNodeInfo> treeInfo(FileTraverseResult content,
final PullOptions options, final boolean generateEmptyFolders) {
final TreeNode tree;
final TreeNodeInfo treeNodeInfo;
var optionalTree = content.tree();
var optionalAsset = content.asset();
if (optionalTree.isPresent()) {
// Collect important information about the tree
tree = optionalTree.get();
treeNodeInfo = tree.collectUniqueStatusAndLanguage(generateEmptyFolders);
} else if (optionalAsset.isPresent()) {
// Parsing and validating the given path
var dotCMSPath = AssetsUtils.parseRemotePath(options.contentKey().orElseThrow());
// Create a simple tree node for the asset to handle
var asset = optionalAsset.get();
var folder = FolderView.builder()
.host(dotCMSPath.site())
.path(dotCMSPath.folderPath().toString())
.name(dotCMSPath.folderName())
.assets(asset)
.build();
tree = new TreeNode(folder);
// Collect important information about the tree
treeNodeInfo = tree.collectUniqueStatusAndLanguage(false);
} else {
throw new IllegalStateException("Invalid state. Either tree or asset must be present.");
}
return Pair.of(tree, treeNodeInfo);
}
/**
* Generates the header string for a given `content` and `treeNodeInfo`.
* <p>
* If `content` is a tree, the header includes the number of assets, number of folders, and
* number of languages in the tree node.
* <p>
* If `content` is an asset, the header includes the number of assets and number of languages in
* the tree node.
*
* @param content the file traverse result (either a FileInfo or TreeInfo)
* @param treeNodeInfo the collected information about the tree node
* @return the formatted header string
* @throws IllegalStateException if both `content.tree()` and `content.asset()` are absent
*/
private String header(FileTraverseResult content, TreeNodeInfo treeNodeInfo) {
if (content.tree().isPresent()) {
return String.format("\r@|bold,green [%s]|@ - " +
"@|bold,green [%s]|@ Assets in " +
"@|bold,green [%s]|@ Folders and " +
"@|bold,green [%s]|@ Languages to pull\n\n",
treeNodeInfo.site(),
treeNodeInfo.assetsCount(),
treeNodeInfo.foldersCount(),
treeNodeInfo.languages().size());
} else if (content.asset().isPresent()) {
return String.format("\r@|bold,green [%s]|@ - " +
"@|bold,green [%s]|@ Assets in " +
"@|bold,green [%s]|@ Languages to pull\n\n",
treeNodeInfo.site(),
1,
treeNodeInfo.languages().size());
} else {
throw new IllegalStateException("Invalid state. Either tree or asset must be present.");
}
}
}