Skip to content

Commit e445fd0

Browse files
committed
Merge PR #1971 by @flo - include dependencies for module downloads
2 parents 858a5a8 + 476ec49 commit e445fd0

File tree

3 files changed

+319
-59
lines changed

3 files changed

+319
-59
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2015 MovingBlocks
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.terasology.rendering.nui.layers.mainMenu;
17+
18+
import org.terasology.assets.ResourceUrn;
19+
import org.terasology.rendering.nui.CoreScreenLayer;
20+
import org.terasology.rendering.nui.WidgetUtil;
21+
import org.terasology.rendering.nui.widgets.UILabel;
22+
23+
/**
24+
* Ask the user to confirm or cancel an action.
25+
*/
26+
public class ConfirmPopup extends CoreScreenLayer {
27+
public static final ResourceUrn ASSET_URI = new ResourceUrn("engine:confirmPopup!instance");
28+
29+
private Runnable okHandler;
30+
31+
@Override
32+
public void initialise() {
33+
WidgetUtil.trySubscribe(this, "ok",(button) -> {
34+
getManager().popScreen();
35+
okHandler.run();
36+
});
37+
WidgetUtil.trySubscribe(this, "cancel", (button) -> getManager().popScreen());
38+
}
39+
40+
public void setMessage(String title, String message) {
41+
UILabel titleLabel = find("title", UILabel.class);
42+
if (titleLabel != null) {
43+
titleLabel.setText(title);
44+
}
45+
46+
UILabel messageLabel = find("message", UILabel.class);
47+
if (messageLabel != null) {
48+
messageLabel.setText(message);
49+
}
50+
}
51+
52+
53+
/**
54+
* @param runnable will be called when the user clicks okay
55+
*/
56+
public void setOkHandler(Runnable runnable) {
57+
this.okHandler = runnable;
58+
}
59+
}

engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/SelectModulesScreen.java

Lines changed: 195 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import com.google.common.collect.ImmutableSet;
1919
import com.google.common.collect.Lists;
2020
import com.google.common.collect.Maps;
21-
2221
import org.slf4j.Logger;
2322
import org.slf4j.LoggerFactory;
2423
import org.terasology.config.Config;
@@ -29,6 +28,7 @@
2928
import org.terasology.engine.module.RemoteModuleExtension;
3029
import org.terasology.engine.paths.PathManager;
3130
import org.terasology.math.Vector2i;
31+
import org.terasology.module.DependencyInfo;
3232
import org.terasology.module.DependencyResolver;
3333
import org.terasology.module.Module;
3434
import org.terasology.module.ModuleLoader;
@@ -53,19 +53,24 @@
5353
import java.io.IOException;
5454
import java.net.URL;
5555
import java.nio.file.Path;
56+
import java.util.ArrayList;
5657
import java.util.Collections;
5758
import java.util.Comparator;
59+
import java.util.LinkedHashMap;
60+
import java.util.LinkedList;
5861
import java.util.List;
5962
import java.util.Map;
6063
import java.util.Set;
61-
import java.util.function.Predicate;
64+
import java.util.concurrent.Callable;
65+
import java.util.stream.Collectors;
6266

