diff --git a/deegree-core/deegree-core-protocol/deegree-protocol-wms/src/main/java/org/deegree/protocol/wms/client/WMSCapabilitiesAdapter.java b/deegree-core/deegree-core-protocol/deegree-protocol-wms/src/main/java/org/deegree/protocol/wms/client/WMSCapabilitiesAdapter.java index 95ef8d80d0..9a7c395be4 100644 --- a/deegree-core/deegree-core-protocol/deegree-protocol-wms/src/main/java/org/deegree/protocol/wms/client/WMSCapabilitiesAdapter.java +++ b/deegree-core/deegree-core-protocol/deegree-protocol-wms/src/main/java/org/deegree/protocol/wms/client/WMSCapabilitiesAdapter.java @@ -444,6 +444,7 @@ private Style parseStyle( String styleName, OMElement styleEl ) String url = getNodeAsString( styleEl, new XPath( getPrefix() + "LegendURL/" + getPrefix() + "OnlineResource/@xlink:href", nsContext ), null ); Style style = new Style(); + style.setName( styleName ); if ( url != null ) { style.setLegendURL( new URL( url ) ); } diff --git a/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/main/java/org/deegree/sqldialect/SortCriterion.java b/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/main/java/org/deegree/sqldialect/SortCriterion.java index a094acfa86..cdb65dc749 100644 --- a/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/main/java/org/deegree/sqldialect/SortCriterion.java +++ b/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/main/java/org/deegree/sqldialect/SortCriterion.java @@ -1,25 +1,33 @@ package org.deegree.sqldialect; +import org.deegree.commons.jdbc.TableName; + /** * @author Lyn Goltz */ public class SortCriterion { - private final String columneName; + private final String columnName; + + private final TableName tableName; private final boolean sortAscending; - public SortCriterion( String columneName, boolean sortAscending ) { - this.columneName = columneName; + public SortCriterion( String columnName, TableName tableName, boolean sortAscending ) { + this.columnName = columnName; this.sortAscending = sortAscending; + this.tableName = tableName; } - public String getColumneName() { - return columneName; + public String getColumnName() { + return columnName; + } + + public TableName getTableName() { + return tableName; } public boolean isSortAscending() { return sortAscending; } - } \ No newline at end of file diff --git a/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/main/java/org/deegree/sqldialect/filter/AbstractWhereBuilder.java b/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/main/java/org/deegree/sqldialect/filter/AbstractWhereBuilder.java index c5c798abae..6be5d71cc5 100644 --- a/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/main/java/org/deegree/sqldialect/filter/AbstractWhereBuilder.java +++ b/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/main/java/org/deegree/sqldialect/filter/AbstractWhereBuilder.java @@ -981,8 +981,8 @@ protected SQLExpression toProtoSQL( List sortCriteria ) if ( sortCriteria.indexOf( sortCriterion ) > 0 ) { builder.add( "," ); } - String rootTableAlias = aliasManager.getRootTableAlias(); - String columnName = sortCriterion.getColumneName(); + String rootTableAlias = aliasManager.getTableAlias( sortCriterion.getTableName() ); + String columnName = sortCriterion.getColumnName(); builder.add( rootTableAlias == null ? columnName : ( rootTableAlias + "." + columnName ) ); if ( sortCriterion.isSortAscending() ) { builder.add( " ASC" ); diff --git a/deegree-core/deegree-core-theme/src/main/java/org/deegree/theme/persistence/standard/StandardThemeBuilder.java b/deegree-core/deegree-core-theme/src/main/java/org/deegree/theme/persistence/standard/StandardThemeBuilder.java index e3f6801594..b0503e6ba9 100644 --- a/deegree-core/deegree-core-theme/src/main/java/org/deegree/theme/persistence/standard/StandardThemeBuilder.java +++ b/deegree-core/deegree-core-theme/src/main/java/org/deegree/theme/persistence/standard/StandardThemeBuilder.java @@ -45,8 +45,11 @@ Occam Labs UG (haftungsbeschränkt) import static org.deegree.theme.Themes.aggregateSpatialMetadata; import static org.slf4j.LoggerFactory.getLogger; +import java.io.File; +import java.net.URL; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -77,10 +80,10 @@ Occam Labs UG (haftungsbeschränkt) /** * Builds a {@link StandardTheme} from jaxb config beans. - * + * * @author Andreas Schmitz * @author last edited by: $Author: stranger $ - * + * * @version $Revision: $, $Date: $ */ public class StandardThemeBuilder implements ResourceBuilder { @@ -170,11 +173,46 @@ private StandardTheme buildTheme( ThemeType current, List layer md.setRequestable( false ); } md.setDimensions( dims ); - md.setStyles( styles ); - md.setLegendStyles( legendStyles ); + if ( current.getLegendGraphic() != null && current.getLegendGraphic().getValue() != null + && !current.getLegendGraphic().getValue().isEmpty() ) { + Map configuredLegendStyles = new HashMap<>(); + Style style = parseConfiguredStyle( current.getLegendGraphic() ); + configuredLegendStyles.put( style.getName(), style ); + md.setStyles( configuredLegendStyles ); + md.setLegendStyles( configuredLegendStyles ); + } else { + md.setStyles( styles ); + md.setLegendStyles( legendStyles ); + } return new StandardTheme( md, thms, lays, metadata ); } + private Style parseConfiguredStyle( ThemeType.LegendGraphic configuredLegendGraphic ) { + Style style = new Style(); + style.setName( "default" ); + URL url = null; + try { + url = new URL( configuredLegendGraphic.getValue() ); + if ( url.toURI().isAbsolute() ) { + style.setLegendURL( url ); + } + style.setPrefersGetLegendGraphicUrl( configuredLegendGraphic.isOutputGetLegendGraphicUrl() ); + } catch ( Exception e ) { + LOG.debug( "LegendGraphic was not an absolute URL." ); + LOG.trace( "Stack trace:", e ); + } + + if ( url == null ) { + File file = metadata.getLocation().resolveToFile( configuredLegendGraphic.getValue() ); + if ( file.exists() ) { + style.setLegendFile( file ); + } else { + LOG.warn( "LegendGraphic {} could not be resolved to a legend.", configuredLegendGraphic ); + } + } + return style; + } + private Theme buildAutoTheme( Layer layer ) { LayerMetadata md = new LayerMetadata( null, null, null ); LayerMetadata lmd = layer.getMetadata(); diff --git a/deegree-core/deegree-core-theme/src/main/resources/META-INF/schemas/themes/themes.xsd b/deegree-core/deegree-core-theme/src/main/resources/META-INF/schemas/themes/themes.xsd index 16cc6cf26b..7f2778af0a 100644 --- a/deegree-core/deegree-core-theme/src/main/resources/META-INF/schemas/themes/themes.xsd +++ b/deegree-core/deegree-core-theme/src/main/resources/META-INF/schemas/themes/themes.xsd @@ -26,6 +26,15 @@ + + + + + + + + + diff --git a/deegree-datastores/deegree-featurestores/deegree-featurestore-commons/src/main/java/org/deegree/feature/persistence/FeatureStoreManager.java b/deegree-datastores/deegree-featurestores/deegree-featurestore-commons/src/main/java/org/deegree/feature/persistence/FeatureStoreManager.java index 8a49ebf874..4f78dbd026 100644 --- a/deegree-datastores/deegree-featurestores/deegree-featurestore-commons/src/main/java/org/deegree/feature/persistence/FeatureStoreManager.java +++ b/deegree-datastores/deegree-featurestores/deegree-featurestore-commons/src/main/java/org/deegree/feature/persistence/FeatureStoreManager.java @@ -41,9 +41,6 @@ Occam Labs UG (haftungsbeschränkt) ----------------------------------------------------------------------------*/ package org.deegree.feature.persistence; -import java.io.File; -import java.io.IOException; - import org.deegree.feature.persistence.cache.BBoxCache; import org.deegree.feature.persistence.cache.BBoxPropertiesCache; import org.deegree.workspace.Workspace; @@ -53,12 +50,16 @@ Occam Labs UG (haftungsbeschränkt) import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + /** * Responsible for finding feature store resources. - * + * * @author Andreas Schmitz * @author last edited by: $Author: stranger $ - * * @version $Revision: $, $Date: $ */ public class FeatureStoreManager extends DefaultResourceManager { @@ -67,29 +68,82 @@ public class FeatureStoreManager extends DefaultResourceManager { private static final String BBOX_CACHE_FILE = "bbox_cache.properties"; + private static final String BBOX_CACHE_FEATURESTOE_FILE = "bbox_cache_%s.properties"; + private BBoxPropertiesCache bboxCache; + private final Map customBboxCaches = new HashMap<>(); + + private Workspace workspace; + public FeatureStoreManager() { - super( new DefaultResourceManagerMetadata( FeatureStoreProvider.class, "feature stores", - "datasources/feature" ) ); + super( new DefaultResourceManagerMetadata<>( FeatureStoreProvider.class, "feature stores", + "datasources/feature" ) ); } @Override public void startup( Workspace workspace ) { + this.workspace = workspace; + super.startup( workspace ); + } + + /** + * Returns the bbox_cache.properties file (which is created if not existing). + * As there may be feature store specific bbox_cache_FEATURESTOE_ID.properties + * file the method getBBoxCache( String featureStoreId ) should be used. + */ + public BBoxCache getBBoxCache() { + return getOrCreateBBoxCache(); + } + + /** + * Returns the feature store specific bbox_cache_FEATURESTOE_ID.properties if existing, + * if not the bbox_cache.properties file is returned (which is created if not existing). + * + * @param featureStoreId + * @return + */ + public BBoxCache getBBoxCache( String featureStoreId ) { + if ( customBboxCaches.containsValue( featureStoreId ) ) { + return customBboxCaches.get( featureStoreId ); + } + BBoxPropertiesCache customBBoxCache = getCustomBBoxCache( featureStoreId ); + if ( customBBoxCache != null ) { + customBboxCaches.put( featureStoreId, customBBoxCache ); + return customBBoxCache; + } + return getOrCreateBBoxCache(); + } + + private BBoxPropertiesCache getOrCreateBBoxCache() { try { - if ( workspace instanceof DefaultWorkspace ) { + if ( bboxCache == null ) { File dir = new File( ( (DefaultWorkspace) workspace ).getLocation(), getMetadata().getWorkspacePath() ); - bboxCache = new BBoxPropertiesCache( new File( dir, BBOX_CACHE_FILE ) ); + File propsFile = new File( dir, BBOX_CACHE_FILE ); + bboxCache = new BBoxPropertiesCache( propsFile ); } - // else? } catch ( IOException e ) { - LOG.error( "Unable to initialize global envelope cache: " + e.getMessage(), e ); + LOG.error( "Unable to initialize envelope cache {}: {}", BBOX_CACHE_FILE, e.getMessage() ); + LOG.trace( e.getMessage(), e ); } - super.startup( workspace ); + return bboxCache; } - public BBoxCache getBBoxCache() { - return bboxCache; + private BBoxPropertiesCache getCustomBBoxCache( String featureStoreId ) { + try { + if ( workspace instanceof DefaultWorkspace ) { + File dir = new File( ( (DefaultWorkspace) workspace ).getLocation(), getMetadata().getWorkspacePath() ); + File propsFile = new File( dir, String.format( BBOX_CACHE_FEATURESTOE_FILE, featureStoreId ) ); + if ( propsFile.exists() ) { + return new BBoxPropertiesCache( propsFile ); + } + } + } catch ( IOException e ) { + LOG.error( "Unable to initialize envelope cache for feature store with id {}: {}", featureStoreId, + e.getMessage() ); + LOG.trace( e.getMessage(), e ); + } + return null; } } diff --git a/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/pom.xml b/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/pom.xml index fbff01eedf..1a53c7cb26 100644 --- a/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/pom.xml +++ b/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/pom.xml @@ -68,7 +68,7 @@ antlr-runtime - ${project.groupId} + net.gcardone.junidecode junidecode diff --git a/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/src/main/java/org/deegree/feature/persistence/sql/SQLFeatureStore.java b/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/src/main/java/org/deegree/feature/persistence/sql/SQLFeatureStore.java index 4d48a4d72c..6aa25d469f 100644 --- a/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/src/main/java/org/deegree/feature/persistence/sql/SQLFeatureStore.java +++ b/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/src/main/java/org/deegree/feature/persistence/sql/SQLFeatureStore.java @@ -77,7 +77,6 @@ import org.deegree.commons.utils.kvp.InvalidParameterValueException; import org.deegree.cs.coordinatesystems.ICRS; import org.deegree.cs.persistence.CRSManager; -import org.deegree.cs.refs.coordinatesystem.CRSRef; import org.deegree.db.ConnectionProvider; import org.deegree.db.ConnectionProviderProvider; import org.deegree.feature.Feature; @@ -1011,10 +1010,10 @@ public FeatureInputStream query( final Query[] queries ) } } + boolean isMaxFeaturesAndStartIndexApplicable = isMaxFeaturesAndStartIndexApplicable( queries ); if ( wmsStyleQuery ) { - return queryMultipleFts( queries, env ); + return queryMultipleFtsFromBlob( queries, env, isMaxFeaturesAndStartIndexApplicable ); } - boolean isMaxFeaturesAndStartIndexApplicable = isMaxFeaturesAndStartIndexApplicable( queries ); Iterator rsIter = new Iterator() { int i = 0; @@ -1534,7 +1533,8 @@ private FeatureInputStream queryByOperatorFilter( Query query, List ftNam return result; } - private FeatureInputStream queryMultipleFts( Query[] queries, Envelope looseBBox ) + private FeatureInputStream queryMultipleFtsFromBlob( Query[] queries, Envelope looseBBox, + boolean isMaxFeaturesAndStartIndexApplicable ) throws FeatureStoreException { FeatureInputStream result = null; Connection conn = null; @@ -1547,14 +1547,20 @@ private FeatureInputStream queryMultipleFts( Query[] queries, Envelope looseBBox blobWb = getWhereBuilderBlob( bboxFilter, conn ); } conn = getConnection(); - final short[] ftId = getQueriedFeatureTypeIds( queries ); + final List ftIds = new ArrayList<>(); final StringBuilder sql = new StringBuilder(); - for ( int i = 0; i < ftId.length; i++ ) { + for ( int i = 0; i < queries.length; i++ ) { + Query query = queries[i]; + if ( query.getTypeNames() == null || query.getTypeNames().length > 1 ) { + String msg = "Join queries between multiple feature types are currently not supported."; + throw new UnsupportedOperationException( msg ); + } + short ftId = getFtId( query.getTypeNames()[0].getFeatureTypeName() ); if ( i > 0 ) { sql.append( " UNION " ); } sql.append( "SELECT gml_id,binary_object" ); - if ( ftId.length > 1 ) { + if ( queries.length > 1 ) { sql.append( "," ); sql.append( i ); sql.append( " AS QUERY_POS" ); @@ -1565,14 +1571,18 @@ private FeatureInputStream queryMultipleFts( Query[] queries, Envelope looseBBox if ( looseBBox != null ) { sql.append( " AND gml_bounded_by && ?" ); } + if (isMaxFeaturesAndStartIndexApplicable) { + appendOffsetAndFetch(sql, query.getMaxFeatures(), query.getStartIndex()); + } + ftIds.add( ftId ); } - if ( ftId.length > 1 ) { + if ( queries.length > 1 ) { sql.append( " ORDER BY QUERY_POS" ); } stmt = conn.prepareStatement( sql.toString() ); stmt.setFetchSize( fetchSize ); int argIdx = 1; - for ( final short ftId2 : ftId ) { + for ( final short ftId2 : ftIds ) { stmt.setShort( argIdx++, ftId2 ); if ( blobWb != null && blobWb.getWhere() != null ) { for ( SQLArgument o : blobWb.getWhere().getArguments() ) { @@ -1595,19 +1605,6 @@ private FeatureInputStream queryMultipleFts( Query[] queries, Envelope looseBBox return result; } - private short[] getQueriedFeatureTypeIds( Query[] queries ) { - short[] ftId = new short[queries.length]; - for ( int i = 0; i < ftId.length; i++ ) { - Query query = queries[i]; - if ( query.getTypeNames() == null || query.getTypeNames().length > 1 ) { - String msg = "Join queries between multiple feature types are currently not supported."; - throw new UnsupportedOperationException( msg ); - } - ftId[i] = getFtId( query.getTypeNames()[0].getFeatureTypeName() ); - } - return ftId; - } - private AbstractWhereBuilder getWhereBuilder( Collection ftMappings, OperatorFilter filter, SortProperty[] sortCrit, boolean handleStrict ) throws FilterEvaluationException, UnmappableException { @@ -1773,9 +1770,10 @@ public void init() { } // TODO make this configurable + String sqlFeatureStoreId = getMetadata().getIdentifier().getId(); FeatureStoreManager fsMgr = this.workspace.getResourceManager( FeatureStoreManager.class ); if ( fsMgr != null ) { - this.bboxCache = fsMgr.getBBoxCache(); + this.bboxCache = fsMgr.getBBoxCache( sqlFeatureStoreId ); } else { LOG.warn( "Unmanaged feature store." ); } diff --git a/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/src/main/java/org/deegree/feature/persistence/sql/config/AbstractMappedSchemaBuilder.java b/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/src/main/java/org/deegree/feature/persistence/sql/config/AbstractMappedSchemaBuilder.java index ccd3d32e1f..d4d95d4d12 100644 --- a/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/src/main/java/org/deegree/feature/persistence/sql/config/AbstractMappedSchemaBuilder.java +++ b/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/src/main/java/org/deegree/feature/persistence/sql/config/AbstractMappedSchemaBuilder.java @@ -57,7 +57,6 @@ import org.antlr.runtime.ANTLRStringStream; import org.antlr.runtime.CommonTokenStream; import org.antlr.runtime.RecognitionException; -import org.deegree.commons.config.DeegreeWorkspace; import org.deegree.commons.jdbc.SQLIdentifier; import org.deegree.commons.jdbc.TableName; import org.deegree.commons.tom.primitive.BaseType; @@ -236,12 +235,13 @@ protected List buildJoinTable( TableName from, org.deegree.feature.pe return null; } - protected List createSortCriteria( FeatureTypeMappingJAXB ftDecl ) { + protected List createSortCriteria( FeatureTypeMappingJAXB ftDecl, TableName tableName ) { if ( ftDecl.getOrderBy() != null ) { List columns = ftDecl.getOrderBy().getColumn(); List sortCriteria = columns.stream().map( - o -> new SortCriterion( o.getName(), "ASC".equals( o.getSortOrder() ) ) ).collect( - Collectors.toList() ); + o -> new SortCriterion( o.getName(), tableName, "ASC".equals( o.getSortOrder() ) + ) ).collect( + Collectors.toList() ); return sortCriteria; } return Collections.emptyList(); diff --git a/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/src/main/java/org/deegree/feature/persistence/sql/config/MappedSchemaBuilderGML.java b/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/src/main/java/org/deegree/feature/persistence/sql/config/MappedSchemaBuilderGML.java index 5723411073..5341cfa4be 100644 --- a/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/src/main/java/org/deegree/feature/persistence/sql/config/MappedSchemaBuilderGML.java +++ b/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/src/main/java/org/deegree/feature/persistence/sql/config/MappedSchemaBuilderGML.java @@ -331,7 +331,7 @@ private FeatureTypeMapping buildFtMapping( FeatureTypeMappingJAXB ftMappingConf particleMappings.add( buildMapping( ftTable, new Pair( elDecl, TRUE ), particle.getValue() ) ); } - List sortCriteria = createSortCriteria( ftMappingConf ); + List sortCriteria = createSortCriteria( ftMappingConf, ftTable ); return new FeatureTypeMapping( ftName, ftTable, fidMapping, particleMappings, sortCriteria ); } diff --git a/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/src/main/java/org/deegree/feature/persistence/sql/config/MappedSchemaBuilderTable.java b/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/src/main/java/org/deegree/feature/persistence/sql/config/MappedSchemaBuilderTable.java index 799382dda6..558a20d6e1 100644 --- a/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/src/main/java/org/deegree/feature/persistence/sql/config/MappedSchemaBuilderTable.java +++ b/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/src/main/java/org/deegree/feature/persistence/sql/config/MappedSchemaBuilderTable.java @@ -103,9 +103,9 @@ /** * Generates {@link MappedAppSchema} instances (table-driven mode). - * + * * @author Markus Schneider - * + * * @since 3.2 */ public class MappedSchemaBuilderTable extends AbstractMappedSchemaBuilder { @@ -129,7 +129,7 @@ public class MappedSchemaBuilderTable extends AbstractMappedSchemaBuilder { /** * Creates a new {@link MappedSchemaBuilderTable} instance. - * + * * @param jdbcConnId * identifier of JDBC connection, must not be null (used to determine columns / types) * @param ftDecls @@ -155,7 +155,7 @@ public MappedSchemaBuilderTable( String jdbcConnId, List /** * Returns the {@link MappedAppSchema} derived from configuration / tables. - * + * * @return mapped application schema, never null */ @Override @@ -198,7 +198,7 @@ private void process( FeatureTypeMappingJAXB ftDecl ) FIDMapping fidMapping = buildFIDMapping( table, ftName, ftDecl.getFIDMapping() ); List> propDecls = ftDecl.getAbstractParticle(); - List sortCriteria = createSortCriteria( ftDecl ); + List sortCriteria = createSortCriteria( ftDecl, table ); if ( propDecls != null && !propDecls.isEmpty() ) { buildFeatureTypeAndMapping( table, ftName, fidMapping, propDecls, sortCriteria ); } else { diff --git a/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/src/main/java/org/deegree/feature/persistence/sql/mapper/MappingContextManager.java b/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/src/main/java/org/deegree/feature/persistence/sql/mapper/MappingContextManager.java index c7dfd118fd..442db2faa2 100644 --- a/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/src/main/java/org/deegree/feature/persistence/sql/mapper/MappingContextManager.java +++ b/deegree-datastores/deegree-featurestores/deegree-featurestore-sql/src/main/java/org/deegree/feature/persistence/sql/mapper/MappingContextManager.java @@ -40,7 +40,7 @@ import javax.xml.namespace.QName; -import gcardone.junidecode.Junidecode; +import net.gcardone.junidecode.Junidecode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/deegree-layers/deegree-layers-remotewms/src/main/java/org/deegree/layer/persistence/remotewms/RemoteWmsLayerBuilder.java b/deegree-layers/deegree-layers-remotewms/src/main/java/org/deegree/layer/persistence/remotewms/RemoteWmsLayerBuilder.java index ed38c1ae25..a110dfb1ee 100644 --- a/deegree-layers/deegree-layers-remotewms/src/main/java/org/deegree/layer/persistence/remotewms/RemoteWmsLayerBuilder.java +++ b/deegree-layers/deegree-layers-remotewms/src/main/java/org/deegree/layer/persistence/remotewms/RemoteWmsLayerBuilder.java @@ -41,11 +41,14 @@ Occam Labs UG (haftungsbeschränkt) ----------------------------------------------------------------------------*/ package org.deegree.layer.persistence.remotewms; +import java.io.File; import java.net.URL; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; import org.deegree.commons.ows.metadata.Description; import org.deegree.commons.ows.metadata.DescriptionConverter; @@ -58,23 +61,23 @@ Occam Labs UG (haftungsbeschränkt) import org.deegree.layer.metadata.LayerMetadata; import org.deegree.layer.metadata.XsltFile; import org.deegree.layer.persistence.LayerStore; -import org.deegree.layer.persistence.remotewms.jaxb.GMLVersionType; import org.deegree.layer.persistence.base.jaxb.ScaleDenominatorsType; import org.deegree.layer.persistence.remotewms.jaxb.LayerType; -import org.deegree.layer.persistence.remotewms.jaxb.LayerType.XSLTFile; import org.deegree.layer.persistence.remotewms.jaxb.RemoteWMSLayers; import org.deegree.layer.persistence.remotewms.jaxb.RequestOptionsType; +import org.deegree.layer.persistence.remotewms.jaxb.StyleType; +import org.deegree.layer.persistence.remotewms.jaxb.LayerType.XSLTFile; import org.deegree.protocol.wms.client.WMSClient; +import org.deegree.style.se.unevaluated.Style; import org.deegree.workspace.ResourceMetadata; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Builds remote wms layers from jaxb beans. - * + * * @author Andreas Schmitz * @author last edited by: $Author: stranger $ - * * @version $Revision: $, $Date: $ */ class RemoteWmsLayerBuilder { @@ -101,8 +104,7 @@ Map buildLayerMap() { } private Map parseAllRemoteLayers() { - Map map = new LinkedHashMap(); - + Map map = new LinkedHashMap<>(); RequestOptionsType opts = cfg.getRequestOptions(); List layers = client.getLayerTree().flattenDepthFirst(); for ( LayerMetadata md : layers ) { @@ -114,7 +116,7 @@ private Map parseAllRemoteLayers() { } private Map collectConfiguredRemoteLayers( Map configured ) { - Map map = new LinkedHashMap(); + Map map = new LinkedHashMap<>(); RequestOptionsType opts = cfg.getRequestOptions(); List layers = client.getLayerTree().flattenDepthFirst(); for ( LayerMetadata md : layers ) { @@ -122,14 +124,58 @@ private Map collectConfiguredRemoteLayers( Map configuredLegendStyles = confMd.getLegendStyles(); + Map remoteServiceLegendStyles = remoteServiceMd.getLegendStyles(); + Map remoteServiceStyles = remoteServiceMd.getStyles(); + if ( !configuredLegendStyles.isEmpty() ) { + for ( String styleName : configuredLegendStyles.keySet() ) { + Style configuredLegendStyle = configuredLegendStyles.get( styleName ); + Style remoteServiceStyle = remoteServiceStyles.get( styleName ); + if ( remoteServiceStyle != null ) { + setLegendUrlAndFile( remoteServiceStyle, configuredLegendStyle ); + } + Style remoteServiceLegendStyle = remoteServiceLegendStyles.get( styleName ); + if ( remoteServiceLegendStyle != null ) { + setLegendUrlAndFile( remoteServiceLegendStyle, configuredLegendStyle ); + } + } + removeUnconfiguredStyles( configuredLegendStyles, remoteServiceLegendStyles, remoteServiceStyles ); + } + confMd.setLegendStyles( remoteServiceLegendStyles ); + confMd.setStyles( remoteServiceStyles ); + } + + private void removeUnconfiguredStyles( Map configuredLegendStyles, + Map remoteServiceLegendStyles, + Map remoteServiceStyles ) { + for ( String remoteServiceStyleName : remoteServiceStyles.keySet() ) { + if ( !"default".equalsIgnoreCase( remoteServiceStyleName ) + && !configuredLegendStyles.containsKey( remoteServiceStyleName ) ) { + remoteServiceStyles.remove( remoteServiceStyleName ); + remoteServiceLegendStyles.remove( remoteServiceStyleName ); + } + } + } + + private void setLegendUrlAndFile( Style targetStyle, Style sourceStyle ) { + targetStyle.setPrefersGetLegendGraphicUrl( sourceStyle.prefersGetLegendGraphicUrl() ); + if ( sourceStyle.getLegendURL() != null ) { + targetStyle.setLegendURL( sourceStyle.getLegendURL() ); + } + if ( sourceStyle.getLegendFile() != null ) { + targetStyle.setLegendURL( null ); + targetStyle.setLegendFile( sourceStyle.getLegendFile() ); + } + } + private Map collectConfiguredLayers() { Map configured = new HashMap(); if ( cfg.getLayer() != null ) { @@ -154,14 +200,45 @@ private Map collectConfiguredLayers() { } md.setMapOptions( ConfigUtils.parseLayerOptions( l.getLayerOptions() ) ); md.setXsltFile( parseXsltFile( md, l.getXSLTFile() ) ); + md.setLegendStyles( parseConfiguredStyles( l ) ); configured.put( l.getOriginalName(), md ); } } return configured; } + private Map parseConfiguredStyles( LayerType l ) { + return l.getStyle().stream().map( configuredStyle -> { + Style style = new Style(); + style.setName( configuredStyle.getOriginalName() ); + StyleType.LegendGraphic g = configuredStyle.getLegendGraphic(); + + URL url = null; + try { + url = new URL( g.getValue() ); + if ( url.toURI().isAbsolute() ) { + style.setLegendURL( url ); + } + style.setPrefersGetLegendGraphicUrl( g.isOutputGetLegendGraphicUrl() ); + } catch ( Exception e ) { + LOG.debug( "LegendGraphic was not an absolute URL." ); + LOG.trace( "Stack trace:", e ); + } + + if ( url == null ) { + File file = metadata.getLocation().resolveToFile( g.getValue() ); + if ( file.exists() ) { + style.setLegendFile( file ); + } else { + LOG.warn( "LegendGraphic {} could not be resolved to a legend.", g.getValue() ); + } + } + return style; + } ).collect( Collectors.toMap( Style::getName, Function.identity() ) ); + } + private XsltFile parseXsltFile( LayerMetadata md, XSLTFile xsltFileConfig ) { - if(xsltFileConfig != null){ + if ( xsltFileConfig != null ) { GMLVersion gmlVersion = GMLVersion.valueOf( xsltFileConfig.getTargetGmlVersion().value() ); String xslFile = xsltFileConfig.getValue(); URL xsltFileUrl = metadata.getLocation().resolveToUrl( xslFile ); diff --git a/deegree-layers/deegree-layers-remotewms/src/main/resources/META-INF/schemas/layers/remotewms/remotewms.xsd b/deegree-layers/deegree-layers-remotewms/src/main/resources/META-INF/schemas/layers/remotewms/remotewms.xsd index 64b05bf175..651a6d6518 100644 --- a/deegree-layers/deegree-layers-remotewms/src/main/resources/META-INF/schemas/layers/remotewms/remotewms.xsd +++ b/deegree-layers/deegree-layers-remotewms/src/main/resources/META-INF/schemas/layers/remotewms/remotewms.xsd @@ -43,6 +43,7 @@ + @@ -56,6 +57,21 @@ + + + + + + + + + + + + + + + diff --git a/deegree-services/deegree-services-commons/src/api/java/org/deegree/services/controller/DeegreeWorkspaceUpdater.java b/deegree-services/deegree-services-commons/src/api/java/org/deegree/services/controller/DeegreeWorkspaceUpdater.java index c5a732372c..93874d4448 100644 --- a/deegree-services/deegree-services-commons/src/api/java/org/deegree/services/controller/DeegreeWorkspaceUpdater.java +++ b/deegree-services/deegree-services-commons/src/api/java/org/deegree/services/controller/DeegreeWorkspaceUpdater.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.regex.Pattern; import javax.xml.namespace.QName; import javax.xml.stream.XMLInputFactory; @@ -170,9 +171,10 @@ private boolean isFileToIgnore( File file ) { final String path = file.getAbsolutePath(); if ( path.contains( "appschemas" ) ) return true; - if ( "bbox_cache.properties".equals( file.getName() ) ) + String fileName = file.getName(); + if ( Pattern.matches( "bbox_cache.*\\.properties", fileName ) ) return true; - if ( "main.xml".equals( file.getName() ) ) + if ( "main.xml".equals( fileName ) ) return true; return false; } diff --git a/deegree-services/deegree-services-config/src/main/java/org/deegree/services/config/ApiKey.java b/deegree-services/deegree-services-config/src/main/java/org/deegree/services/config/ApiKey.java new file mode 100644 index 0000000000..8ef8502e1f --- /dev/null +++ b/deegree-services/deegree-services-config/src/main/java/org/deegree/services/config/ApiKey.java @@ -0,0 +1,211 @@ +package org.deegree.services.config; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.util.Base64; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.List; +import java.util.Random; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; +import org.deegree.commons.config.DeegreeWorkspace; +import org.deegree.commons.utils.TunableParameter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handle access to an API key file containing a token + * + * @author Stephan Reichhelm + */ +public class ApiKey { + + private static final Logger LOG = LoggerFactory.getLogger( ApiKey.class ); + + private static final String API_TOKEN_FILE = "config.apikey"; + + /** + * Token to be checked + * + * Every value matches this token if the value is "*". No value matches this token if the value is null or an empty + * string. + */ + class Token { + + final boolean allowAll; + + private final String key; + + public Token( String value ) { + this.allowAll = value != null && "*".equals( value.trim() ); + this.key = value != null && value.trim().length() > 0 ? value.trim() : value; + } + + public Token() { + this.allowAll = false; + this.key = null; + } + + public boolean matches( String value ) { + if ( allowAll ) + return true; + + if ( key == null ) + return false; + + return key.matches( value != null ? value.trim() : value ); + } + + public boolean isAnyAllowed() { + return allowAll; + } + } + + private Path getPasswordFile() { + String workspace = DeegreeWorkspace.getWorkspaceRoot(); + return Paths.get( workspace, API_TOKEN_FILE ); + } + + private String generateRandomApiKey() { + try { + MessageDigest md = DigestUtils.getSha1Digest(); + // add some random data + Random rnd = new Random(); + byte[] data = new byte[128]; + rnd.nextBytes( data ); + // add random data + md.update( data ); + md.update( new Date().toString().getBytes() ); + byte[] digest = md.digest(); + + return Hex.encodeHexString( digest ); + } catch ( Exception ex ) { + LOG.warn( "Could not generate random key with SHA-1: {}", ex.getMessage() ); + LOG.trace( "Exception", ex ); + } + return null; + } + + public Token getCurrentToken() + throws SecurityException { + Path file = getPasswordFile(); + Token token = null; + final String ls = System.lineSeparator(); + final String marker = "*************************************************************" + ls; + + try { + if ( Files.isReadable( file ) ) { + List lines = Files.readAllLines( file ); + if ( lines.size() != 1 ) { + LOG.warn( "{}API Key file '{}' has an incorrect format (multiple lines). {} " + // + "The REST API will not be accessible. {}", // + ls + ls + marker + marker + marker + ls, // + file, ls, // + ls + marker + marker + marker ); + } else { + token = new Token( lines.get( 0 ) ); + } + } else if ( !Files.exists( file ) ) { + // create new one, if no file exists + String apikey = generateRandomApiKey(); + Files.write( file, Collections.singleton( apikey ) ); + token = new Token( apikey ); + LOG.warn( "{}An API Key file with an random key was generated at '{}'.{}", // + ls + ls + marker + marker + marker + ls, // + file, ls, // + ls + marker + marker + marker ); + } else { + LOG.warn( "{}API Key file '{}' is not a regular file or not readable. {} " + // + "The REST API will not be accessible.{}", // + ls + ls + marker + marker + marker + ls, // + file, ls, // + ls + marker + marker + marker ); + } + } catch ( IOException ioe ) { + LOG.warn( "{}API Key file '{}' could not be accessed. {} " + // + "The REST API will not be accessible.{}", // + ls + ls + marker + marker + marker + ls, // + file, ls, // + ls + marker + marker + marker ); + LOG.debug("API key file could not be accessed", ioe); + } + + if ( token == null ) { + token = new Token(); + } else if ( token.isAnyAllowed() ) { + if ( TunableParameter.get( "deegree.config.apikey.warn-when-disabled", true ) ) { + LOG.warn( "{}The REST API is currently configured insecure. We strongly recommend to use a key value instead at '{}'.{}", + ls + ls + marker + marker + marker + ls, // + file, // + ls + marker + marker + marker ); + } + } else { + LOG.info( "***" ); + LOG.info( "*** NOTE: The REST API is secured, so that the key set in file '{}' is required to access it." ); + LOG.info( "***" ); + } + + return token; + } + + public void validate( HttpServletRequest req ) + throws SecurityException { + String tmp, value = null; + // check for headers + if ( value == null ) { + value = req.getHeader( "X-API-Key" ); + } + if ( value == null ) { + tmp = req.getHeader( "Authorization" ); + if ( tmp != null && tmp.toLowerCase().startsWith( "bearer " ) ) { + value = tmp.substring( 7 ); + } else if ( tmp != null && tmp.toLowerCase().startsWith( "basic " ) ) { + tmp = tmp.substring( 6 ); + final byte[] decoded = Base64.getDecoder().decode( tmp ); + final String credentials = new String( decoded, StandardCharsets.UTF_8 ); + // credentials = username:password + final String[] values = credentials.split( ":", 2 ); + if ( values.length == 2 && values[1] != null ) { + value = values[1]; + } + } + } + + // check for parameter + if ( value == null ) { + Enumeration keys = req.getParameterNames(); + while ( keys.hasMoreElements() ) { + String key = (String) keys.nextElement(); + if ( "token".equalsIgnoreCase( key ) || "api_key".equalsIgnoreCase( key ) ) { + value = req.getParameter( key ); + break; + } + } + } + + // initialize early to allow creation of apikey/token + Token token = getCurrentToken(); + + if ( token.isAnyAllowed() ) { + // no API Key required + return; + } + + if ( value == null || value.trim().length() == 0 ) { + throw new SecurityException( "Please specify API Key" ); + } + + if ( !token.matches( value ) ) { + throw new SecurityException( "Invalid API Key specified" ); + } + } +} diff --git a/deegree-services/deegree-services-config/src/main/java/org/deegree/services/config/servlet/ConfigServlet.java b/deegree-services/deegree-services-config/src/main/java/org/deegree/services/config/servlet/ConfigServlet.java index cb699096cb..543d09b0c3 100644 --- a/deegree-services/deegree-services-config/src/main/java/org/deegree/services/config/servlet/ConfigServlet.java +++ b/deegree-services/deegree-services-config/src/main/java/org/deegree/services/config/servlet/ConfigServlet.java @@ -58,6 +58,7 @@ import javax.servlet.http.HttpServletResponse; import org.apache.commons.io.IOUtils; +import org.deegree.services.config.ApiKey; import org.slf4j.Logger; /** @@ -69,6 +70,8 @@ public class ConfigServlet extends HttpServlet { private static final long serialVersionUID = -4412872621677620591L; private static final Logger LOG = getLogger( ConfigServlet.class ); + + private static ApiKey token = new ApiKey(); @Override public void init() @@ -128,6 +131,8 @@ protected void doGet( HttpServletRequest req, HttpServletResponse resp ) private void dispatch( String path, HttpServletRequest req, HttpServletResponse resp ) throws IOException, ServletException { + token.validate( req ); + if ( path.toLowerCase().startsWith( "/download" ) ) { download( path.substring( 9 ), resp ); } @@ -200,5 +205,4 @@ protected void doPut( HttpServletRequest req, HttpServletResponse resp ) dispatch( path, req, resp ); } } - } diff --git a/deegree-services/deegree-services-wfs/src/main/java/org/deegree/services/wfs/GetCapabilitiesHandler.java b/deegree-services/deegree-services-wfs/src/main/java/org/deegree/services/wfs/GetCapabilitiesHandler.java index 64723ccc6f..7af970d621 100644 --- a/deegree-services/deegree-services-wfs/src/main/java/org/deegree/services/wfs/GetCapabilitiesHandler.java +++ b/deegree-services/deegree-services-wfs/src/main/java/org/deegree/services/wfs/GetCapabilitiesHandler.java @@ -270,11 +270,7 @@ void export100() if ( ftMd != null && ftMd.getTitle( null ) != null ) { writer.writeCharacters( ftMd.getTitle( null ).getString() ); } else { - if ( prefix != null ) { - writer.writeCharacters( prefix + ":" + ftName.getLocalPart() ); - } else { - writer.writeCharacters( ftName.getLocalPart() ); - } + writer.writeCharacters( ftName.getLocalPart() ); } writer.writeEndElement(); @@ -658,7 +654,7 @@ void export110() if ( ftMd != null && ftMd.getTitle( null ) != null ) { writer.writeCharacters( ftMd.getTitle( null ).getString() ); } else { - writer.writeCharacters( prefix + ":" + ftName.getLocalPart() ); + writer.writeCharacters( ftName.getLocalPart() ); } writer.writeEndElement(); @@ -999,7 +995,7 @@ void export200() if ( ftMd != null && ftMd.getTitle( null ) != null ) { writer.writeCharacters( ftMd.getTitle( null ).getString() ); } else { - writer.writeCharacters( prefix + ":" + ftName.getLocalPart() ); + writer.writeCharacters( ftName.getLocalPart() ); } writer.writeEndElement(); diff --git a/deegree-services/deegree-webservices-handbook/src/main/asciidoc/appendix.adoc b/deegree-services/deegree-webservices-handbook/src/main/asciidoc/appendix.adoc index 609a4cb9e8..16e4c5e316 100644 --- a/deegree-services/deegree-webservices-handbook/src/main/asciidoc/appendix.adoc +++ b/deegree-services/deegree-webservices-handbook/src/main/asciidoc/appendix.adoc @@ -57,4 +57,6 @@ f |deegree.gml.property.simple.trim |java.lang.Boolean |true |When deegree reads GML data, by default (`true`) simple property values get their leading and trailing whitespace characters removed. +|deegree.config.apikey.warn-when-disabled |java.lang.Boolean |true |Log warning if security on REST api is disabled by specifying `*` in _config.apikey_. + |=== \ No newline at end of file diff --git a/deegree-services/deegree-webservices-handbook/src/main/asciidoc/basics.adoc b/deegree-services/deegree-webservices-handbook/src/main/asciidoc/basics.adoc index 51b16a59d2..38ce259a6a 100644 --- a/deegree-services/deegree-webservices-handbook/src/main/asciidoc/basics.adoc +++ b/deegree-services/deegree-webservices-handbook/src/main/asciidoc/basics.adoc @@ -138,6 +138,7 @@ files exist: |console.pw |Password for services console |proxy.xml |Proxy settings |webapps.properties |Selects the active workspace +|config.apikey |Contains the key to protect the REST API |=== Note that only a single workspace can be active at a time. The @@ -156,6 +157,11 @@ every instance can use a different workspace. The file _webapps.properties_ stores the active workspace for every deegree webapp separately. +TIP: If there is no _config.apikey_ file, one will be generated on startup +with an random value. Alternatively, a value of `*` in config.apikey will +turn off security for the REST API. We strongly advise against doing this +in productive environments. + === Structure of the deegree workspace directory The workspace directory is a container for resource files with a diff --git a/deegree-services/deegree-webservices-handbook/src/main/asciidoc/featurestores.adoc b/deegree-services/deegree-webservices-handbook/src/main/asciidoc/featurestores.adoc index 4cffbf0871..9fb62c182e 100644 --- a/deegree-services/deegree-webservices-handbook/src/main/asciidoc/featurestores.adoc +++ b/deegree-services/deegree-webservices-handbook/src/main/asciidoc/featurestores.adoc @@ -2008,3 +2008,19 @@ After entering the URL, click *Import*: .Imported INSPIRE datasets via the Loader image::console_featurestore_mapping20.jpg[Imported INSPIRE datasets via the Loader,scaledwidth=50.0%] + +==== Spatial extent of FeatureTypes + +The spatial extent of all feature types defined in all SQLFeatureStore configurations are cached in a file named _bbox_cache.properties_. The file is created when the workspace is initialised. +The file contains the bounding box with its coordinate system assigned to the qualified name of each feature type, e.g.: + +---- +{http\://www.deegree.org/app}Lakes=epsg\:4326,11.16,51.29,14.83,53.59 +{http\://www.deegree.org/app}Railroads=epsg\:4326,11.16,51.29,14.83,53.59 +---- + +Inserting new features via WFS-T results in an increased bounding box in the _bbox_cache.properties_ file, if the extent did not include the features. +The file can also be used to configure the bounding box to a larger extent than the data, e.g. if the extent is already known but not all data imported. +The extent of a FeatureType is written in the capabilities as WGS84BoundingBox (WFS 2.0) of the FeatureType. + +TIP: It is possible to configure a _bbox_cache_.properties_ per SQLFeatureStore, this FeatureStore specific configuration is preferred over the bbox_cache.properties. diff --git a/deegree-services/deegree-webservices-handbook/src/main/asciidoc/gdal.adoc b/deegree-services/deegree-webservices-handbook/src/main/asciidoc/gdal.adoc index fdb1ae5fb5..fcbb2e0e75 100644 --- a/deegree-services/deegree-webservices-handbook/src/main/asciidoc/gdal.adoc +++ b/deegree-services/deegree-webservices-handbook/src/main/asciidoc/gdal.adoc @@ -23,8 +23,15 @@ to be installed correctly and must be accessible by your deegree webservices installation. Please see https://www.gdal.org/ for general GDAL installation instructions. -NOTE: Currently, GDAL library version 3.0 is supported. Other versions may -work as well, but have not been tested. +NOTE: Currently, GDAL library version 3 is supported. + +NOTE: deegree uses version 3.6.0 of the GDAL jar file. +Most likely, this is compatible with any minor version of GDAL library 3. +The deegree developer team tested several combinations without detecting any issues. +However, if any problems occur, you might try to exchange the version of the GDAL jar file inside the webapp to the GDAL library version installed on your operating system. +The gdal-VERSION.jar is located in deegree-webapp/WEB-INF/lib/. +You can delete it from the exploded war archive (make sure that the deegree-webservices.war is removed from webapp folder after shutting down Tomcat). +Afterward, copy the correct gdal-VERSION.jar into deegree-webapp/WEB-INF/lib/. In order to verify that deegree webservices can use the GDAL library, check the log file of the web container (e.g. _catalina.out_ for diff --git a/deegree-services/deegree-webservices-handbook/src/main/asciidoc/layers.adoc b/deegree-services/deegree-webservices-handbook/src/main/asciidoc/layers.adoc index 5bf9be43d3..3200548c4b 100644 --- a/deegree-services/deegree-webservices-handbook/src/main/asciidoc/layers.adoc +++ b/deegree-services/deegree-webservices-handbook/src/main/asciidoc/layers.adoc @@ -661,7 +661,7 @@ parameters fixed to the configured values. ==== Layer configuration The manual configuration allows you to pick out a layer, rename it, and -optionally override the _common description and spatial metadata. What +optionally override the common description, spatial metadata and legend graphic of the styles. What you don't override, will be copied from the source. Let's look at an example: @@ -674,6 +674,7 @@ example: basic_polygons + + new_legendGraphic.png +