package org.graalvm.component.installer.ce;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.ConnectException;
import java.net.MalformedURLException;
import java.net.NoRouteToHostException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.graalvm.component.installer.BundleConstants;
import org.graalvm.component.installer.CommandInput;
import org.graalvm.component.installer.CommonConstants;
import org.graalvm.component.installer.Feedback;
import org.graalvm.component.installer.IncompatibleException;
import org.graalvm.component.installer.model.ComponentRegistry;
import org.graalvm.component.installer.model.ComponentStorage;
import org.graalvm.component.installer.remote.FileDownloader;
import org.graalvm.component.installer.remote.RemotePropertiesStorage;
import org.graalvm.component.installer.SoftwareChannel;
import org.graalvm.component.installer.SoftwareChannelSource;
import org.graalvm.component.installer.SystemUtils;
import org.graalvm.component.installer.Version;
import org.graalvm.component.installer.model.ComponentInfo;
import org.graalvm.component.installer.model.RemoteInfoProcessor;
public class WebCatalog implements SoftwareChannel {
private final String urlString;
private final SoftwareChannelSource source;
private URL catalogURL;
private Feedback feedback;
private ComponentRegistry local;
private ComponentStorage storage;
private RuntimeException savedException;
private RemoteInfoProcessor remoteProcessor = RemoteInfoProcessor.NONE;
private Version.Match matchVersion;
private boolean enableV1Versions;
public WebCatalog(String u, SoftwareChannelSource source) {
this.urlString = u;
this.source = source;
}
public RemoteInfoProcessor getRemoteProcessor() {
return remoteProcessor;
}
public void setRemoteProcessor(RemoteInfoProcessor remoteProcessor) {
this.remoteProcessor = remoteProcessor;
}
public Version.Match getMatchVersion() {
return matchVersion;
}
public boolean isEnableV1Versions() {
return enableV1Versions;
}
public void setEnableV1Versions(boolean enableV1Versions) {
this.enableV1Versions = enableV1Versions;
}
public void setMatchVersion(Version.Match aMatchVersion) {
this.matchVersion = aMatchVersion;
}
protected static boolean acceptURLScheme(String scheme, String urlSpec) {
switch (scheme) {
case "http":
case "https":
case "ftp":
case "ftps":
return true;
case "file":
try {
Path p = new File(new URI(urlSpec)).toPath();
return Files.isRegularFile(p) && Files.isReadable(p);
} catch (URISyntaxException ex) {
break;
}
}
return false;
}
public void init(CommandInput in, Feedback out) {
init(in.getLocalRegistry(), out);
}
public void init(ComponentRegistry aLocal, Feedback out) {
this.feedback = out.withBundle(WebCatalog.class);
this.local = aLocal;
}
@Override
public ComponentStorage getStorage() {
if (this.storage != null) {
return this.storage;
}
Map<String, String> graalCaps = local.getGraalCapabilities();
StringBuilder sb = new StringBuilder();
sb.append(SystemUtils.patternOsName(graalCaps.get(CommonConstants.CAP_OS_NAME)).toLowerCase());
sb.append("_");
sb.append(SystemUtils.patternOsArch(graalCaps.get(CommonConstants.CAP_OS_ARCH).toLowerCase()));
try {
catalogURL = new URL(urlString);
} catch (MalformedURLException ex) {
throw feedback.failure("REMOTE_InvalidURL", ex, catalogURL, ex.getLocalizedMessage());
}
Properties props = new Properties();
RemotePropertiesStorage newStorage = createPropertiesStorage(feedback, local, props, sb.toString(), catalogURL);
if (remoteProcessor != null) {
newStorage.setRemoteProcessor(remoteProcessor);
}
Properties loadProps = new Properties();
FileDownloader dn;
try {
if (savedException != null) {
throw savedException;
}
catalogURL = new URL(urlString);
String l = source.getLabel();
dn = new FileDownloader(feedback.l10n(l == null || l.isEmpty() ? "REMOTE_CatalogLabel2" : "REMOTE_CatalogLabel", l), catalogURL, feedback);
dn.download();
} catch (NoRouteToHostException | ConnectException ex) {
throw savedException = feedback.failure("REMOTE_ErrorDownloadCatalogProxy", ex, catalogURL, ex.getLocalizedMessage());
} catch (FileNotFoundException ex) {
feedback.error("REMOTE_WarningErrorDownloadCatalogNotFoundSkip", ex, catalogURL);
this.storage = newStorage;
return storage;
} catch (IOException ex) {
throw savedException = feedback.failure("REMOTE_ErrorDownloadCatalog", ex, catalogURL, ex.getLocalizedMessage());
}
this.storage = newStorage;
StringBuilder oldGraalPref = new StringBuilder("^" + BundleConstants.GRAAL_COMPONENT_ID);
oldGraalPref.append('.');
String graalVersionString;
String normalizedVersion;
if (matchVersion != null) {
graalVersionString = matchVersion.getVersion().displayString();
normalizedVersion = matchVersion.getVersion().toString();
} else {
graalVersionString = graalCaps.get(CommonConstants.CAP_GRAALVM_VERSION).toLowerCase();
normalizedVersion = local.getGraalVersion().toString();
}
StringBuilder graalPref = new StringBuilder(oldGraalPref);
oldGraalPref.append(Pattern.quote(graalVersionString));
oldGraalPref.append('_').append(sb);
graalPref.append(sb).append('/');
graalPref.append(Pattern.quote(normalizedVersion));
try (FileInputStream fis = new FileInputStream(dn.getLocalFile())) {
loadProps.load(fis);
} catch (IllegalArgumentException | IOException ex) {
throw feedback.failure("REMOTE_CorruptedCatalogFile", ex, catalogURL);
}
Pattern oldPrefixPattern = Pattern.compile(oldGraalPref.toString(), Pattern.CASE_INSENSITIVE);
Pattern newPrefixPattern = Pattern.compile(graalPref.toString(), Pattern.CASE_INSENSITIVE);
Stream<String> propNames = loadProps.stringPropertyNames().stream();
boolean foundPrefix = propNames.anyMatch(p -> oldPrefixPattern.matcher(p).matches() ||
newPrefixPattern.matcher(p).find());
if (!foundPrefix) {
boolean graalPrefixFound = false;
boolean componentFound = false;
for (String s : loadProps.stringPropertyNames()) {
if (s.startsWith(BundleConstants.GRAAL_COMPONENT_ID)) {
graalPrefixFound = true;
}
if (s.startsWith("Component.")) {
componentFound = true;
}
}
if (!componentFound) {
feedback.verboseOutput("REMOTE_CatalogDoesNotContainComponents", catalogURL);
return newStorage;
} else if (!graalPrefixFound) {
throw feedback.failure("REMOTE_CorruptedCatalogFile", null, catalogURL);
} else {
throw new IncompatibleException(
feedback.l10n("REMOTE_UnsupportedGraalVersion",
graalCaps.get(CommonConstants.CAP_GRAALVM_VERSION),
graalCaps.get(CommonConstants.CAP_OS_NAME),
graalCaps.get(CommonConstants.CAP_OS_ARCH)),
null);
}
}
props.putAll(loadProps);
return newStorage;
}
protected RemotePropertiesStorage createPropertiesStorage(Feedback aFeedback,
ComponentRegistry aLocal, Properties props, String selector, URL baseURL) {
return new RemotePropertiesStorage(
aFeedback, aLocal, props, selector, null, baseURL);
}
@Override
public FileDownloader configureDownloader(ComponentInfo cInfo, FileDownloader dn) {
return dn;
}
public static class WebCatalogFactory implements SoftwareChannel.Factory {
private CommandInput input;
@Override
public SoftwareChannel createChannel(SoftwareChannelSource src, CommandInput in, Feedback fb) {
String urlSpec = src.getLocationURL();
int schColon = urlSpec.indexOf(':');
if (schColon == -1) {
return null;
}
String scheme = urlSpec.toLowerCase().substring(0, schColon);
if (acceptURLScheme(scheme, urlSpec)) {
WebCatalog c = new WebCatalog(urlSpec, src);
c.init(in, fb);
return c;
}
return null;
}
@Override
public void init(CommandInput in, Feedback out) {
assert this.input == null;
this.input = in;
}
}
}