Java tutorial
/** * Copyright 2018 interactive instruments GmbH * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package de.ii.xtraplatform.feature.provider.pgis; import akka.Done; import akka.actor.ActorSystem; import akka.stream.ActorMaterializer; import akka.stream.alpakka.slick.javadsl.SlickSession; import akka.stream.javadsl.RunnableGraph; import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.ListenableFuture; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import de.ii.xtraplatform.akka.http.ActorSystemProvider; import de.ii.xtraplatform.crs.api.CrsTransformation; import de.ii.xtraplatform.crs.api.CrsTransformer; import de.ii.xtraplatform.feature.query.api.FeatureConsumer; import de.ii.xtraplatform.feature.query.api.FeatureQuery; import de.ii.xtraplatform.feature.query.api.FeatureStream; import de.ii.xtraplatform.feature.query.api.TargetMapping; import de.ii.xtraplatform.feature.transformer.api.FeatureTransformer; import de.ii.xtraplatform.feature.transformer.api.FeatureTypeMapping; import de.ii.xtraplatform.feature.transformer.api.SourcePathMapping; import de.ii.xtraplatform.feature.transformer.api.TransformingFeatureProvider; import org.apache.felix.ipojo.annotations.Component; import org.apache.felix.ipojo.annotations.Context; import org.apache.felix.ipojo.annotations.Property; import org.apache.felix.ipojo.annotations.Provides; import org.apache.felix.ipojo.annotations.Requires; import org.apache.felix.ipojo.annotations.StaticServiceProperty; import org.apache.http.HttpEntity; import org.osgi.framework.BundleContext; import org.osgi.framework.wiring.BundleWiring; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import scala.reflect.ClassTag$; import slick.basic.DatabaseConfig; import slick.basic.DatabaseConfig$; import slick.jdbc.JdbcProfile; import javax.ws.rs.BadRequestException; import javax.ws.rs.NotFoundException; import javax.ws.rs.WebApplicationException; import java.util.AbstractMap; import java.util.Base64; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; import java.util.function.Function; import java.util.stream.Collectors; import static de.ii.xtraplatform.feature.provider.pgis.FeatureProviderPgis.PROVIDER_TYPE; import static de.ii.xtraplatform.feature.query.api.TargetMapping.BASE_TYPE; /** * @author zahnen */ @Component @Provides(properties = { @StaticServiceProperty(name = "providerType", type = "java.lang.String", value = PROVIDER_TYPE) }) public class FeatureProviderPgis implements TransformingFeatureProvider<FeatureTransformer, FeatureConsumer> { private static final Logger LOGGER = LoggerFactory.getLogger(FeatureProviderPgis.class); static final String PROVIDER_TYPE = "PGIS"; private static final Config config = ConfigFactory.parseMap(new ImmutableMap.Builder<String, Object>().build()); private final ActorSystem system; private final ActorMaterializer materializer; private final FeatureProviderDataPgis data; private SlickSession session; private Map<String, SqlFeatureSource> featureSources; private Map<String, SqlFeatureInserts> featureAddSinks; private Map<String, SqlFeatureInserts> featureUpdateSinks; private SqlFeatureRemover featureRemover; FeatureProviderPgis(@Context BundleContext context, @Requires ActorSystemProvider actorSystemProvider, @Requires CrsTransformation crsTransformation, @Property(name = ".data") FeatureProviderDataPgis data) { //TODO: starts akka for every instance, move to singleton this.system = actorSystemProvider.getActorSystem(context, config); this.materializer = ActorMaterializer.create(system); this.data = data; LOGGER.debug("CREATED PGIS: {}"/*, data*/); try { // bundle class loader has to be passed to Slick for initialization ClassLoader classLoader = context.getBundle().adapt(BundleWiring.class).getClassLoader(); Thread.currentThread().setContextClassLoader(classLoader); DatabaseConfig<JdbcProfile> databaseConfig = DatabaseConfig$.MODULE$.forConfig("", createSlickConfig(data.getConnectionInfo()), classLoader, ClassTag$.MODULE$.apply(JdbcProfile.class)); this.session = SlickSession.forConfig(databaseConfig); system.registerOnTermination(session::close); this.featureSources = createFeatureSources(data.getMappings()); this.featureAddSinks = createFeatureSinks(data.getMappings(), false); this.featureUpdateSinks = createFeatureSinks(data.getMappings(), true); this.featureRemover = new SqlFeatureRemover(session, materializer); } catch (Throwable e) { LOGGER.error("CONNECTING TO DB FAILED", e); this.session = null; this.featureSources = new LinkedHashMap<>(); } } @Override public FeatureStream<FeatureConsumer> getFeatureStream(FeatureQuery query) { return featureConsumer -> createFeatureStream(query, featureConsumer); } @Override public FeatureStream<FeatureTransformer> getFeatureTransformStream(FeatureQuery query) { return featureTransformer -> createFeatureStream(query, new FeatureTransformerFromSql(data.getMappings().get(query.getType()), featureTransformer)); } @Override public List<String> addFeaturesFromStream(String featureType, CrsTransformer crsTransformer, Function<FeatureTransformer, RunnableGraph<CompletionStage<Done>>> stream) { Optional<SqlFeatureInserts> featureSink = Optional.ofNullable(featureAddSinks.get(featureType)); if (!featureSink.isPresent()) { throw new NotFoundException("Feature type " + featureType + " not found"); } //TODO: merge classes SqlFeatureCreator sqlFeatureCreator = new SqlFeatureCreator(session, materializer, featureSink.get()); FeatureTransformerSql featureTransformerSql = new FeatureTransformerSql(sqlFeatureCreator, crsTransformer); try { stream.apply(featureTransformerSql).run(materializer).toCompletableFuture().join(); return featureTransformerSql.getIds(); } catch (CompletionException e) { if (e.getCause() instanceof WebApplicationException) { throw (WebApplicationException) e.getCause(); } else if (e.getCause() instanceof JsonProcessingException) { throw new BadRequestException("Input could not be parsed", e.getCause()); } LOGGER.error("Could not add feature", e.getCause()); throw new BadRequestException("Feature not valid, could not be written"); } } @Override public void updateFeatureFromStream(String featureType, String id, CrsTransformer crsTransformer, Function<FeatureTransformer, RunnableGraph<CompletionStage<Done>>> stream) { Optional<SqlFeatureInserts> featureSink = Optional.ofNullable(featureUpdateSinks.get(featureType)); if (!featureSink.isPresent()) { throw new NotFoundException("Feature type " + featureType + " not found"); } /* boolean removed = featureRemover.remove(featureType, id); if (!removed) { throw new NotFoundException(); }*/ //TODO: merge classes SqlFeatureCreator sqlFeatureCreator = new SqlFeatureCreator(session, materializer, featureSink.get()); FeatureTransformerSql featureTransformerSql = new FeatureTransformerSql(sqlFeatureCreator, crsTransformer, id); try { stream.apply(featureTransformerSql).run(materializer).toCompletableFuture().join(); List<String> ids = featureTransformerSql.getIds(); LOGGER.debug("PUT {}", ids); } catch (CompletionException e) { if (e.getCause() instanceof WebApplicationException) { throw (WebApplicationException) e.getCause(); } else if (e.getCause() instanceof JsonProcessingException) { throw new BadRequestException("Input could not be parsed", e.getCause()); } LOGGER.error("Could not add feature", e.getCause()); throw new BadRequestException("Feature not valid, could not be written"); } } @Override public void deleteFeature(String featureType, String id) { featureRemover.remove(featureType, id); } @Override public Optional<String> encodeFeatureQuery(FeatureQuery query) { return Optional.empty(); } @Override public String getSourceFormat() { return null; } private CompletionStage<Done> createFeatureStream(FeatureQuery query, FeatureConsumer featureConsumer) { Optional<SqlFeatureSource> featureSource = Optional.ofNullable(featureSources.get(query.getType())); if (!featureSource.isPresent()) { CompletableFuture<Done> promise = new CompletableFuture<>(); promise.completeExceptionally(new IllegalStateException("No features available for type")); return promise; } return featureSource.get().runQuery(query, featureConsumer); } private Config createSlickConfig(ConnectionInfo connectionInfo) { String password = connectionInfo.getPassword(); try { password = new String(Base64.getDecoder().decode(password), Charsets.UTF_8); } catch (IllegalArgumentException e) { //ignore if not valid base64 } return ConfigFactory.parseMap(ImmutableMap.<String, Object>builder() .put("profile", "slick.jdbc.PostgresProfile$") //.put("dataSourceClass", "org.postgresql.ds.PGSimpleDataSource") .put("db", ImmutableMap.<String, Object>builder().put("user", connectionInfo.getUser()) .put("password", password).put("dataSourceClass", "org.postgresql.ds.PGSimpleDataSource") //.put("hikaricp.dataSourceClassName", "org.postgresql.ds.PGSimpleDataSource") //.put("hikaricp.datasource.user", connectionInfo.getUser()) //.put("hikaricp.datasource.password", connectionInfo.getPassword()) .put("properties.serverName", connectionInfo.getHost()) .put("properties.databaseName", connectionInfo.getDatabase()) //.put("url", String.format("jdbc:postgresql://%s/%s", connectionInfo.getHost(), connectionInfo.getDatabase())) .put("numThreads", 10).put("initializationFailFast", true).build()) .build()); } private Map<String, SqlFeatureSource> createFeatureSources(Map<String, FeatureTypeMapping> mappings) { return mappings.entrySet().stream().map(featureTypeMappings -> { Set<String> multiTables = new HashSet<>(); List<String> paths = featureTypeMappings.getValue().getMappings().entrySet().stream() .sorted((stringSourcePathMappingEntry, t1) -> { SourcePathMapping value = t1.getValue(); TargetMapping mappingForType = value.getMappingForType(TargetMapping.BASE_TYPE); Integer sortPriority = mappingForType.getSortPriority(); SourcePathMapping value1 = stringSourcePathMappingEntry.getValue(); TargetMapping mappingForType1 = value1.getMappingForType(TargetMapping.BASE_TYPE); Integer sortPriority1 = mappingForType1.getSortPriority(); if (sortPriority1 == null) { return 1; } if (sortPriority == null) { return -1; } return sortPriority1 - sortPriority; }).peek(stringSourcePathMappingEntry -> { TargetMapping mapping = stringSourcePathMappingEntry.getValue() .getMappingForType(TargetMapping.BASE_TYPE); if (mapping != null && mapping.getName() != null) { int i = mapping.getName().indexOf("["); while (i > -1) { multiTables .add(mapping.getName().substring(i + 1, mapping.getName().indexOf("]", i))); i = mapping.getName().indexOf("[", i + 1); } } }).map(toPathWithGeomAsWkt()).collect(Collectors.toList()); //LOGGER.debug("PATHS {} {}", featureTypeMappings.getKey(), paths); SqlPathTree sqlPathTree = new SqlPathTree.Builder().fromPaths(paths).build(); ImmutableSqlFeatureQueries queries = ImmutableSqlFeatureQueries.builder().paths(paths) .multiTables(multiTables).sqlPaths(sqlPathTree).build(); SqlFeatureSource sqlFeatureSource = new SqlFeatureSource(session, queries, materializer, data.computeNumberMatched()); return new AbstractMap.SimpleImmutableEntry<>(featureTypeMappings.getKey(), sqlFeatureSource); }).collect(ImmutableMap.toImmutableMap(AbstractMap.SimpleImmutableEntry::getKey, AbstractMap.SimpleImmutableEntry::getValue)); } private Map<String, SqlFeatureInserts> createFeatureSinks(Map<String, FeatureTypeMapping> mappings, boolean withId) { return mappings.entrySet().stream().map(entry -> new AbstractMap.SimpleImmutableEntry<>(entry.getKey(), ImmutableSqlFeatureInserts.builder().withId(withId) .sqlPaths(new SqlPathTree.Builder().fromPaths(entry.getValue().getMappings().entrySet() .stream().map(toPathWithGeomAsWkt()).collect(Collectors.toList())).build()) .build())) .collect(ImmutableMap.toImmutableMap(AbstractMap.SimpleImmutableEntry::getKey, AbstractMap.SimpleImmutableEntry::getValue)); } private Function<Map.Entry<String, SourcePathMapping>, String> toPathWithGeomAsWkt() { return mapping -> mapping.getValue().getMappingForType(BASE_TYPE).isSpatial() ? geomToWkt(mapping.getKey()) : mapping.getKey(); } private String geomToWkt(String path) { int sep = path.lastIndexOf("/") + 1; return path.substring(0, sep) + "ST_AsText(ST_ForcePolygonCCW(" + path.substring(sep) + "))"; } }