/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jkiss.dbeaver.registry.maven;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.registry.driver.DriverUtils;
import org.jkiss.dbeaver.registry.maven.versioning.DefaultArtifactVersion;
import org.jkiss.dbeaver.registry.maven.versioning.VersionRange;
import org.jkiss.dbeaver.runtime.WebUtils;
import org.jkiss.utils.CommonUtils;
import org.jkiss.utils.IOUtils;
import org.jkiss.utils.xml.SAXListener;
import org.jkiss.utils.xml.SAXReader;
import org.jkiss.utils.xml.XMLException;
import org.xml.sax.Attributes;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Maven artifact descriptor
*/
public class MavenArtifact implements IMavenIdentifier
{
private static final Log log = Log.getLog(MavenArtifact.class);
public static final String MAVEN_METADATA_XML = "maven-metadata.xml";
public static final String FILE_JAR = "jar";
public static final String FILE_POM = "pom";
@NotNull
private final MavenRepository repository;
@NotNull
private final String groupId;
@NotNull
private final String artifactId;
@Nullable
private final String classifier;
private final List<String> versions = new ArrayList<>();
private String latestVersion;
private String releaseVersion;
private Date lastUpdate;
private final List<MavenArtifactVersion> localVersions = new ArrayList<>();
private transient boolean metadataLoaded = false;
public MavenArtifact(@NotNull MavenRepository repository, @NotNull String groupId, @NotNull String artifactId, @Nullable String classifier)
{
this.repository = repository;
this.groupId = groupId;
this.artifactId = artifactId;
this.classifier = classifier;
}
public void loadMetadata(DBRProgressMonitor monitor) throws IOException {
latestVersion = null;
releaseVersion = null;
versions.clear();
lastUpdate = null;
String metadataPath = getBaseArtifactURL() + MAVEN_METADATA_XML;
monitor.subTask("Load metadata " + this + "");
try (InputStream mdStream = WebUtils.openConnection(metadataPath, getRepository().getAuthInfo()).getInputStream()) {
parseMetadata(mdStream);
} catch (XMLException e) {
log.warn("Error parsing artifact metadata", e);
} catch (IOException e) {
// Metadata xml not found. It happens in rare cases. Let's try to get directory listing
try (InputStream dirStream = WebUtils.openConnection(getBaseArtifactURL(), getRepository().getAuthInfo()).getInputStream()) {
parseDirectory(dirStream);
} catch (XMLException e1) {
log.warn("Error parsing artifact directory", e);
}
} finally {
removeIgnoredVersions();
monitor.worked(1);
}
metadataLoaded = true;
}
private void removeIgnoredVersions() {
for (Iterator<String> iter = versions.iterator(); iter.hasNext(); ) {
String version = iter.next();
if (MavenRegistry.getInstance().isVersionIgnored(groupId + ":" + artifactId + ":" + version)) {
iter.remove();
}
}
}
private void parseDirectory(InputStream dirStream) throws IOException, XMLException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
IOUtils.copyStream(dirStream, baos);
String dir = baos.toString();
Pattern hrefPattern = Pattern.compile("a href=\"(.+)/?\"");
Matcher matcher = hrefPattern.matcher(dir);
while (matcher.find()) {
String href = matcher.group(1);
while (href.endsWith("/")) {
href = href.substring(0, href.length() - 1);
}
int divPos = href.lastIndexOf('/');
if (divPos != -1) {
href = href.substring(divPos + 1);
}
if (href.equals("..")) {
continue;
}
versions.add(href);
}
}
private void parseMetadata(InputStream mdStream) throws IOException, XMLException {
SAXReader reader = new SAXReader(mdStream);
reader.parse(new SAXListener() {
public String lastTag;
@Override
public void saxStartElement(SAXReader reader, String namespaceURI, String localName, Attributes atts) throws XMLException {
lastTag = localName;
}
@Override
public void saxText(SAXReader reader, String data) throws XMLException {
if ("version".equals(lastTag)) {
versions.add(data);
} else if ("latest".equals(lastTag)) {
latestVersion = data;
} else if ("release".equals(lastTag)) {
releaseVersion = data;
} else if ("lastUpdate".equals(lastTag)) {
try {
lastUpdate = new Date(Long.parseLong(data));
} catch (NumberFormatException e) {
log.warn(e);
}
}
}
@Override
public void saxEndElement(SAXReader reader, String namespaceURI, String localName) throws XMLException {
lastTag = null;
}
});
}
@NotNull
public MavenRepository getRepository() {
return repository;
}
@NotNull
public String getGroupId() {
return groupId;
}
@NotNull
public String getArtifactId() {
return artifactId;
}
@Nullable
public String getClassifier() {
return classifier;
}
@NotNull
@Override
public String getVersion() {
return "";
}
@NotNull
@Override
public String getId() {
return MavenArtifactReference.makeId(this);
}
@Nullable
public Collection<String> getAvailableVersions(DBRProgressMonitor monitor, String versionSpec) throws IOException {
if (CommonUtils.isEmpty(versions) && !metadataLoaded) {
loadMetadata(monitor);
}
if (!isVersionPattern(versionSpec)) {
return versions;
}
// Filter versions according to spec
Pattern versionPattern = null;
VersionRange versionRange = null;
if (versionSpec.startsWith("{") && versionSpec.endsWith("}")) {
// Regex - find most recent version matching this pattern
try {
versionPattern = Pattern.compile(versionSpec.substring(1, versionSpec.length() - 1));
} catch (Exception e) {
log.error("Bad version pattern: " + versionSpec);
}
} else {
try {
versionRange = VersionRange.createFromVersionSpec(versionSpec);
} catch (Exception e) {
log.error("Bad version specification: " + versionSpec);
}
}
List<String> filtered = new ArrayList<>();
for (String version : versions) {
boolean matches;
if (versionPattern != null) {
matches = versionPattern.matcher(version).matches();
} else if (versionRange != null) {
matches = versionRange.containsVersion(new DefaultArtifactVersion(version));
} else {
matches = true;
}
if (matches) {
filtered.add(version);
}
}
return filtered;
}
public Date getLastUpdate() {
return lastUpdate;
}
private String getBaseArtifactURL() {
String dir = groupId.replace('.', '/') + "/" + artifactId;
return repository.getUrl() + dir + "/";
}
public String getFileURL(String version, String fileType) {
return getBaseArtifactURL() + version + "/" + getVersionFileName(version, fileType);
}
@NotNull
String getVersionFileName(@NotNull String version, @NotNull String fileType) {
StringBuilder sb = new StringBuilder();
sb.append(artifactId).append("-").append(version);
if (FILE_JAR.equals(fileType) && !CommonUtils.isEmpty(classifier)) {
sb.append('-').append(classifier);
}
sb.append(".").append(fileType);
return sb.toString();
}
@Override
public String toString() {
return getId();
}
// @Nullable
// public MavenArtifactVersion getActiveVersion() {
// return getVersion(activeVersion);
// }
@Nullable
public MavenArtifactVersion getVersion(String versionStr) {
for (MavenArtifactVersion version : localVersions) {
if (version.getVersion().equals(versionStr)) {
return version;
}
}
return null;
}
private MavenArtifactVersion makeLocalVersion(DBRProgressMonitor monitor, String versionStr, boolean setActive) throws IllegalArgumentException, IOException {
MavenArtifactVersion version = getVersion(versionStr);
if (version == null) {
version = new MavenArtifactVersion(monitor, this, versionStr);
localVersions.add(version);
}
return version;
}
public MavenArtifactVersion resolveVersion(DBRProgressMonitor monitor, String versionRef) throws IOException {
if (CommonUtils.isEmpty(versionRef)) {
throw new IOException("Empty artifact " + this + " version");
}
boolean predefinedVersion =
versionRef.equals(MavenArtifactReference.VERSION_PATTERN_RELEASE) ||
versionRef.equals(MavenArtifactReference.VERSION_PATTERN_LATEST) ||
versionRef.equals(MavenArtifactReference.VERSION_PATTERN_SNAPSHOT);
boolean lookupVersion = predefinedVersion || isVersionPattern(versionRef);
if (lookupVersion && !metadataLoaded) {
loadMetadata(monitor);
}
String versionInfo;
if (lookupVersion) {
List<String> allVersions = versions;
switch (versionRef) {
case MavenArtifactReference.VERSION_PATTERN_RELEASE:
versionInfo = releaseVersion;
if (!CommonUtils.isEmpty(versionInfo) && DriverUtils.isBetaVersion(versionInfo)) {
versionInfo = null;
}
break;
case MavenArtifactReference.VERSION_PATTERN_LATEST:
versionInfo = latestVersion;
break;
default:
if (versionRef.startsWith("{") && versionRef.endsWith("}")) {
// Regex - find most recent version matching this pattern
String regex = versionRef.substring(1, versionRef.length() - 1);
try {
Pattern versionPattern = Pattern.compile(regex);
List<String> versions = new ArrayList<>(allVersions);
for (Iterator<String> iter = versions.iterator(); iter.hasNext(); ) {
if (!versionPattern.matcher(iter.next()).matches()) {
iter.remove();
}
}
versionInfo = DriverUtils.findLatestVersion(versions);
} catch (Exception e) {
throw new IOException("Bad version pattern: " + regex);
}
} else {
versionInfo = getVersionFromSpec(versionRef);
}
break;
}
if (versionInfo == null) {
if (allVersions.isEmpty()) {
throw new IOException("Artifact '" + this + "' has empty version list");
}
// Use latest version
versionInfo = DriverUtils.findLatestVersion(allVersions);
}
} else {
if (versionRef.startsWith("[") || versionRef.startsWith("(")) {
versionInfo = getVersionFromSpec(versionRef);
} else {
versionInfo = versionRef;
}
}
MavenArtifactVersion localVersion = getVersion(versionInfo);
if (localVersion == null) {
localVersion = makeLocalVersion(monitor, versionInfo, lookupVersion);
}
return localVersion;
}
public static boolean versionMatches(String version, String versionSpec) {
try {
if (versionSpec.startsWith("{") && versionSpec.endsWith("}")) {
Pattern versionPattern = Pattern.compile(versionSpec.substring(1, versionSpec.length() - 1));
return versionPattern.matcher(version).matches();
} else {
return VersionRange.createFromVersionSpec(versionSpec).containsVersion(new DefaultArtifactVersion(version));
}
} catch (Exception e) {
log.debug(e);
return false;
}
}
@Nullable
private String getVersionFromSpec(String versionRef) throws IOException {
String versionInfo;
try {
VersionRange range = VersionRange.createFromVersionSpec(versionRef);
if (range.getRecommendedVersion() != null) {
versionInfo = range.getRecommendedVersion().toString();
} else if (!range.getRestrictions().isEmpty()) {
versionInfo = range.getRestrictions().get(0).getLowerBound().toString();
} else {
versionInfo = null;
}
} catch (Exception e) {
throw new IOException("Bad version pattern: " + versionRef, e);
}
return versionInfo;
}
private static boolean isVersionPattern(String versionSpec) {
if (versionSpec.isEmpty()) {
return false;
}
char firstChar = versionSpec.charAt(0), lastChar = versionSpec.charAt(versionSpec.length() - 1);
return firstChar == '[' || firstChar == '(' || firstChar == '{' ||
lastChar == ']' || lastChar == ')' || lastChar == '}' ||
versionSpec.contains(",");
}
}