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
+