package aQute.bnd.deployer.obr;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import aQute.bnd.deployer.repository.AbstractIndexedRepo;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.Verifier;
import aQute.bnd.version.Version;
import aQute.lib.base64.Base64;
import aQute.lib.io.IO;
import aQute.lib.io.IOConstants;
/**
* A read-write nexus OBR-based repository.
* <p>
* You will need to install the nexus-obr-plugin in nexus (you can download it
* on <a href="http://search.maven.org/#search|ga|1|a%3A%22nexus-obr-plugin%22">
* maven central</a>)
* </p>
* <p>
* <h2>Properties</h2>
* <ul>
* <li><b>repositoryUrl</b>: the nexus repository url
* (http://localhost:8081/nexus/content/repositories/obr/ for example)</li>
* <li><b>username</b>: the username; defaults to "deployment"</li>
* <li><b>password</b>: the password; defaults to "deployment123"</li>
* <li><b>name</b>: repository name; defaults to the nexus repository url</li>
* <li><b>cache</b>: local cache directory. May be omitted, in which case a
* default directory will be used.</li>
* <li><b>readonly</b>: if readonly, no bundle can be added to the repository
* </ul>
* </p>
* <p>
* <h2>Example</h2>
*
* <pre>
* -plugin:
* aQute.bnd.deployer.obr.NexusOBR;readonly=false;repositoryUrl=http://localhost
* :8081/nexus/content/repositories/obr/;username=deployment;password=
* deployment123;name=nexus-obr
* </pre>
* </p>
*
* @author Cedric Chabanois <cchabanois at gmail.com>
*/
public class NexusOBR extends AbstractIndexedRepo {
static final int BUFFER_SIZE = IOConstants.PAGE_SIZE * 2;
private static final String DEFAULT_PASSWORD = "deployment123";
private static final String DEFAULT_USERNAME = "deployment";
private static final String EMPTY_REPOSITORY_URL = "";
private static final String DEFAULT_CACHE_DIR = ".bnd" + File.separator + "cache";
public static final String PROP_CACHE = "cache";
public static final String PROP_REPOSITORY_URL = "repositoryUrl";
public static final String PROP_READONLY = "readonly";
public static final String PROP_USERNAME = "username";
public static final String PROP_PASSWORD = "password";
public static final String PROP_PUTURL = "puturl";
protected File cacheDir = new File(
System.getProperty("user.home") + File.separator + DEFAULT_CACHE_DIR);
private String nexusRepositoryUrl;
private boolean readOnly;
private String username = DEFAULT_USERNAME;
private String password = DEFAULT_PASSWORD;
/**
* if null, nexusRepositoryUrl is used for puts.
*/
private String puturl = null;
@Override
public synchronized void setProperties(Map<String,String> map) {
super.setProperties(map);
readOnly = Boolean.parseBoolean(map.get(PROP_READONLY));
if (map.containsKey(PROP_USERNAME)) {
username = map.get(PROP_USERNAME);
}
if (map.containsKey(PROP_PASSWORD)) {
password = map.get(PROP_PASSWORD);
}
nexusRepositoryUrl = map.get(PROP_REPOSITORY_URL);
if (nexusRepositoryUrl != null && !nexusRepositoryUrl.endsWith("/")) {
nexusRepositoryUrl = nexusRepositoryUrl + '/';
}
if (map.containsKey(PROP_PUTURL)) {
puturl = map.get(PROP_PUTURL);
}
if (puturl != null && !puturl.endsWith("/")) {
puturl = puturl + '/';
}
String cachePath = map.get(PROP_CACHE);
if (cachePath != null) {
cacheDir = new File(cachePath);
if (!cacheDir.isDirectory())
try {
throw new IllegalArgumentException(String.format(
"Cache path '%s' does not exist, or is not a directory.", cacheDir.getCanonicalPath()));
} catch (IOException e) {
throw new IllegalArgumentException("Could not get cacheDir canonical path", e);
}
}
}
@Override
public boolean canWrite() {
return true;
}
public File getCacheDirectory() {
return cacheDir;
}
public String getLocation() {
if (nexusRepositoryUrl == null)
return EMPTY_REPOSITORY_URL;
else
return nexusRepositoryUrl;
}
@Override
protected List<URI> loadIndexes() throws Exception {
List<URI> result;
if (nexusRepositoryUrl != null) {
result = new ArrayList<URI>();
result.add(new URL(nexusRepositoryUrl + ".meta/obr.xml").toURI());
} else {
result = Collections.emptyList();
}
return result;
}
public void setCacheDirectory(File cacheDir) {
if (cacheDir == null)
throw new IllegalArgumentException("null cache directory not permitted");
this.cacheDir = cacheDir;
}
@Override
public synchronized PutResult put(InputStream stream, PutOptions options) throws Exception {
/* determine if the put is allowed */
if (readOnly) {
throw new IOException("Repository is read-only");
}
if (options == null)
options = DEFAULTOPTIONS;
/* both parameters are required */
if (stream == null)
throw new IllegalArgumentException("No stream and/or options specified");
/*
* setup a new stream that encapsulates the stream and calculates (when
* needed) the digest
*/
DigestInputStream dis = new DigestInputStream(stream, MessageDigest.getInstance("SHA-1"));
File tmpFile = null;
try {
/*
* copy the artifact from the (new/digest) stream into a temporary
* file in the root directory of the repository
*/
tmpFile = IO.createTempFile(null, "put", ".bnd");
IO.copy(dis, tmpFile);
/* beforeGet the digest if available */
byte[] disDigest = dis.getMessageDigest().digest();
if (options.digest != null && !Arrays.equals(options.digest, disDigest))
throw new IOException("Retrieved artifact digest doesn't match specified digest");
/* put the artifact into the repository (from the temporary file) */
URL url = putArtifact(tmpFile);
PutResult result = new PutResult();
if (url != null) {
result.digest = disDigest;
result.artifact = url.toURI();
}
return result;
} finally {
if (tmpFile != null && tmpFile.exists()) {
IO.delete(tmpFile);
}
}
}
protected URL putArtifact(File tmpFile) throws Exception {
assert (tmpFile != null);
assert (tmpFile.isFile());
init();
Version version;
String bsn;
try (Jar jar = new Jar(tmpFile)) {
bsn = jar.getBsn();
if (bsn == null || !Verifier.isBsn(bsn))
throw new IllegalArgumentException(
"Jar does not have a " + Constants.BUNDLE_SYMBOLICNAME + " manifest header");
String versionString = jar.getVersion();
if (versionString == null)
versionString = "0";
else if (!Verifier.isVersion(versionString))
throw new IllegalArgumentException("Invalid version " + versionString + " in file " + tmpFile);
version = Version.parseVersion(versionString);
}
URL url = put(tmpFile, bsn, version);
reset();
return url;
}
protected URL put(File file, String bsn, Version version) throws IOException {
URL url = getTargetURL(bsn, version);
try (InputStream is = IO.stream(file)) {
HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection();
httpUrlConnection.setDoOutput(true);
httpUrlConnection.setFixedLengthStreamingMode((int) file.length());
httpUrlConnection.setRequestMethod("PUT");
if (username != null && password != null) {
String userPassword = username + ":" + password;
httpUrlConnection.setRequestProperty("Authorization",
"Basic " + Base64.encodeBase64(userPassword.getBytes(UTF_8)));
}
try (OutputStream out = httpUrlConnection.getOutputStream()) {
byte[] buffer = new byte[BUFFER_SIZE];
while (true) {
int length = is.read(buffer);
if (length < 0)
break;
out.write(buffer, 0, length);
}
int respondeCode = httpUrlConnection.getResponseCode();
// response code will be 201 (Created) if new bundle is
// successfully
// added
if (respondeCode < 200 || respondeCode > 300) {
throw new IOException(httpUrlConnection.getResponseMessage());
}
} finally {
httpUrlConnection.disconnect();
}
}
return url;
}
@Override
public synchronized String getName() {
if (name != null && !name.equals(this.getClass().getName()))
return name;
return nexusRepositoryUrl;
}
private URL getTargetURL(String bsn, Version version) throws MalformedURLException {
String url = puturl != null ? puturl : nexusRepositoryUrl;
return new URL(url + bsn + "/" + bsn + "-" + version + ".jar");
}
public File getRoot() {
return cacheDir;
}
}