6367
/**
6468
* @author Immortius
6569
*/
6670
public class SelectModulesScreen extends CoreScreenLayer {
6771

6872
private static final Logger logger = LoggerFactory.getLogger(SelectModulesScreen.class);
73+
public static final Name ENGINE_MODULE_NAME = new Name("engine");
6974

7075
@In
7176
private ModuleManager moduleManager;
@@ -219,8 +224,9 @@ public String get() {
219224
description.bindText(new ReadOnlyBinding<String>() {
220225
@Override
221226
public String get() {
222-
if (moduleInfoBinding.get() != null) {
223-
return moduleInfoBinding.get().getDescription().toString();
227+
ModuleMetadata moduleMetadata = moduleInfoBinding.get();
228+
if (moduleMetadata != null) {
229+
return moduleMetadata.getDescription().toString();
224230
}
225231
return "";
226232
}
@@ -300,46 +306,20 @@ public void onActivated(UIWidget button) {
300306
if (moduleList.getSelection() != null) {
301307

302308
ModuleSelectionInfo info = moduleList.getSelection();
303-
startDownload(info);
309+
startDownloadingNewestModulesRequiredFor(info);
304310
}
305311
}
306312
});
307313

308-
Predicate<ModuleSelectionInfo> canDownload = info -> info != null && !info.isPresent();
309-
Predicate<ModuleSelectionInfo> canUpdate = info -> {
310-
if (info != null) {
311-
Module online = info.getOnlineVersion();
312-
if (online != null) {
313-
return online.getVersion().compareTo(info.getLatestVersion().getVersion()) > 0;
314-
}
315-
return false;
316-
}
317-
return false;
318-
};
319-
320-
downloadButton.bindEnabled(new ReadOnlyBinding<Boolean>() {
321-
@Override
322-
public Boolean get() {
323-
ModuleSelectionInfo info = moduleList.getSelection();
324-
if (canDownload.test(info)) {
325-
return true;
326-
}
327-
328-
return canUpdate.test(info);
329-
}
330-
});
331314
downloadButton.bindText(new ReadOnlyBinding<String>() {
332-
333315
@Override
334316
public String get() {
335317
ModuleSelectionInfo info = moduleList.getSelection();
336-
if (canDownload.test(info)) {
318+
if (info != null && !info.isPresent()) {
337319
return "Download";
338-
}
339-
if (canUpdate.test(info)) {
320+
} else {
340321
return "Update";
341322
}
342-
return "Download"; // button should be disabled
343323
}
344324
});
345325
}
@@ -368,37 +348,169 @@ public void onActivated(UIWidget button) {
368348
});
369349
}
370350

371-
private void startDownload(ModuleSelectionInfo info) {
372-
final WaitPopup<Path> popup = getManager().pushScreen(WaitPopup.ASSET_URI, WaitPopup.class);
373-
popup.setMessage("Downloading Module", "Please wait ...");
351+
private void startDownloadingNewestModulesRequiredFor(ModuleSelectionInfo moduleMetadata) {
352+
List<ModuleSelectionInfo> modulesToDownload;
353+
try {
354+
modulesToDownload = getModulesRequiredToDownloadFor(moduleMetadata);
355+
} catch (DependencyResolutionFailed e) {
356+
MessagePopup messagePopup = getManager().pushScreen(MessagePopup.ASSET_URI, MessagePopup.class);
357+
messagePopup.setMessage("Depedency resolution failed", e.getMessage());
358+
return;
359+
}
360+
361+
LinkedHashMap<URL, Path> urlToTargetMap = determineDownloadUrlsFor(modulesToDownload);
362+
363+
ConfirmPopup confirmPopup = getManager().pushScreen(ConfirmPopup.ASSET_URI, ConfirmPopup.class);
364+
confirmPopup.setMessage("Confirm Download", modulesToDownload.size() +" modules will be downloaded");
365+
confirmPopup.setOkHandler(() -> downloadModules(urlToTargetMap));
366+
}
374367

368+
private void downloadModules(LinkedHashMap<URL, Path> urlToTargetMap) {
369+
final WaitPopup<List<Path>> popup = getManager().pushScreen(WaitPopup.ASSET_URI, WaitPopup.class);
370+
ModuleLoader loader = new ModuleLoader(moduleManager.getModuleMetadataReader());
371+
loader.setModuleInfoPath(TerasologyConstants.MODULE_INFO_FILENAME);
372+
popup.onSuccess(paths -> {
373+
for (Path filePath: paths) {
374+
try {
375+
Module module = loader.load(filePath);
376+
modulesLookup.get(module.getId()).setLocalVersion(module);
377+
moduleManager.getRegistry().add(module);
378+
} catch (IOException e) {
379+
logger.warn("Could not load module {}", filePath.getFileName(), e);
380+
return;
381+
}
382+
updateValidToSelect();
383+
}
384+
});
375385
ProgressListener progressListener = progress ->
376-
popup.setMessage("Updating Preview", String.format("Please wait ... %d%%", (int) (progress * 100f)));
377-
378-
ModuleMetadata meta = info.getOnlineVersion().getMetadata();
379-
String version = meta.getVersion().toString();
380-
String id = meta.getId().toString();
381-
URL url = RemoteModuleExtension.getDownloadUrl(meta);
382-
popup.onSuccess(filePath -> {
383-
ModuleLoader loader = new ModuleLoader(moduleManager.getModuleMetadataReader());
384-
loader.setModuleInfoPath(TerasologyConstants.MODULE_INFO_FILENAME);
385-
try {
386-
Module module = loader.load(filePath);
387-
info.setLocalVersion(module);
388-
moduleManager.getRegistry().add(module);
389-
updateValidToSelect();
390-
} catch (IOException e) {
391-
logger.warn("Could not load module '{}:{}'", id, version, e);
386+
popup.setMessage("Downloading required modules", String.format("Please wait ... %d%%", (int) (progress * 100f)));
387+
// to ensure that the intial message gets set:
388+
progressListener.onProgress(0);
389+
MultiFileDownloader operation = new MultiFileDownloader(urlToTargetMap, progressListener);
390+
popup.startOperation(operation, true);
391+
}
392+
393+
private LinkedHashMap<URL, Path> determineDownloadUrlsFor(List<ModuleSelectionInfo> modulesToDownload) {
394+
LinkedHashMap<URL, Path> urlToTargetMap = Maps.newLinkedHashMap();
395+
for (ModuleSelectionInfo moduleSelectionInfo: modulesToDownload) {
396+
ModuleMetadata metaData = moduleSelectionInfo.getOnlineVersion().getMetadata();
397+
String version = metaData.getVersion().toString();
398+
String id = metaData.getId().toString();
399+
URL url = RemoteModuleExtension.getDownloadUrl(metaData);
400+
String fileName = String.format("%s-%s.jar", id, version);
401+
Path folder = PathManager.getInstance().getHomeModPath().normalize();
402+
Path target = folder.resolve(fileName);
403+
urlToTargetMap.put(url, target);
404+
}
405+
return urlToTargetMap;
406+
}
407+
408+
409+
private static final class DependencyResolutionFailed extends Exception {
410+
DependencyResolutionFailed(String message) {
411+
super(message);
412+
}
413+
}
414+
415+
private static class MultiFileDownloader implements Callable<List<Path>> {
416+
private LinkedHashMap<URL, Path> urlToTargetMap;
417+
private ProgressListener progressListener;
418+
419+
public MultiFileDownloader(LinkedHashMap<URL, Path> urlToTargetMap, ProgressListener progressListener) {
420+
this.urlToTargetMap = urlToTargetMap;
421+
this.progressListener = progressListener;
422+
}
423+
424+
@Override
425+
public List<Path> call() throws Exception {
426+
List<Path> downloadedFiles = new ArrayList<>();
427+
float fractionPerFile = (float) 1 / urlToTargetMap.size();
428+
int index = 0;
429+
for (Map.Entry<URL, Path> entry: urlToTargetMap.entrySet()) {
430+
float progressWithFiles= fractionPerFile*index;
431+
ProgressListener singleDownloadListener = new ProgressListener() {
432+
@Override
433+
public void onProgress(float fraction) {
434+
float totalPrecentDone = progressWithFiles + (fraction/urlToTargetMap.size());
435+
progressListener.onProgress(totalPrecentDone);
436+
}
437+
};
438+
FileDownloader fileDownloader = new FileDownloader(entry.getKey(), entry.getValue(),
439+
singleDownloadListener);
440+
downloadedFiles.add(fileDownloader.call());
441+
index++;
442+
};
443+
return downloadedFiles;
444+
}
445+
}
446+
447+
/**
448+
* @return All modules that are required to play the online version of the specified module. The list contains the
449+
* passed module too.
450+
*/
451+
private List<ModuleSelectionInfo> getModulesRequiredFor(ModuleSelectionInfo mainModuleInfo) throws DependencyResolutionFailed {
452+
ModuleMetadata mainModuleMetadata= mainModuleInfo.getOnlineVersion().getMetadata();
453+
LinkedList<Name> idsToCheck = Lists.newLinkedList();
454+
idsToCheck.add(mainModuleMetadata.getId());
455+
Map<Name, ModuleSelectionInfo> requiredIdToMetaDataMap = Maps.newLinkedHashMap();
456+
requiredIdToMetaDataMap.put(mainModuleMetadata.getId(), mainModuleInfo);
457+
while (!idsToCheck.isEmpty()) {
458+
Name moduleToCheck = idsToCheck.removeFirst();
459+
ModuleSelectionInfo moduleToCheckInfo =requiredIdToMetaDataMap.get(moduleToCheck);
460+
ModuleMetadata metaDataOfModuleToCheck = moduleToCheckInfo.getOnlineVersion().getMetadata();
461+
462+
for (DependencyInfo dependencyInfo : metaDataOfModuleToCheck.getDependencies()) {
463+
Name depName = dependencyInfo.getId();
464+
465+
ModuleMetadata depMetaData;
466+
if (depName.equals(ENGINE_MODULE_NAME)) {
467+
depMetaData = moduleManager.getRegistry().getLatestModuleVersion(ENGINE_MODULE_NAME).getMetadata();
468+
if (!dependencyInfo.versionRange().contains(depMetaData.getVersion())) {
469+
throw new DependencyResolutionFailed(String.format(
470+
"Module %s %s requires %s in version range %s, but you are using version %s",
471+
moduleToCheck,metaDataOfModuleToCheck.getVersion(), depName, dependencyInfo.versionRange(),
472+
depMetaData.getVersion()));
473+
}
474+
} else {
475+
ModuleSelectionInfo depInfo = modulesLookup.get(depName);
476+
if (depInfo == null) {
477+
throw new DependencyResolutionFailed(String.format("%s requires %s which is missing", moduleToCheck,
478+
depName));
479+
}
480+
depMetaData = depInfo.getOnlineVersion().getMetadata();
481+
482+
if (!dependencyInfo.versionRange().contains(depMetaData.getVersion())) {
483+
throw new DependencyResolutionFailed(String.format(
484+
"Module %s %s requires %s in version range %s, but the online version has version %s",
485+
moduleToCheck,metaDataOfModuleToCheck.getVersion(), depName, dependencyInfo.versionRange(),
486+
depMetaData.getVersion()));
487+
}
488+
if (!requiredIdToMetaDataMap.containsKey(depName)) {
489+
idsToCheck.add(depName);
490+
requiredIdToMetaDataMap.put(depName, depInfo);
491+
}
492+
}
392493
}
393-
});
394-
String fileName = String.format("%s-%s.jar", id, version);
395-
Path folder = PathManager.getInstance().getHomeModPath().normalize();
396-
Path target = folder.resolve(fileName);
494+
}
495+
List<ModuleSelectionInfo> sortedDependencies = Lists.newArrayList(requiredIdToMetaDataMap.values());
496+
return sortedDependencies;
497+
}
397498

398-
FileDownloader operation = new FileDownloader(url, target, progressListener);
399-
popup.startOperation(operation, true);
499+
/**
500+
*
501+
* @return all modules that need to be downloaded to use the newest version of the specified module and all its
502+
* dependencies.
503+
*/
504+
private List<ModuleSelectionInfo> getModulesRequiredToDownloadFor(ModuleSelectionInfo mainModuleInfo)
505+
throws DependencyResolutionFailed {
506+
List<ModuleSelectionInfo> requiredModules = getModulesRequiredFor(mainModuleInfo);
507+
508+
List<ModuleSelectionInfo> modulesToDownload = requiredModules.stream().filter(m -> m.isOnlineVersionNewer())
509+
.collect(Collectors.toList());
510+
return modulesToDownload;
400511
}
401512

513+
402514
private void updateValidToSelect() {
403515
List<Name> selectedModules = Lists.newArrayList();
404516
for (ModuleSelectionInfo info : sortedModules) {
@@ -429,7 +541,7 @@ private void setSelectedVersions(ResolutionResult currentSelectionResults) {
429541

430542
private void updateModuleInformation() {
431543

432-
Set<Name> filtered = ImmutableSet.of(new Name("engine"), new Name("engine-test"));
544+
Set<Name> filtered = ImmutableSet.of(ENGINE_MODULE_NAME, new Name("engine-test"));
433545
for (RemoteModule remote : metaDownloader.getModules()) {
434546
ModuleSelectionInfo info = modulesLookup.get(remote.getId());
435547
if (!filtered.contains(remote.getId())) {
@@ -602,5 +714,29 @@ public boolean isValidToSelect() {
602714
public void setValidToSelect(boolean validToSelect) {
603715
this.validToSelect = validToSelect;
604716
}
717+
718+
public boolean isOnlineVersionNewer() {
719+
if (onlineVersion == null) {
720+
return false;
721+
}
722+
if (latestVersion == null) {
723+
return true;
724+
}
725+
int versionCompare = onlineVersion.getVersion().compareTo(latestVersion.getVersion());
726+
if (versionCompare > 0) {
727+
return true;
728+
} else if (versionCompare == 0) {
729+
/*
730+
* Multiple binaries get released as the same snapshot version, A version name match thus does not
731+
* gurantee that we have the newest version already if it is a snapshot version.
732+
*
733+
* Having the user redownload the same binary again is not ideal, but it is better then ahving the user
734+
* being stuck on an outdated snapshot binary.
735+
*/
736+
return onlineVersion.getVersion().isSnapshot();
737+
} else {
738+
return false;
739+
}
740+
}
605741
}
606742
}

0 commit comments

Comments
 (0)