/* (c) 2016 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geogig.geoserver.config;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.geoserver.catalog.Predicates.and;
import static org.geoserver.catalog.Predicates.equal;
import static org.locationtech.geogig.geotools.data.GeoGigDataStoreFactory.REPOSITORY;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Serializable;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.UUID;
import javax.annotation.Nullable;
import org.geoserver.catalog.CascadeDeleteVisitor;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.CatalogInfo;
import org.geoserver.catalog.DataStoreInfo;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.util.CloseableIterator;
import org.geoserver.config.GeoServer;
import org.geoserver.config.GeoServerInitializer;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.resource.Resource;
import org.locationtech.geogig.cli.CLIContextBuilder;
import org.locationtech.geogig.geotools.data.GeoGigDataStoreFactory;
import org.locationtech.geogig.model.Ref;
import org.locationtech.geogig.porcelain.BranchListOp;
import org.locationtech.geogig.porcelain.ConfigOp;
import org.locationtech.geogig.porcelain.InitOp;
import org.locationtech.geogig.remote.IRemoteRepo;
import org.locationtech.geogig.remote.RemoteUtils;
import org.locationtech.geogig.repository.Context;
import org.locationtech.geogig.repository.Hints;
import org.locationtech.geogig.repository.Remote;
import org.locationtech.geogig.repository.Repository;
import org.locationtech.geogig.repository.RepositoryConnectionException;
import org.locationtech.geogig.repository.RepositoryResolver;
import org.locationtech.geogig.repository.impl.ContextBuilder;
import org.locationtech.geogig.repository.impl.GeoGIG;
import org.locationtech.geogig.repository.impl.GlobalContextBuilder;
import org.opengis.filter.Filter;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
public class RepositoryManager implements GeoServerInitializer {
static {
if (GlobalContextBuilder.builder() == null
|| GlobalContextBuilder.builder().getClass().equals(ContextBuilder.class)) {
GlobalContextBuilder.builder(new CLIContextBuilder());
}
}
private static class StaticSupplier implements Supplier<RepositoryManager>, Serializable {
private static final long serialVersionUID = 3706728433275296134L;
@Override
public RepositoryManager get() {
return RepositoryManager.get();
}
}
private final ConfigStore store;
private final RepositoryCache repoCache;
private static RepositoryManager INSTANCE;
private Catalog catalog;
public static synchronized RepositoryManager get() {
if (INSTANCE == null) {
INSTANCE = GeoServerExtensions.bean(RepositoryManager.class);
Preconditions.checkState(INSTANCE != null);
}
return INSTANCE;
}
public static void close() {
if (INSTANCE != null) {
INSTANCE.repoCache.invalidateAll();
INSTANCE = null;
}
}
public static Supplier<RepositoryManager> supplier() {
return new StaticSupplier();
}
public RepositoryManager(ConfigStore store) {
checkNotNull(store);
this.store = store;
this.repoCache = new RepositoryCache(this);
}
@Override
public void initialize(GeoServer geoServer) {
// set the catalog\
setCatalog(geoServer.getCatalog());
}
public List<RepositoryInfo> getAll() {
return store.getRepositories();
}
public void invalidate(final String repoId) {
this.repoCache.invalidate(repoId);
}
@Nullable
public Repository createRepo(final Hints hints) {
// get the Config store location
// only generate a location if no URI is set in the hints
URI repoURI;
if (hints.get(Hints.REPOSITORY_URL).isPresent()) {
repoURI = URI.create(hints.get(Hints.REPOSITORY_URL).get().toString());
} else {
// no location set yet, generate one
Resource root = store.getConfigRoot();
File parent = root.parent().dir().getAbsoluteFile();
File f = new File(parent, UUID.randomUUID().toString());
repoURI = f.toURI().normalize();
hints.set(Hints.REPOSITORY_URL, repoURI);
}
Context context = GlobalContextBuilder.builder().build(hints);
RepositoryResolver repositoryResolver = RepositoryResolver.lookup(repoURI);
final boolean exists = repositoryResolver.repoExists(repoURI);
Repository repository = context.repository();
if (exists) {
try {
repository.open();
} catch (RepositoryConnectionException e) {
throw Throwables.propagate(e);
}
}
return repository;
}
public RepositoryInfo get(final String repoId) throws IOException {
try {
return store.get(repoId);
} catch (FileNotFoundException e) {
throw new NoSuchElementException("No repository with ID " + repoId + " exists");
}
}
public RepositoryInfo getByRepoName(final String name) {
List<RepositoryInfo> all = getAll();
for (RepositoryInfo info : all) {
if (info.getRepoName().equals(name)) {
return info;
}
}
// didn't find it
throw new NoSuchElementException("No repository with ID " + name + " exists");
}
public List<DataStoreInfo> findGeogigStores() {
return findGeogigStores(this.catalog);
}
public Catalog getCatalog() {
return this.catalog;
}
public void setCatalog(Catalog catalog) {
this.catalog = catalog;
}
static List<DataStoreInfo> findGeogigStores(Catalog catalog) {
org.opengis.filter.Filter filter = equal("type", GeoGigDataStoreFactory.DISPLAY_NAME);
return findGeoGigStores(catalog, filter);
}
static List<DataStoreInfo> findGeogigStoresWithOldConfiguration(Catalog catalog) {
// NOTE: Using a pre Filter and a post Predicate instead of a single AND Filter because
// JDBCConfig doesn't know how to translate a connectionParameters.resolver Filter into an
// SQL expression
org.opengis.filter.Filter preFilter = equal("type", GeoGigDataStoreFactory.DISPLAY_NAME);
List<DataStoreInfo> stores = findGeoGigStores(catalog, preFilter);
Predicate<DataStoreInfo> postFilter = new Predicate<DataStoreInfo>() {
@Override
public boolean apply(DataStoreInfo st) {
final Map<String, Serializable> conParams = st.getConnectionParameters();
if (conParams.containsKey(REPOSITORY.key)) {
final File repoConfigFile = new File(conParams.get(REPOSITORY.key).toString());
if (repoConfigFile.isDirectory()) {
return true;
}
}
return false;
}
};
return Lists.newArrayList(Iterables.filter(stores, postFilter));
}
private static List<DataStoreInfo> findGeoGigStores(Catalog catalog,
org.opengis.filter.Filter filter) {
List<DataStoreInfo> geogigStores;
try (CloseableIterator<DataStoreInfo> dataStores = catalog.list(DataStoreInfo.class, filter)) {
geogigStores = Lists.newArrayList(dataStores);
}
return geogigStores;
}
public List<DataStoreInfo> findDataStores(final String repoId) {
// get the name
String repoName = null;
try {
repoName = this.get(repoId).getRepoName();
} catch (IOException ioe) {
Throwables.propagate(ioe);
}
Filter filter = equal("type", GeoGigDataStoreFactory.DISPLAY_NAME);
String locationKey = "connectionParameters." + GeoGigDataStoreFactory.REPOSITORY.key;
filter = and(filter, equal(locationKey, GeoServerGeoGigRepositoryResolver.getURI(repoName)));
List<DataStoreInfo> dependent;
try (CloseableIterator<DataStoreInfo> stores = this.catalog.list(DataStoreInfo.class,
filter)) {
dependent = Lists.newArrayList(stores);
}
return dependent;
}
public List<? extends CatalogInfo> findDependentCatalogObjects(final String repoId) {
List<DataStoreInfo> stores = findDataStores(repoId);
List<CatalogInfo> dependent = new ArrayList<>(stores);
for (DataStoreInfo dataStore : stores) {
List<FeatureTypeInfo> ftypes = this.catalog.getFeatureTypesByDataStore(dataStore);
dependent.addAll(ftypes);
for (FeatureTypeInfo ftype : ftypes) {
dependent.addAll(this.catalog.getLayers(ftype));
}
}
return dependent;
}
public List<LayerInfo> findLayers(DataStoreInfo store) {
Filter filter = equal("resource.store.id", store.getId());
try (CloseableIterator<LayerInfo> it = this.catalog.list(LayerInfo.class, filter)) {
return Lists.newArrayList(it);
}
}
public List<FeatureTypeInfo> findFeatureTypes(DataStoreInfo store) {
Filter filter = equal("store.id", store.getId());
try (CloseableIterator<FeatureTypeInfo> it = this.catalog.list(FeatureTypeInfo.class,
filter)) {
return Lists.newArrayList(it);
}
}
public static boolean isGeogigDirectory(final File file) {
if (file == null) {
return false;
}
final File geogigDir = new File(file, ".geogig");
final boolean isGeogigDirectory = geogigDir.exists() && geogigDir.isDirectory();
return isGeogigDirectory;
}
private void handleRepoRename(RepositoryInfo oldRepo, RepositoryInfo newRepo) {
if (Objects.equal(oldRepo.getId(), newRepo.getId())) {
// repos have the same ID, check the names
final String oldName = oldRepo.getRepoName();
final String newName = newRepo.getRepoName();
if (!Objects.equal(oldName, newName)) {
// name has been changed, update the repo
try {
getRepository(oldRepo.getId()).command(ConfigOp.class)
.setAction(ConfigOp.ConfigAction.CONFIG_SET).setName("repo.name")
.setScope(ConfigOp.ConfigScope.LOCAL).setValue(newName).call();
} catch (IOException ioe) {
// log?
}
}
}
}
public RepositoryInfo save(RepositoryInfo info) {
Preconditions.checkNotNull(info.getLocation());
if (info.getId() == null) {
create(info);
} else {
// see if the name has changed. If so, update the repo config
try {
RepositoryInfo currentInfo = get(info.getId());
handleRepoRename(currentInfo, info);
} catch (IOException ioe) {
}
}
// so far we don't need to invalidate the GeoGIG instance from the cache here... re-evaluate
// if any configuration option would require so in the future
return store.save(info);
}
private void create(final RepositoryInfo repoInfo) {
// File targetDirectory = new File(repoInfo.getLocation());
// Preconditions.checkArgument(!isGeogigDirectory(targetDirectory));
URI repoURI = repoInfo.getLocation();
RepositoryResolver resolver = RepositoryResolver.lookup(repoURI);
if (!resolver.repoExists(repoURI)) {
Hints hints = new Hints();
hints.set(Hints.REPOSITORY_URL, repoURI);
hints.set(Hints.REPOSITORY_NAME, repoInfo.getRepoName());
Context context = GlobalContextBuilder.builder().build(hints);
GeoGIG geogig = new GeoGIG(context);
try {
Repository repository = geogig.command(InitOp.class).call();
Preconditions.checkState(repository != null);
} finally {
geogig.close();
}
}
}
public List<Ref> listBranches(final String repositoryId) throws IOException {
Repository geogig = getRepository(repositoryId);
List<Ref> refs = geogig.command(BranchListOp.class).call();
return refs;
}
public Repository getRepository(String repositoryId) throws IOException {
Repository repository = repoCache.get(repositoryId);
return repository;
}
public void delete(final String repoId) {
List<DataStoreInfo> repoStores = findDataStores(repoId);
CascadeDeleteVisitor deleteVisitor = new CascadeDeleteVisitor(this.catalog);
for (DataStoreInfo storeInfo : repoStores) {
storeInfo.accept(deleteVisitor);
}
try {
this.store.delete(repoId);
} finally {
this.repoCache.invalidate(repoId);
}
}
RepositoryInfo findOrCreateByLocation(final URI repositoryURI) {
List<RepositoryInfo> repos = getAll();
for (RepositoryInfo info : repos) {
if (Objects.equal(info.getLocation(), repositoryURI)) {
return info;
}
}
RepositoryInfo info = new RepositoryInfo();
info.setLocation(repositoryURI);
return save(info);
}
/**
* Utility class to connect to a remote to see if its alive and we're able to connect.
*
* @return the remote's head ref if succeeded
* @throws Exception if can't connect for any reason; the exception message should be indicative
* of the problem
*/
public static Ref pingRemote(final String location, @Nullable String user,
@Nullable String password) throws Exception {
if (Strings.isNullOrEmpty(location)) {
throw new IllegalArgumentException("Please indicate the remote repository URL");
}
Remote remote;
{
String fetchurl = location;
String pushurl = location;
String name = "tempremote";
String fetch = "+" + Ref.HEADS_PREFIX + "*:" + Ref.REMOTES_PREFIX + name + "/*";
boolean mapped = false;
String mappedBranch = null;
remote = new Remote(name, fetchurl, pushurl, fetch, mapped, mappedBranch, user,
password);
}
return pingRemote(remote);
}
private static Ref pingRemote(Remote remote) throws Exception {
Optional<IRemoteRepo> remoteRepo;
try {
Hints hints = Hints.readOnly();
Repository localRepo = GlobalContextBuilder.builder().build(hints).repository();
remoteRepo = RemoteUtils.newRemote(localRepo, remote, null);
if (!remoteRepo.isPresent()) {
throw new IllegalArgumentException("Repository not found or not reachable");
} else {
IRemoteRepo repo = remoteRepo.get();
try {
repo.open();
Ref head = repo.headRef();
return head;
} finally {
repo.close();
}
}
} catch (Exception e) {
throw new IllegalArgumentException("Unable to connect: " + e.getMessage(), e);
}
}
}