org.expath.pkg.repo.Repository Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pkg-java Show documentation
Show all versions of pkg-java Show documentation
Java Library implementing the core EXPath Packaging features
/****************************************************************************/
/* File: Repository.java */
/* Author: F. Georges */
/* Company: H2O Consulting */
/* Date: 2009-10-19 */
/* Tags: */
/* Copyright (c) 2009 Florent Georges (see end of file.) */
/* ------------------------------------------------------------------------ */
package org.expath.pkg.repo;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import org.expath.pkg.repo.Storage.PackageResolver;
import org.expath.pkg.repo.parser.DescriptorParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Represent a standard EXPath package repository structure on the disk.
*
* TODO: Because we have a repository descriptor with the list of installed
* packages (by the spec: .expath-pkg/packages.txt and .expath-pkg/packages.xml),
* we don't have to parse all the package descriptors in the repository at the
* instantiation of the object. We can instead create "fake" packages with
* only the information from the repository descriptor, and parse the whole
* package descriptor only when the user ask for that package.
*
* That way, the creation of a Repository object just needs to parse the
* repository descriptor, and parses the package descriptors only as needed.
*
* @author Florent Georges
*/
public class Repository
implements Universe
{
public Repository(Storage storage)
throws PackageException
{
LOG.info("Create a new repository with storage: {}", storage);
myStorage = storage;
myPackages = new HashMap();
myExtensions = new HashMap();
// dynamically register extensions from the classpath
ServiceLoader loader = ServiceLoader.load(Extension.class);
for ( Extension e : loader ) {
registerExtension(e);
}
// TODO: Enable lazy initialization...
parsePublicUris();
}
/**
* Shortcut for {@code makeDefaultRepo(null)}.
*
* @return the repository
*
* @throws PackageException if an error occurs
*/
public static Repository makeDefaultRepo()
throws PackageException
{
return makeDefaultRepo(null);
}
/**
* Return a repository instantiated from default location.
*
* If the parameter is not null, it uses it. If not, then looks at the
* system property {@code expath.repo}, and if it is not set at the
* environment variable {@code EXPATH_REPO}. This string is interpreted
* as a directory path, which must point to a repository.
*
* It throws an exception if the the directory does not exist (or is not a
* directory), or if there is any error creating the repository object from
* it.
*
* @param dir the repository dir
*
* @return the repository
*
* @throws PackageException if an error occurs
*/
public static Repository makeDefaultRepo(String dir)
throws PackageException
{
if ( dir == null ) {
dir = System.getProperty("expath.repo");
}
if ( dir == null ) {
dir = System.getenv("EXPATH_REPO");
}
if ( dir != null ) {
Path f = Paths.get(dir);
if ( ! Files.exists(f) ) {
throw new PackageException("Repo directory does not exist: " + dir);
}
if ( ! Files.isDirectory(f) ) {
throw new PackageException("Repo is not a directory: " + dir);
}
try {
Storage storage = new FileSystemStorage(f);
return new Repository(storage);
}
catch ( PackageException ex ) {
throw new PackageException("Error setting the repo (" + dir + ")", ex);
}
}
return null;
}
/**
* Register an extension.
*
* @param ext the extension
*
* @throws PackageException if an error occurs
*/
final public void registerExtension(Extension ext)
throws PackageException
{
if ( ! myExtensions.containsKey(ext.getName()) ) {
myExtensions.put(ext.getName(), ext);
ext.init(this, myPackages);
}
}
/**
* Reload the repository configuration, so parse again the package descriptors.
*
* @throws PackageException if an error occurs
*/
public synchronized void reload()
throws PackageException
{
// TODO: Reload extensions as well?
myPackages = new HashMap();
parsePublicUris();
}
/**
* Get the repository storage.
*
* @return The {@link Storage} object this repository is based upon.
*/
public Storage getStorage()
{
return myStorage;
}
public Collection listPackages()
{
return myPackages.values();
}
public Packages getPackages(String name)
{
return myPackages.get(name);
}
/**
* Create a repository.
*
* TODO: Must be delegated to the storage!
*
* @param dir the repository dir
*
* @return the repository
*
* @throws PackageException if an error occurs
*/
public static Repository createRepository(Path dir)
throws PackageException
{
if ( Files.exists(dir) ) {
// must be a dir and empty, or that's an error
if ( ! Files.isDirectory(dir) ) {
throw new PackageException("File exists and is not a directory (" + dir + ")");
}
if ( !FileHelper.isEmpty(dir) ) {
throw new PackageException("Directory exists and is not empty (" + dir + ")");
}
// TODO: Add a force option to delete the dir if it exists and is
// not empty?
}
else {
try {
Files.createDirectories(dir);
} catch (final IOException e) {
throw new PackageException("Error creating the directory (" + dir + ")", e);
}
}
// here, we know 'dir' is a directory and is empty...
Path priv_dir = dir.resolve(".expath-pkg");
try {
Files.createDirectories(priv_dir);
} catch (final IOException e) {
throw new PackageException("Error creating the private directory (" + priv_dir + ")", e);
}
return new Repository(new FileSystemStorage(dir));
}
/**
* Install a XAR package into this repository, from a URI location.
*
* TODO: Anything to delegate to the storage?
*
* @param pkg The package file (typically a {@code *.xar} or {@code *.xaw} file).
*
* @param force If force is false, this is an error if the same package has
* already been installed in the repository. If it is true, it is first
* deleted if existing.
*
* @param interact How the repository interacts with the user.
*
* @return The freshly installed package.
*
* @throws PackageException If any error occurs.
*/
public Package installPackage(URI pkg, boolean force, UserInteractionStrategy interact)
throws PackageException
{
if ( myStorage.isReadOnly() ) {
throw new PackageException("The storage is read-only, package install not supported");
}
// TODO: Must be moved within the storage class (because we are writing on disk)...
return installPackage(new XarUriSource(pkg), force, interact);
}
/**
* Install a XAR package into this repository.
*
* TODO: In case of exception, the temporary dir is not removed, solve that.
*
* @param xarSource A source for the package file (typically a {@code *.xar} or {@code *.xaw} file).
*
* @param force If force is false, this is an error if the same package has
* already been installed in the repository. If it is true, it is first
* deleted if existing.
*
* @param interact How the repository interacts with the user.
*
* @return The freshly installed package.
*
* @throws PackageException If any error occurs.
*/
public Package installPackage(XarSource xarSource, boolean force, UserInteractionStrategy interact)
throws PackageException
{
// preconditions
if ( ! xarSource.isValid()) {
throw new PackageException("Package file does not exist (" + xarSource.getURI() + ")");
}
myStorage.beforeInstall(force, interact);
// the temporary dir, to unzip the package
Path tmp_dir = myStorage.makeTempDir("install");
// unzip in the package in destination dir
try {
ZipHelper zip = new ZipHelper(xarSource);
zip.unzip(tmp_dir);
}
catch ( IOException ex ) {
throw new PackageException("Error unziping the package", ex);
}
interact.logInfo("Package unziped to " + tmp_dir);
// parse the package
final Path desc_f = tmp_dir.resolve("expath-pkg.xml");
if ( ! Files.exists(desc_f) ) {
throw new PackageException("Package descriptor does NOT exist in: " + tmp_dir);
}
final Package pkg;
try (final InputStream is = Files.newInputStream(desc_f)) {
final Source desc = new StreamSource(is);
// parse the descriptor
final DescriptorParser parser = new DescriptorParser();
pkg = parser.parse(desc, null, myStorage, this);
} catch (final IOException e) {
throw new PackageException(e.getMessage(), e);
}
// is the package already in the repo?
String name = pkg.getName();
String version = pkg.getVersion();
Packages pp = myPackages.get(name);
if ( pp != null ) {
Package p2 = pp.version(version);
if ( p2 != null ) {
if ( force || interact.ask("Force override " + name + " - " + version + "?", false) ) {
myStorage.remove(p2);
pp.remove(p2);
if ( pp.latest() == null ) {
myPackages.remove(name);
}
}
else {
throw new AlreadyInstalledException(name, version);
}
}
}
// where to move the temporary dir? (where within the repo)
String key = pkg.getAbbrev() + "-" + version;
for ( int i = 1; myStorage.packageKeyExists(key) && i < 100 ; ++i ) {
key = pkg.getAbbrev() + "-" + version + "__" + i;
}
if ( myStorage.packageKeyExists(key) ) {
String msg = "Impossible to find a non-existing package key in the repo, stopped at: ";
throw new PackageException(msg + key);
}
myStorage.storeInstallDir(tmp_dir, key, pkg);
if ( pp == null ) {
pp = new Packages(name);
myPackages.put(name, pp);
}
pp.add(pkg);
myStorage.updatePackageLists(pkg);
for ( Extension ext : myExtensions.values() ) {
ext.install(this, pkg);
}
return pkg;
}
/**
* Remove a package from the repository, by name.
*
* If a package with that name does not exist, or if there are several
* versions installed, this is an error (except if the package does not
* exist and {@code force} is {@code true}, then simply returns {@code
* false}).
*
* @param pkg The package name.
*
* @param force To silently ignore a non existing package (simply returns
* {@code false} in that case).
*
* @param interact How the repository interacts with the user.
*
* @return True if the package has been successfully removed, false if not
* (false is returned when the user canceled removing interactively, or if
* the package does not exist and {@code force} is true).
*
* @throws PackageException If any error occurs during removal.
*/
public boolean removePackage(String pkg, boolean force, UserInteractionStrategy interact)
throws PackageException
{
if ( ! interact.ask("Remove package " + pkg + "?", true) ) {
return false;
}
// delete the package content
Packages pp = myPackages.get(pkg);
if ( pp == null ) {
if ( force ) {
return false;
}
throw new PackageException("The package does not exist: " + pkg);
}
if ( pp.packages().size() != 1 ) {
throw new PackageException("The package has several versions installed: " + pkg);
}
Package p = pp.latest();
myStorage.remove(p);
pp.remove(p);
// remove the package from the list
myPackages.remove(pkg);
return true;
}
/**
* Remove a package from the repository, by name and version.
*
* If a package with that name and that version does not exist, this is an
* error, except if the package does not exist and {@code force} is {@code
* true} (then it simply returns {@code false}).
*
* @param pkg The package name.
*
* @param version The package version.
*
* @param force To silently ignore a non existing package (simply returns
* {@code false} in that case).
*
* @param interact How the repository interacts with the user.
*
* @return True if the package has been successfully removed, false if not
* (false is returned when the user canceled removing interactively, or if
* the package does not exist and {@code force} is true).
*
* @throws PackageException If any error occurs during removal.
*/
public boolean removePackage(String pkg, String version, boolean force, UserInteractionStrategy interact)
throws PackageException
{
if ( ! interact.ask("Remove package " + pkg + ", version " + version + "?", true) ) {
return false;
}
// delete the package content
Packages pp = myPackages.get(pkg);
if ( pp == null ) {
if ( force ) {
return false;
}
throw new PackageException("The package does not exist: " + pkg);
}
Package p = pp.version(version);
if ( p == null ) {
if ( force ) {
return false;
}
throw new PackageException("The version " + version + " does not exist for the package: " + pkg);
}
myStorage.remove(p);
pp.remove(p);
// remove the package from the list if it was the only version
if ( pp.latest() == null ) {
myPackages.remove(pkg);
}
return true;
}
/**
* Resolve a URI in this repository, in the specified space, return a File.
*
* For each package, use only the latest version.
*
* TODO: What about the packages with a versionning scheme which does NOT
* follow SemVer? (because basically those are not ordered)
*
* TODO: And when we want to resolve into a specific version? For instance
* when we are evaluating within the context of a specific package, and we
* want to resolve only in its declared dependencies? Or at least to use
* the versionning of its dependencies to guide within which package we
* should search (instead of taking always the latest systematically). Same
* comments for SaxonRepository.
*/
@Override
public Source resolve(String href, URISpace space)
throws PackageException
{
LOG.debug("Repository, resolve in {}: '{}'", space, href);
for ( Packages pp : myPackages.values() ) {
Package p = pp.latest();
Source src = p.resolve(href, space);
if ( src != null ) {
return src;
}
}
return null;
}
@Override
public Source resolve(String href, URISpace space, boolean transitive)
throws PackageException
{
// transitive or not is meaningless, as anyway the universe is the whole
// respository (and dependencies are defined within the repo)
return resolve(href, space);
}
/**
* ...
*/
private synchronized void parsePublicUris()
throws PackageException
{
// the list of package dirs
Set packages = myStorage.listPackageDirectories();
// the parser
DescriptorParser parser = new DescriptorParser();
// loop over the packages
for ( String p : packages ) {
PackageResolver res = myStorage.makePackageResolver(p, null);
Source desc;
try {
desc = res.resolveResource("expath-pkg.xml");
}
catch ( Storage.NotExistException ex ) {
throw new PackageException("Package descriptor does NOT exist in: " + p, ex);
}
try {
Package pkg = parser.parse(desc, p, myStorage, this);
addPackage(pkg);
for ( Extension ext : myExtensions.values() ) {
ext.init(this, pkg);
}
} catch (PackageException e) {
// do not abort: package should be ignored
}
}
}
/**
* Package-level to be used in tests (to "manually" build a repo).
*/
void addPackage(Package pkg)
{
String name = pkg.getName();
Packages pp = myPackages.get(name);
if ( pp == null ) {
pp = new Packages(name);
myPackages.put(name, pp);
}
pp.add(pkg);
}
/**
* Package-level, only to be used in tests (to "manually" build a repo).
*/
Repository()
{
// nothing, packages will be added "by hand" in tests
myStorage = null; // make javac happy, init the final variable
myPackages = new HashMap(); // init the variable, for addPackage()
myExtensions = new HashMap();
}
/**
* The storage object to physically access the repository content.
*/
private final Storage myStorage;
/**
* The list of packages in this repository (indexed by name).
*/
private Map myPackages;
/**
* The registered extensions (indexed by name).
*/
private Map myExtensions;
/**
* The logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(Repository.class);
/**
* Exception raised when trying to install a package already installed.
*/
public static class AlreadyInstalledException
extends PackageException
{
public AlreadyInstalledException(String name, String version)
{
super("Same version of the package is already installed: " + name + ", " + version);
myName = name;
myVersion = version;
}
public String getName()
{
return myName;
}
public String getVersion()
{
return myVersion;
}
private final String myName;
private final String myVersion;
}
/**
* Exception raised when receiving an error when trying to read a package on the web.
*/
public static class OnlineException
extends PackageException
{
public OnlineException(URI url)
{
super("Error downloading the package at URL: " + url);
myUrl = url;
}
public OnlineException(URI url, String msg)
{
super(msg);
myUrl = url;
}
public OnlineException(URI url, Exception cause)
{
super("Error downloading the package at URL: " + url, cause);
myUrl = url;
}
public URI getUrl()
{
return myUrl;
}
private final URI myUrl;
}
/**
* Exception raised when receiving 404 when trying to read a package on the web.
*/
public static class NotFoundException
extends OnlineException
{
public NotFoundException(URI url)
{
super(url, "Package not found at URL: " + url);
}
}
/**
* Exception raised when receiving 404 when trying to read a package on the web.
*/
public static class HttpException
extends OnlineException
{
public HttpException(URI url, int code, String status)
{
super(url, "HTTP error at URL: " + url + ", code: " + code + ", status: " + status);
myCode = code;
myStatus = status;
}
public int getCode()
{
return myCode;
}
public String getStatus()
{
return myStatus;
}
private final int myCode;
private final String myStatus;
}
}
/* ------------------------------------------------------------------------ */
/* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS COMMENT. */
/* */
/* The contents of this file are subject to the Mozilla Public License */
/* Version 1.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.mozilla.org/MPL/. */
/* */
/* Software distributed under the License is distributed on an "AS IS" */
/* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See */
/* the License for the specific language governing rights and limitations */
/* under the License. */
/* */
/* The Original Code is: all this file. */
/* */
/* The Initial Developer of the Original Code is Florent Georges. */
/* */
/* Contributor(s): none. */
/* ------------------------------------------------------------------------ */
© 2015 - 2024 Weber Informatics LLC | Privacy Policy