/*
* Copyright to the original author or authors
*
* 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.rioproject.url.artifact;
import org.rioproject.resolver.Artifact;
import org.rioproject.resolver.Resolver;
import org.rioproject.resolver.ResolverException;
import org.rioproject.resolver.ResolverHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
/**
* <p>A stream handler for URLs with the <code>artifact</code> protocol. The <code>artifact</code> URL
* provides a way to resolve an artifact's dependencies.</p>
*
* <p>The URL scheme for this handler is:<br/>
* <pre>artifact:groupId/artifactId/version[/type[/classifier]][;repository[@repositoryId]]</pre></p>
*
* <p>This handler has to be installed before any connection is made by using the following code:
* <pre>URL.setURLStreamHandlerFactory(new ArtifactURLStreamHandlerFactory());</pre>
* or must be set via a JVM property such as:
* <pre>-Djava.protocol.handler.pkgs=org.rioproject.url</pre>
* </p>
*
* <p>Here are some examples:</p>
* <ul>
* <li>An artifact URL for groupId, artifactId and version<br/>
* <code>artifact:org.rioproject.examples.calculator/calculator-service/2.0.1</code></li>
* <li>An artifact URL for groupId, artifactId, version and repository with an id<br/>
* <code>artifact:org.rioproject.examples.calculator/calculator-proxy/2.0.1;http://www.rio-project.org@rio</code></li>
* </ul>
*
* <p>Once an artifact has been resolved to an {@code URL}, the {@code artifact:URL} pair is
* cached, avoiding future interactions with the underlying {@link Resolver}</p>
*
* @author Dennis Reedy
*/
/* We want to throw a RuntimeException if we cannot get a Resolver */
@SuppressWarnings("PMD.AvoidThrowingRawExceptionTypes")
public class Handler extends URLStreamHandler {
private static final Logger logger = LoggerFactory.getLogger(Handler.class.getName());
private static Resolver resolver;
static {
try {
resolver = ResolverHelper.getResolver();
} catch (ResolverException e) {
logger.error("Could not get a ResolverInstance", e);
throw new RuntimeException("Could not get a ResolverInstance", e);
}
}
private final ConcurrentMap<Artifact, URL> cache = new ConcurrentHashMap<Artifact, URL>();
public Handler() {
ScheduledExecutorService snapshotReaper = Executors.newScheduledThreadPool(1, new ThreadFactory() {
@Override public Thread newThread(Runnable runnable) {
Thread thread = Executors.defaultThreadFactory().newThread(runnable);
thread.setDaemon(true);
return thread;
}
});
snapshotReaper.scheduleAtFixedRate(new SnapShotReaper(), 3, 3, TimeUnit.HOURS);
}
protected URLConnection openConnection(URL url) throws IOException {
if(url==null)
throw new MalformedURLException("url cannot be null");
String path = url.getPath();
if(path==null)
throw new MalformedURLException("url has null path");
ArtifactURLConfiguration configuration = new ArtifactURLConfiguration(path);
String artifact = configuration.getArtifact();
Artifact a;
try {
a = new Artifact(artifact);
} catch(IllegalArgumentException e) {
throw new MalformedURLException(e.getLocalizedMessage());
}
URL u;
try {
u = cache.get(a);
if(u==null) {
u = resolver.getLocation(artifact, a.getType(), configuration.getRepositories());
cache.put(a, u);
if(logger.isDebugEnabled())
logger.debug("Location of {} is {}", a, u==null?"<NULL>":u.toExternalForm());
}
} catch (ResolverException e) {
logger.warn(String.format("Could not resolve %s", a), e);
throw new IOException(e.getLocalizedMessage());
}
if(u!=null)
return u.openConnection();
return null;
}
/**
* The {@code SnapShotReaper} is used to check if any cached artifacts are SNAPSHOTs. This class
* is run by a ScheduledExecutor every 3 hours to check the cache, if there are SNAPSHOTs in the cache
* they will be purged, meaning the next time the artifact gets resolved, it may be downloaded if necessary.
*
* TODO: The timing of this needs to be configurable.
*/
private class SnapShotReaper implements Runnable {
public void run() {
List<Artifact> removals = new ArrayList<Artifact>();
for(Map.Entry<Artifact, URL> entry : cache.entrySet()) {
if(entry.getKey().getVersion().endsWith("SNAPSHOT")) {
removals.add(entry.getKey());
}
}
for(Artifact a : removals) {
if(logger.isDebugEnabled())
logger.debug("Removing "+a.toString()+" from cache");
cache.remove(a);
}
}
}
}