diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b9c2036d589c..e4f7920f8c5d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -12,9 +12,16 @@ Ask in a comment if you have troubles with any of them. --> - [ ] *Clean commit history* broken into understandable chucks, avoiding big commits with hundreds of files, cautious of reformatting and whitespace changes - [ ] *Clean commit message*s, longer verbose messages are encouraged - [ ] *API Changes* are identified in commit messages -- [ ] *Testing* provided for features or enhancements using [automatic tests](https://github.com/geonetwork/core-geonetwork/blob/main/software_development/TESTING.md)) -- [ ] *User documentation* provided for new features or enhancements in [mannual](https://github.com/geonetwork/core-geonetwork/tree/main/docs/manual) +- [ ] *Testing* provided for features or enhancements using [automatic tests](https://github.com/geonetwork/core-geonetwork/blob/main/software_development/TESTING.md) +- [ ] *User documentation* provided for new features or enhancements in [manual](https://github.com/geonetwork/core-geonetwork/tree/main/docs/manual) - [ ] *Build documentation* provided for development instructions in `README.md` files - [ ] *Library management* using `pom.xml` dependency management. Update build documentation with intended library use and library tutorials or documentation + + + diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 000000000000..272cd05d974f --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,25 @@ +name: ♻ Backport +on: + pull_request_target: + types: + - closed + - labeled + +permissions: + contents: read + +jobs: + backport: + permissions: + contents: write + pull-requests: write + issues: write + runs-on: ubuntu-20.04 + name: Backport + steps: + - name: Backport Bot + id: backport + if: github.event.pull_request.merged && ( ( github.event.action == 'closed' && contains( join( github.event.pull_request.labels.*.name ), 'backport') ) || contains( github.event.label.name, 'backport' ) ) + uses: m-kuhn/backport@v1.2.7 + with: + github_token: ${{ secrets.GH_TOKEN_BOT }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index dbda173e220c..3815b70a9bc9 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -43,7 +43,7 @@ jobs: show-progress: 'false' - name: Setup Java JDK - uses: actions/setup-java@v3.12.0 + uses: actions/setup-java@v4.0.0 with: java-version: 11 # Java distribution. See the list of supported distributions in README file @@ -85,6 +85,3 @@ jobs: - name: Remove SNAPSHOT jars from repository run: | find ~/.m2/repository -name "*SNAPSHOT*" -type d | xargs rm -rf {} - - name: Remove Schema 3.8 jars from repository - run: | - find ~/.m2/repository -name "*3.8*" -type d | xargs rm -rf {} diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index c0ee1e8c4ecb..dd60f346486c 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -22,7 +22,7 @@ jobs: submodules: 'recursive' show-progress: 'false' - name: Set up JDK - uses: actions/setup-java@v3.12.0 + uses: actions/setup-java@v4.0.0 with: distribution: 'temurin' java-version: ${{ matrix.jdk }} @@ -40,13 +40,10 @@ jobs: maven-version: 3.6.3 - name: Build with Maven run: | - mvn -B -ntp -V install -DskipTests=true -Dmaven.javadoc.skip=true -Pwith-doc + mvn -B -ntp -V install -DskipTests=true -Dmaven.javadoc.skip=true -Drelease -Pwith-doc - name: Remove SNAPSHOT jars from repository run: | find ~/.m2/repository -name "*SNAPSHOT*" -type d | xargs rm -rf {} - - name: Remove Schema 3.8 jars from repository - run: | - find ~/.m2/repository -name "*3.8*" -type d | xargs rm -rf {} QA: runs-on: ubuntu-22.04 @@ -58,7 +55,7 @@ jobs: submodules: 'recursive' show-progress: 'false' - name: Set up JDK - uses: actions/setup-java@v3.12.0 + uses: actions/setup-java@v4.0.0 with: distribution: 'temurin' java-version: 11 @@ -70,10 +67,7 @@ jobs: - name: Test with maven run: | mvn -B resources:resources@copy-index-schema-to-source -f web - mvn -B -ntp -V -fae verify -Pit + mvn -B -ntp -V -fae verify -Drelesae -Pit - name: Remove SNAPSHOT jars from repository run: | find ~/.m2/repository -name "*SNAPSHOT*" -type d | xargs rm -rf {} - - name: Remove Schema 3.8 jars from repository - run: | - find ~/.m2/repository -name "*3.8*" -type d | xargs rm -rf {} diff --git a/.github/workflows/mvn-dep-tree.yml b/.github/workflows/mvn-dep-tree.yml index 74bd2f9cad77..1b5e7ce00880 100644 --- a/.github/workflows/mvn-dep-tree.yml +++ b/.github/workflows/mvn-dep-tree.yml @@ -20,7 +20,7 @@ jobs: show-progress: 'false' - name: Setup Java JDK - uses: actions/setup-java@v3.12.0 + uses: actions/setup-java@v4.0.0 with: java-version: 11 # Java distribution. See the list of supported distributions in README file diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 33abdc2a46ba..fb2f9cf23885 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -19,7 +19,7 @@ jobs: submodules: 'recursive' show-progress: 'false' - name: Set up JDK 11 - uses: actions/setup-java@v3.12.0 + uses: actions/setup-java@v4.0.0 with: distribution: 'temurin' java-version: '11' @@ -33,6 +33,12 @@ jobs: - name: Build GN run: mvn -B package -DskipTests + - name: Set up JDK 21 # Sonarcloud analyzer needs at least JDK 17 + uses: actions/setup-java@v4.0.0 + with: + distribution: 'temurin' + java-version: '21' + cache: 'maven' - name: Analyze with Sonar env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any diff --git a/.gitignore b/.gitignore index 60446e3e3c01..265f74f1dc07 100644 --- a/.gitignore +++ b/.gitignore @@ -80,7 +80,6 @@ web/src/main/webapp/WEB-INF/data/wro4j*.db web/src/main/webapp/WEB-INF/data/wro4j-cache* web/src/main/webapp/WEB-INF/data_* web/src/main/webapp/WEB-INF/metadata_subversion/ -web/src/main/webapp/WEB-INF/server.prop web/src/main/webapp/WEB-INF/prebuilt web/src/main/webapp/data/ web/src/main/webapp/doc/en diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ae045a912f0..1bff3df03f5d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,31 +1,31 @@ # Contributing -Thank you for contributing to GeoNetwork: +Thank you for contributing to GeoNetwork! * Free-software: GeoNetwork is free-software, using the [GNU GENERAL PUBLIC LICENSE](LICENSE.md). Contributions provided by you, or your employer, are required to be compatible with this free-software license. * Pull-request: GeoNetwork uses a pull-request workflow to review and accept changes. Pull-requests must be submitted against the *main* branch first, and may be back ported as required. # Pull requests -* Pull request is required, even if you have commit access, so the tests are run and other developer can check your code. +* Pull request is required, even if you have commit access, so the tests are run and another developer can check your code. * Pull requests must be applied to `main`, before being backported. -* Before merging a pull request, should be defined the following properties: +* Before merging a pull request, the following properties should be defined: - Milestone to include the change. - - Add the label `changelog` when the change is relevant to be added to the release changelog file . - - Add the label(s) to the backport to previous branch(es), when the change is a bug fix or if it is a small improvement that may be relevant to the backport. + - Add the label `changelog` when the change is relevant to be added to the release changelog file. + - Add `backport ` to indicate when the change is a bug fix or when it is a small improvement that is relevant to be backported. -* Good housekeeping: Anytime you commit, try and clean the code around it to latest style guide. If you improve a function without comments: add comments. If you modify functionality that does not have tests: write a test. If you fix functionality without documentation: add documentation. +* Good housekeeping: Anytime you commit, try and clean the code around it according to the latest style guide. If you improve a function without comments: _add comments!!_ If you modify functionality that does not have tests: _write a test!!_ If you fix functionality without documentation: _add documentation!!_ -* History: Clean commit messages and history: avoid big commits with hundreds of files, break commits up into understandable chunks, longer verbose commit messages are encouraged. Avoid reformatting and needless whitespace changes. +* History: Clean commit messages and history. Avoid big commits with hundreds of files, break commits up into understandable chunks. Longer, verbose commit messages are encouraged. Avoid reformatting and needless whitespace changes. -* Draft: Use pull request *Draft** (or even the text "WIP") to identify work in progress. +* Draft: Use pull request **Draft** (or even the text "WIP") to identify _Work In Progress_. -* Rebase / Squash and merge: No merge commits with current branch, use Rebase or Squash and merge! +* Rebase / Squash and merge: Do not merge commits with the current branch, use Rebase or Squash and merge! -* API Change: Please identify any API change or behavior changes in commit messages. Also make sure that a [process for deprecation](PROCESS_FOR_DEPRECATION.md) of a feature is carefully dealt with. +* API Changes: Please identify any API change or behavior changes in commit messages. Also make sure that a [process for deprecation](PROCESS_FOR_DEPRECATION.md) of a feature is carefully dealt with. * Review: Review is required by another person, or more than one! Don't be shy asking for help or reviewing. diff --git a/core/src/main/java/org/fao/geonet/api/records/attachments/AbstractStore.java b/core/src/main/java/org/fao/geonet/api/records/attachments/AbstractStore.java index dfcd55a1b511..c2c20f8c8984 100644 --- a/core/src/main/java/org/fao/geonet/api/records/attachments/AbstractStore.java +++ b/core/src/main/java/org/fao/geonet/api/records/attachments/AbstractStore.java @@ -97,7 +97,7 @@ public final ResourceHolder getResource(ServiceContext context, String metadataU } protected static AccessManager getAccessManager(final ServiceContext context) { - return context.getBean(AccessManager.class); + return ApplicationContextHolder.get().getBean(AccessManager.class); } public static int getAndCheckMetadataId(String metadataUuid, Boolean approved) throws Exception { diff --git a/core/src/main/java/org/fao/geonet/kernel/setting/Settings.java b/core/src/main/java/org/fao/geonet/kernel/setting/Settings.java index 01a878903fbe..f64e17ee3bf1 100644 --- a/core/src/main/java/org/fao/geonet/kernel/setting/Settings.java +++ b/core/src/main/java/org/fao/geonet/kernel/setting/Settings.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2021 Food and Agriculture Organization of the + * Copyright (C) 2001-2023 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -128,6 +128,7 @@ public class Settings { public static final String REGION_GETMAP_MAPPROJ = "region/getmap/mapproj"; public static final String REGION_GETMAP_WIDTH = "region/getmap/width"; public static final String REGION_GETMAP_SUMMARY_WIDTH = "region/getmap/summaryWidth"; + public static final String REGION_GETMAP_GEODESIC_EXTENTS = "region/getmap/useGeodesicExtents"; public static final String METADATA_WORKFLOW_ENABLE = "metadata/workflow/enable"; public static final String METADATA_WORKFLOW_DRAFT_WHEN_IN_GROUP = "metadata/workflow/draftWhenInGroup"; public static final String METADATA_WORKFLOW_ALLOW_SUBMIT_APPROVE_INVALID_MD = "metadata/workflow/allowSubmitApproveInvalidMd"; diff --git a/csw-server/src/main/java/org/fao/geonet/kernel/csw/services/getrecords/es/CswFilter2Es.java b/csw-server/src/main/java/org/fao/geonet/kernel/csw/services/getrecords/es/CswFilter2Es.java index ba7aff93722c..2122a8c4a10d 100644 --- a/csw-server/src/main/java/org/fao/geonet/kernel/csw/services/getrecords/es/CswFilter2Es.java +++ b/csw-server/src/main/java/org/fao/geonet/kernel/csw/services/getrecords/es/CswFilter2Es.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2016 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -26,131 +26,73 @@ import org.apache.commons.lang.NotImplementedException; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.math.NumberUtils; +import org.apache.commons.text.StringEscapeUtils; import org.fao.geonet.constants.Geonet; import org.fao.geonet.kernel.csw.services.getrecords.IFieldMapper; import org.fao.geonet.utils.Log; -import org.geotools.filter.visitor.AbstractFilterVisitor; -import org.locationtech.jts.geom.*; -import org.locationtech.jts.io.WKTReader; -import org.geotools.api.filter.And; -import org.geotools.api.filter.BinaryComparisonOperator; -import org.geotools.api.filter.BinaryLogicOperator; -import org.geotools.api.filter.ExcludeFilter; -import org.geotools.api.filter.Filter; -import org.geotools.api.filter.Id; -import org.geotools.api.filter.IncludeFilter; -import org.geotools.api.filter.Not; -import org.geotools.api.filter.Or; -import org.geotools.api.filter.PropertyIsBetween; -import org.geotools.api.filter.PropertyIsEqualTo; -import org.geotools.api.filter.PropertyIsGreaterThan; -import org.geotools.api.filter.PropertyIsGreaterThanOrEqualTo; -import org.geotools.api.filter.PropertyIsLessThan; -import org.geotools.api.filter.PropertyIsLessThanOrEqualTo; -import org.geotools.api.filter.PropertyIsLike; -import org.geotools.api.filter.PropertyIsNil; -import org.geotools.api.filter.PropertyIsNotEqualTo; -import org.geotools.api.filter.PropertyIsNull; +import org.geotools.api.filter.*; import org.geotools.api.filter.expression.Expression; import org.geotools.api.filter.expression.Literal; import org.geotools.api.filter.expression.PropertyName; -import org.geotools.api.filter.spatial.BBOX; -import org.geotools.api.filter.spatial.Beyond; -import org.geotools.api.filter.spatial.BinarySpatialOperator; -import org.geotools.api.filter.spatial.Contains; -import org.geotools.api.filter.spatial.Crosses; -import org.geotools.api.filter.spatial.DWithin; -import org.geotools.api.filter.spatial.Disjoint; -import org.geotools.api.filter.spatial.Equals; -import org.geotools.api.filter.spatial.Intersects; -import org.geotools.api.filter.spatial.Overlaps; -import org.geotools.api.filter.spatial.Touches; -import org.geotools.api.filter.spatial.Within; -import org.geotools.api.filter.temporal.After; -import org.geotools.api.filter.temporal.AnyInteracts; -import org.geotools.api.filter.temporal.Before; -import org.geotools.api.filter.temporal.Begins; -import org.geotools.api.filter.temporal.BegunBy; -import org.geotools.api.filter.temporal.During; -import org.geotools.api.filter.temporal.EndedBy; -import org.geotools.api.filter.temporal.Ends; -import org.geotools.api.filter.temporal.Meets; -import org.geotools.api.filter.temporal.MetBy; -import org.geotools.api.filter.temporal.OverlappedBy; -import org.geotools.api.filter.temporal.TContains; -import org.geotools.api.filter.temporal.TEquals; -import org.geotools.api.filter.temporal.TOverlaps; +import org.geotools.api.filter.spatial.*; +import org.geotools.api.filter.temporal.*; import org.geotools.api.geometry.BoundingBox; +import org.geotools.filter.visitor.AbstractFilterVisitor; +import org.locationtech.jts.geom.*; +import org.locationtech.jts.io.WKTReader; import java.util.*; import java.util.regex.Pattern; /** - * Manages the translation from CSW <Filter> into a ES query. + * Manages the translation from CSW <Filter> into an ES query. */ public class CswFilter2Es extends AbstractFilterVisitor { - private final String BINARY_OPERATOR_AND = "AND"; - private final String BINARY_OPERATOR_OR = "OR"; + private static final String BINARY_OPERATOR_AND = "AND"; + private static final String BINARY_OPERATOR_OR = "OR"; - static final String SPECIAL_RE = "([" + Pattern.quote("+-&|!(){}[]^\\\"~*?:/") + "])"; - static final String SPECIAL_LIKE_RE = "(? stack = new ArrayDeque(); - - private final String templateNot = " {\"bool\": {\n" + + private static final String SPECIAL_RE = "([" + Pattern.quote("+-&|!(){}[]^\\\"~*?:/") + "])"; + private static final String SPECIAL_LIKE_RE = "(? stack = new ArrayDeque<>(); + private boolean useFilter = true; public CswFilter2Es(IFieldMapper fieldMapper) { expressionVisitor = new Expression2CswVisitor(stack, fieldMapper); @@ -214,22 +159,24 @@ protected static String convertLikePattern(PropertyIsLike filter) { : filter.getSingleChar(); result = result.replaceAll(singleCharRe, "?"); } + + result = StringEscapeUtils.escapeJson(escapeLikeLiteral(result)); return result; } public String getFilter() { - String condition = stack.isEmpty()?"":stack.pop(); + String condition = stack.isEmpty() ? "" : stack.pop(); // Check for single condition (no binary operators to wrap the query if (!condition.startsWith(" \"bool\":")) { - condition = String.format(templateAndWithFilter, condition, "%s"); + condition = String.format(TEMPLATE_AND_WITH_FILTER, condition, "%s"); } if (StringUtils.isEmpty(condition)) { // No filter - condition = "{\"bool\":{\"must\":[{\"query_string\":{\"query\":\"*\"}}],\"filter\":{\"query_string\":{\"query\":\"%s\"}}}"; + condition = "{\"bool\":{\"must\":[{\"query_string\":{\"query\":\"*\"}}],\"filter\":{\"query_string\":{\"query\":\"%s\"}}}"; } else { // Add wrapper - condition = "{" + condition + "}"; + condition = "{" + condition + "}"; } outQueryString.append(condition); @@ -237,21 +184,6 @@ public String getFilter() { return outQueryString.toString(); } - @Override - public Object visitNullFilter(Object extraData) { - return super.visitNullFilter(extraData); - } - - @Override - public Object visit(ExcludeFilter filter, Object extraData) { - return super.visit(filter, extraData); - } - - @Override - public Object visit(IncludeFilter filter, Object extraData) { - return super.visit(filter, extraData); - } - @Override public Object visit(And filter, Object extraData) { return visitBinaryLogic(filter, BINARY_OPERATOR_AND, extraData); @@ -261,9 +193,9 @@ private Object visitBinaryLogic(BinaryLogicOperator filter, String operator, Obj String filterCondition; if (operator.equals(BINARY_OPERATOR_AND)) { - filterCondition = (useFilter?templateAndWithFilter:templateAnd); + filterCondition = (useFilter ? TEMPLATE_AND_WITH_FILTER : TEMPLATE_AND); } else if (operator.equals(BINARY_OPERATOR_OR)) { - filterCondition = (useFilter?templateOrWithFilter:templateOr); + filterCondition = (useFilter ? TEMPLATE_OR_WITH_FILTER : TEMPLATE_OR); } else { throw new NotImplementedException(); } @@ -288,10 +220,10 @@ private Object visitBinaryLogic(BinaryLogicOperator filter, String operator, Obj int count = StringUtils.countMatches(filterCondition, "%s"); if (count == 1) { - filterCondition = String.format(filterCondition, String.join(",", conditionList)); + filterCondition = String.format(filterCondition, String.join(",", conditionList)); } else { - filterCondition = String.format(filterCondition, String.join(",", conditionList), "%s"); + filterCondition = String.format(filterCondition, String.join(",", conditionList), "%s"); } stack.push(filterCondition); @@ -307,7 +239,7 @@ public Object visit(Id filter, Object extraData) { @Override public Object visit(Not filter, Object extraData) { - String filterNot = templateNot; + String filterNot = TEMPLATE_NOT; filter.getFilter().accept(this, extraData); @@ -324,25 +256,32 @@ public Object visit(Or filter, Object extraData) { @Override public Object visit(PropertyIsBetween filter, Object extraData) { - String filterBetween = templateBetween; + String filterBetween = TEMPLATE_BETWEEN; - assert filter.getExpression() instanceof PropertyName; - filter.getExpression().accept(expressionVisitor, extraData); + if (!(filter.getExpression() instanceof PropertyName)) { + throw new IllegalArgumentException("Invalid expression property provided"); + } - assert filter.getLowerBoundary() instanceof Literal; - filter.getLowerBoundary().accept(expressionVisitor, extraData); + if (!(filter.getLowerBoundary() instanceof Literal)) { + throw new IllegalArgumentException("Invalid expression lower boundary literal provided"); + } + + if (!(filter.getUpperBoundary() instanceof Literal)) { + throw new IllegalArgumentException("Invalid expression upper boundary literal provided"); + } - assert filter.getUpperBoundary() instanceof Literal; + filter.getExpression().accept(expressionVisitor, extraData); + filter.getLowerBoundary().accept(expressionVisitor, extraData); filter.getUpperBoundary().accept(expressionVisitor, extraData); String dataPropertyUpperValue = stack.pop(); if (!NumberUtils.isNumber(dataPropertyUpperValue)) { - dataPropertyUpperValue = CswFilter2Es.quoteString(dataPropertyUpperValue); + dataPropertyUpperValue = StringEscapeUtils.escapeJson(CswFilter2Es.quoteString(dataPropertyUpperValue)); } String dataPropertyLowerValue = stack.pop(); if (!NumberUtils.isNumber(dataPropertyLowerValue)) { - dataPropertyLowerValue = CswFilter2Es.quoteString(dataPropertyLowerValue); + dataPropertyLowerValue = StringEscapeUtils.escapeJson(CswFilter2Es.quoteString(dataPropertyLowerValue)); } String dataPropertyName = stack.pop(); @@ -355,17 +294,15 @@ public Object visit(PropertyIsBetween filter, Object extraData) { @Override public Object visit(PropertyIsEqualTo filter, Object extraData) { + checkFilterExpressionsInBinaryComparisonOperator(filter); - assert filter.getExpression1() instanceof PropertyName; filter.getExpression1().accept(expressionVisitor, extraData); - - assert filter.getExpression2() instanceof Literal; filter.getExpression2().accept(expressionVisitor, extraData); String dataPropertyValue = stack.pop(); String dataPropertyName = stack.pop(); - final String filterEqualTo = String.format(templateMatch, dataPropertyName, dataPropertyValue); + final String filterEqualTo = String.format(TEMPLATE_MATCH, dataPropertyName, StringEscapeUtils.escapeJson(escapeLiteral(dataPropertyValue))); stack.push(filterEqualTo); return this; @@ -373,37 +310,36 @@ public Object visit(PropertyIsEqualTo filter, Object extraData) { @Override public Object visit(PropertyIsNotEqualTo filter, Object extraData) { - String filterPropertyIsNot = templatePropertyIsNot; + String filterPropertyIsNot = TEMPLATE_PROPERTY_IS_NOT; - assert filter.getExpression1() instanceof PropertyName; - filter.getExpression1().accept(expressionVisitor, extraData); + checkFilterExpressionsInBinaryComparisonOperator(filter); - assert filter.getExpression2() instanceof Literal; + filter.getExpression1().accept(expressionVisitor, extraData); filter.getExpression2().accept(expressionVisitor, extraData); String dataPropertyValue = stack.pop(); String dataPropertyName = stack.pop(); - filterPropertyIsNot = String.format(filterPropertyIsNot, dataPropertyName, dataPropertyValue); + filterPropertyIsNot = String.format(filterPropertyIsNot, dataPropertyName, + StringEscapeUtils.escapeJson(escapeLiteral(dataPropertyValue))); stack.push(filterPropertyIsNot); return this; } public Object visitRange(BinaryComparisonOperator filter, String operator, Object extraData) { - String filterRange = templateRange; + String filterRange = TEMPLATE_RANGE; - assert filter.getExpression1() instanceof PropertyName; - filter.getExpression1().accept(expressionVisitor, extraData); + checkFilterExpressionsInBinaryComparisonOperator(filter); - assert filter.getExpression2() instanceof Literal; + filter.getExpression1().accept(expressionVisitor, extraData); filter.getExpression2().accept(expressionVisitor, extraData); String dataPropertyValue = stack.pop(); String dataPropertyName = stack.pop(); if (!NumberUtils.isNumber(dataPropertyValue)) { - dataPropertyValue = CswFilter2Es.quoteString(dataPropertyValue); + dataPropertyValue = StringEscapeUtils.escapeJson(CswFilter2Es.quoteString(dataPropertyValue)); } filterRange = String.format(filterRange, dataPropertyName, operator, dataPropertyValue); @@ -423,7 +359,7 @@ public Object visit(PropertyIsGreaterThanOrEqualTo filter, Object extraData) { } @Override - public Object visit(PropertyIsLessThan filter, Object extraData) { + public Object visit(PropertyIsLessThan filter, Object extraData) { return visitRange(filter, "lt", extraData); } @@ -434,7 +370,7 @@ public Object visit(PropertyIsLessThanOrEqualTo filter, Object extraData) { @Override public Object visit(PropertyIsLike filter, Object extraData) { - String filterIsLike = templateIsLike; + String filterIsLike = TEMPLATE_IS_LIKE; String expression = convertLikePattern(filter); @@ -472,7 +408,7 @@ public Object visit(PropertyIsNil filter, Object extraData) { * @return */ private String fillTemplateSpatial(String shapeType, String coords, String relation) { - return String.format(templateSpatial, shapeType, coords, relation); + return String.format(TEMPLATE_SPATIAL, shapeType, coords, relation); } @Override @@ -495,14 +431,12 @@ public Object visit(BBOX filter, Object extraData) { } private Object addGeomFilter(BinarySpatialOperator filter, String geoOperator, Object extraData) { - if (!(filter.getExpression2() == null || filter.getExpression1() == null)) { filter.getExpression1().accept(expressionVisitor, extraData); } - // out.append(":\"").append(geoOperator).append("("); final Expression geoExpression = filter.getExpression2() == null ? filter.getExpression1() - : filter.getExpression2(); + : filter.getExpression2(); geoExpression.accept(expressionVisitor, extraData); String geom = stack.pop(); @@ -542,7 +476,7 @@ private Object addGeomFilter(BinarySpatialOperator filter, String geoOperator, O stack.push(filterSpatial); } catch (Exception ex) { Log.error(Geonet.CSW, "Error parsing geospatial object", ex); - throw new RuntimeException(ex); + throw new IllegalArgumentException("Invalid expression for spatial filter", ex); } return this; @@ -674,7 +608,7 @@ public Object visit(TOverlaps contains, Object extraData) { private String buildCoordinatesString(Coordinate[] coordinates) { List coordinatesList = new ArrayList<>(); - for(Coordinate c : coordinates) { + for (Coordinate c : coordinates) { // Use Locale.US to make Java use dot "." as decimal separator String coordsValue = String.format(Locale.US, "[%f, %f] ", c.getX(), c.getY()); @@ -684,4 +618,14 @@ private String buildCoordinatesString(Coordinate[] coordinates) { return String.join(" , ", coordinatesList); } + + private void checkFilterExpressionsInBinaryComparisonOperator(BinaryComparisonOperator filter) { + if (!(filter.getExpression1() instanceof PropertyName)) { + throw new IllegalArgumentException("Invalid expression property provided"); + } + + if (!(filter.getExpression2() instanceof Literal)) { + throw new IllegalArgumentException("Invalid expression literal provided"); + } + } } diff --git a/csw-server/src/test/java/org/fao/geonet/kernel/csw/services/getrecords/es/CswFilter2EsTest.java b/csw-server/src/test/java/org/fao/geonet/kernel/csw/services/getrecords/es/CswFilter2EsTest.java index 4b8a60242582..f8a31dabbfbd 100644 --- a/csw-server/src/test/java/org/fao/geonet/kernel/csw/services/getrecords/es/CswFilter2EsTest.java +++ b/csw-server/src/test/java/org/fao/geonet/kernel/csw/services/getrecords/es/CswFilter2EsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2023 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -29,10 +29,10 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import org.fao.geonet.kernel.csw.services.getrecords.FilterParser; import org.fao.geonet.kernel.csw.services.getrecords.IFieldMapper; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.geotools.api.filter.Filter; import org.geotools.api.filter.capability.FilterCapabilities; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -141,12 +141,11 @@ void testPropertyIsEqualTo() throws IOException { // INPUT: final String input = "\n" // - + " \n" // - + " Title\n" // - + " Hydrological\n" // - + " \n" // - + " " // - + ""; + + " \n" // + + " Title\n" // + + " Hydrological\n" // + + " \n" // + + " "; // EXPECTED: final ObjectNode expected = EsJsonHelper.boolbdr(). // @@ -157,34 +156,109 @@ void testPropertyIsEqualTo() throws IOException { assertFilterEquals(expected, input); } + @Test + void testPropertyIsEqualToSpecialChars() throws IOException { + final String input = + "\n" // + + " \n" // + + " OnlineResourceType\n" // + + " OGC:WMS\n" // + + " \n" // + + " "; + + // EXPECTED: + final ObjectNode expected = EsJsonHelper.boolbdr(). // + must(array(queryStringPart("OnlineResourceType", "OGC\\:WMS"))). // + filter(queryStringPart()). // + bld(); + + assertFilterEquals(expected, input); + } + + @Test + void testPropertyIsLike() throws IOException { + + final String input = + "\n" // + + " \n" // + + " AnyText\n" // + + " s\\_rvice\\%\n" // + + " \n" // + + " "; + + // EXPECTED: + final ObjectNode expected = EsJsonHelper.boolbdr(). // + must(array(queryStringPart("AnyText", "s?rvice*"))). // + filter(queryStringPart()). // + bld(); + + assertFilterEquals(expected, input); + } + + @Test + void testPropertyIsLikeSpecialChars() throws IOException { + + final String input = + "\n" // + + " \n" // + + " AnyText\n" // + + " \"service\"\n" // + + " \n" // + + " "; + + // EXPECTED: + final ObjectNode expected = EsJsonHelper.boolbdr(). // + must(array(queryStringPart("AnyText", "\\\"service\\\""))). // + filter(queryStringPart()). // + bld(); + + assertFilterEquals(expected, input); + + + final String input2 = + "\n" // + + " \n" // + + " AnyText\n" // + + " OGC:WMS\\%\n" // + + " \n" // + + " "; + + // EXPECTED: + final ObjectNode expected2 = EsJsonHelper.boolbdr(). // + must(array(queryStringPart("AnyText", "OGC\\:WMS*"))). // + filter(queryStringPart()). // + bld(); + + assertFilterEquals(expected2, input2); + } + @Test void testLogicalAnd() throws IOException { // INPUT: final String input = " \n" // - + " \n" // - + " \n" // - + " Title\n" // - + " Hydrological\n" // - + " \n" // - + " \n" // - + " Title\n" // - + " Africa\n" // - + " \n" // - + " \n" // - + " \n" // - + ""; + + " \n" // + + " \n" // + + " Title\n" // + + " Hydrological\n" // + + " \n" // + + " \n" // + + " Title\n" // + + " Africa\n" // + + " \n" // + + " \n" // + + " \n"; // EXPECTED: final ObjectNode expected = EsJsonHelper.boolbdr(). // must( - array( - queryStringPart("Title", "Africa"), - queryStringPart("Title", "Hydrological"))) // + array( + queryStringPart("Title", "Africa"), + queryStringPart("Title", "Hydrological"))) // . // - filter(queryStringPart()). // - bld(); + filter(queryStringPart()). // + bld(); assertFilterEquals(expected, input); } @@ -195,23 +269,22 @@ void testSpatialBBox() throws IOException { // INPUT: final String input = // " \n" // - + " \n" // - + " \n" // - + " -180 -90\n" // - + " 180 90\n" // - + " \n" // - + " \n" // - + " \n" // - + ""; + + " \n" // + + " \n" // + + " -180 -90\n" // + + " 180 90\n" // + + " \n" // + + " \n" // + + " \n"; // EXPECTED: final ObjectNode expected = boolbdr(). // must(array(geoShape("geom", // - envelope(-180d, 90d, 180d, -90d), // - "intersects"))) // + envelope(-180d, 90d, 180d, -90d), // + "intersects"))) // . // - filter(queryStringPart()). // - bld(); + filter(queryStringPart()). // + bld(); assertFilterEquals(expected, input); } @@ -227,33 +300,33 @@ void testFilterWithAndOrAndSpatialBBox() throws IOException { // INPUT: final String input = // " \n" // - + " \n" // - + " \n" // - + " \n" // - + " Type\n" // - + " data\n" // - + " \n" // - + " \n" // - + " Type\n" // - + " dataset\n" // - + " \n" // - + " \n" // - + " Type\n" // - + " datasetcollection\n" // - + " \n" // - + " \n" // - + " Type\n" // - + " series\n" // - + " \n" // - + " \n" // - + " \n" // - + " \n" // - + " -180 -90\n" // - + " 180 90\n" // - + " \n" // - + " \n" // - + " \n" // - + " "; + + " \n" // + + " \n" // + + " \n" // + + " Type\n" // + + " data\n" // + + " \n" // + + " \n" // + + " Type\n" // + + " dataset\n" // + + " \n" // + + " \n" // + + " Type\n" // + + " datasetcollection\n" // + + " \n" // + + " \n" // + + " Type\n" // + + " series\n" // + + " \n" // + + " \n" // + + " \n" // + + " \n" // + + " -180 -90\n" // + + " 180 90\n" // + + " \n" // + + " \n" // + + " \n" // + + " "; final ObjectNode propertiesPart = boolbdr().should(array( // queryStringPart("Type", "series"), // @@ -270,10 +343,10 @@ void testFilterWithAndOrAndSpatialBBox() throws IOException { // EXPECTED: final ObjectNode expected = boolbdr(). // must(array(geoShapePart, // - propertiesPart)) // + propertiesPart)) // . // - filter(queryStringPart()). // - bld(); + filter(queryStringPart()). // + bld(); assertFilterEquals(expected, input); } diff --git a/datastorages/cmis/src/main/java/org/fao/geonet/api/records/attachments/CMISStore.java b/datastorages/cmis/src/main/java/org/fao/geonet/api/records/attachments/CMISStore.java index 87f74ab99820..769267a5a141 100644 --- a/datastorages/cmis/src/main/java/org/fao/geonet/api/records/attachments/CMISStore.java +++ b/datastorages/cmis/src/main/java/org/fao/geonet/api/records/attachments/CMISStore.java @@ -617,7 +617,7 @@ protected Path getBaseMetadataDir(ServiceContext context, Path metadataFullDir) } private GeonetworkDataDirectory getDataDirectory(ServiceContext context) { - return context.getBean(GeonetworkDataDirectory.class); + return ApplicationContextHolder.get().getBean(GeonetworkDataDirectory.class); } /** @@ -690,7 +690,7 @@ protected MetadataResourceExternalManagementProperties getMetadataResourceExtern if (metadataResourceExternalManagementPropertiesUrl.contains("{lang}") || metadataResourceExternalManagementPropertiesUrl.contains("{ISO3lang}")) { final IsoLanguagesMapper mapper = ApplicationContextHolder.get().getBean(IsoLanguagesMapper.class); - String contextLang = context.getLanguage() == null ? Geonet.DEFAULT_LANGUAGE : context.getLanguage(); + String contextLang = context==null || context.getLanguage() == null ? Geonet.DEFAULT_LANGUAGE : context.getLanguage(); String lang; String iso3Lang; diff --git a/datastorages/jcloud/src/main/java/org/fao/geonet/api/records/attachments/JCloudStore.java b/datastorages/jcloud/src/main/java/org/fao/geonet/api/records/attachments/JCloudStore.java index d7ed09b1837d..c4f11d4a3656 100644 --- a/datastorages/jcloud/src/main/java/org/fao/geonet/api/records/attachments/JCloudStore.java +++ b/datastorages/jcloud/src/main/java/org/fao/geonet/api/records/attachments/JCloudStore.java @@ -30,6 +30,7 @@ import jeeves.server.context.ServiceContext; import org.apache.commons.lang.StringUtils; +import org.fao.geonet.ApplicationContextHolder; import org.fao.geonet.api.exception.ResourceNotFoundException; import org.fao.geonet.constants.Geonet; import org.fao.geonet.domain.MetadataResource; @@ -381,7 +382,7 @@ private Path getBaseMetadataDir(ServiceContext context, Path metadataFullDir) { } private GeonetworkDataDirectory getDataDirectory(ServiceContext context) { - return context.getBean(GeonetworkDataDirectory.class); + return ApplicationContextHolder.get().getBean(GeonetworkDataDirectory.class); } /** diff --git a/docs/manual/docs/index.fr.md b/docs/manual/docs/index.fr.md index a491841fe52b..13e3b55195bd 100644 --- a/docs/manual/docs/index.fr.md +++ b/docs/manual/docs/index.fr.md @@ -9,36 +9,52 @@ Bienvenue à GeoNetwork. Cette documentation est organisée en guides spécifiqu
-:fontawesome-solid-signs-post: [Vue d'ensemble](overview/index.md) - -: Historique de GeoNetwork, communauté, détails de la licence et derniers changements. - -:fontawesome-solid-circle-info: [Aide en ligne](help/index.md) - -: Aide en ligne pour les visiteurs du catalogue (aucune connexion n'est requise). - -:fontawesome-solid-person-circle-question: [Guide de l'utilisateur](user-guide/index.md) - -: Guide de l'utilisateur opérationnel décrivant l'édition, la révision et la gestion des enregistrements (nécessite une connexion). - -:fontawesome-solid-screwdriver-wrench: [Guide du mainteneur](maintainer-guide/index.md) - -: Instructions d'installation, de configuration et de mise à jour - -:fontawesome-solid-user-graduate: [Tutoriels](tutorials/index.md) - -: Explorer des sujets à l'aide de tutoriels étape par étape - -:fontawesome-solid-plug: [Référence API](api/index.md) - -: Référence API pour les développeurs accédant aux services du catalogue. - -:fontawesome-regular-file-code: [Développement](devel/index.md) - -: Informations sur le développement, la personnalisation de GeoNetwork et la participation au projet GeoNetwork. - -:fontawesome-regular-bookmark: [Annexes](annexes/index.md) - -: Informations de référence +- :fontawesome-solid-signs-post: [Vue d'ensemble](overview/index.md) + + --- + + Historique de GeoNetwork, communauté, détails de la licence et derniers changements. + +- :fontawesome-solid-circle-info: [Aide en ligne](help/index.md) + + --- + + Aide en ligne pour les visiteurs du catalogue (aucune connexion n'est requise). + +- :fontawesome-solid-person-circle-question: [Guide de l'utilisateur](user-guide/index.md) + + --- + + Guide de l'utilisateur opérationnel décrivant l'édition, la révision et la gestion des enregistrements (nécessite une connexion). + +- :fontawesome-solid-screwdriver-wrench: [Guide du mainteneur](maintainer-guide/index.md) + + --- + + Instructions d'installation, de configuration et de mise à jour + +- :fontawesome-solid-user-graduate: [Tutoriels](tutorials/index.md) + + --- + + Explorer des sujets à l'aide de tutoriels étape par étape + +- :fontawesome-solid-plug: [Référence API](api/index.md) + + --- + + Référence API pour les développeurs accédant aux services du catalogue. + +- :fontawesome-regular-file-code: [Développement](devel/index.md) + + --- + + Informations sur le développement, la personnalisation de GeoNetwork et la participation au projet GeoNetwork. + +- :fontawesome-regular-bookmark: [Annexes](annexes/index.md) + + --- + + Informations de référence
diff --git a/docs/manual/docs/index.md b/docs/manual/docs/index.md index 5780bdfdb5e8..d8e34f4b6b09 100644 --- a/docs/manual/docs/index.md +++ b/docs/manual/docs/index.md @@ -9,36 +9,52 @@ Welcome to GeoNetwork. This documentation is organized into specific guides targ
-:fontawesome-solid-signs-post: [Overview](overview/index.md) - -: GeoNetwork background, community, license details, and the latest changes. - -:fontawesome-solid-circle-info: [Online Help](help/index.md) - -: Online help for visitors to the catalogue (no login required). - -:fontawesome-solid-person-circle-question: [User Guide](user-guide/index.md) - -: Operational user-guide describing the editing, review and management of records (requires-login). - -:fontawesome-solid-screwdriver-wrench: [Maintainer Guide](maintainer-guide/index.md) - -: Installation, setup and update instructions - -:fontawesome-solid-user-graduate: [Tutorials](tutorials/index.md) - -: Explore topics using step-by-step tutorials - -:fontawesome-solid-plug: [API Reference](api/index.md) - -: API Reference for developers accecssing catalogue services. - -:fontawesome-regular-file-code: [Development](devel/index.md) - -: Development information on customizing GeoNetwork and taking part in the GeoNetwork project. - -:fontawesome-regular-bookmark: [Annexes](annexes/index.md) - -: Reference information +- :fontawesome-solid-signs-post: [Overview](overview/index.md) + + --- + + GeoNetwork background, community, license details, and the latest changes. + +- :fontawesome-solid-circle-info: [Online Help](help/index.md) + + --- + + Online help for visitors to the catalogue (no login required). + +- :fontawesome-solid-person-circle-question: [User Guide](user-guide/index.md) + + --- + + Operational user-guide describing the editing, review and management of records (requires-login). + +- :fontawesome-solid-screwdriver-wrench: [Maintainer Guide](maintainer-guide/index.md) + + --- + + Installation, setup and update instructions + + - :fontawesome-solid-user-graduate: [Tutorials](tutorials/index.md) + + --- + + Explore topics using step-by-step tutorials + +- :fontawesome-solid-plug: [API Reference](api/index.md) + + --- + + API Reference for developers accecssing catalogue services. + +- :fontawesome-regular-file-code: [Development](devel/index.md) + + --- + + Development information on customizing GeoNetwork and taking part in the GeoNetwork project. + +- :fontawesome-regular-bookmark: [Annexes](annexes/index.md) + + --- + + Reference information
diff --git a/docs/manual/overrides/assets/stylesheets/extra.css b/docs/manual/overrides/assets/stylesheets/extra.css index 8155e94365e0..07b39e34520e 100644 --- a/docs/manual/overrides/assets/stylesheets/extra.css +++ b/docs/manual/overrides/assets/stylesheets/extra.css @@ -9,48 +9,6 @@ img + em, .browser-border + em, .browser-mockup + em { font-size: 0.75rem; } -/* grid */ -.md-typeset .grid { - column-count: 2; - column-gap: 2em; - margin-bottom: 20px; -} -.md-typeset .grid dl { - display: grid; - grid-template-columns: repeat(auto-fit,minmax(16rem,1fr)); - margin: 0; -} -.md-typeset .grid.cards dt, .md-typeset .grid.cards dd { - border: 0.05rem solid var(--md-default-fg-color--lightest); - border-radius: 0.1rem; - display: block; - margin: 0; - padding: 0.8rem; - transition: border .25s,box-shadow .25s; -} -.md-typeset .grid.cards dt { - font-weight: bold; -} -.md-typeset .grid.cards dt .twemoji { - margin-right: 5px; -} -.md-typeset .grid.cards dd { - margin-bottom: 0.8rem; - margin-top: -1px; -} -.md-typeset .grid.cards dd p { - margin: 0; -} -@media (max-width: 768px) { - .md-typeset .grid dl { - display: inline-block; - margin-bottom: 20px; - } - .md-typeset .grid.cards dt, .md-typeset .grid.cards dd { - width: calc(100vw - 1.2rem - 1.2rem); - } -} - /* definition list used to display general inputs */ .md-typeset dl dd { margin: 10px 0; diff --git a/docs/manual/pom.xml b/docs/manual/pom.xml index 5c5ba26f7344..84e3b980a7d2 100644 --- a/docs/manual/pom.xml +++ b/docs/manual/pom.xml @@ -104,13 +104,6 @@ - - - maven-install-plugin - - true - - maven-deploy-plugin diff --git a/docs/manual/requirements.txt b/docs/manual/requirements.txt index a3fae0623c69..8a3fcb93da67 100644 --- a/docs/manual/requirements.txt +++ b/docs/manual/requirements.txt @@ -1,4 +1,4 @@ -mkdocs-material +mkdocs-material>=9.5.3 mkdocs-static-i18n>=1.0.5 mkdocs-include-markdown-plugin mkdocs-exclude diff --git a/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomSearch.java b/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomSearch.java index cd1e247b53c9..5cdad3f20266 100644 --- a/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomSearch.java +++ b/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomSearch.java @@ -56,10 +56,13 @@ import org.jdom.Content; import org.jdom.Element; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; + +import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.stream.Collectors; @@ -103,14 +106,17 @@ public class AtomSearch { description = "") @GetMapping( value = "/feeds", - produces = MediaType.APPLICATION_XML_VALUE + produces = { + MediaType.APPLICATION_XML_VALUE, + MediaType.TEXT_HTML_VALUE + } ) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Get a list of feeds."), @ApiResponse(responseCode = "204", description = "Not authenticated.") }) @ResponseStatus(OK) - public Element feeds( + public Object feeds( @Parameter( description = "fileIdentifier", required = false) @@ -118,6 +124,20 @@ public Element feeds( String fileIdentifier, @Parameter(hidden = true) HttpServletRequest request) throws Exception { + + String acceptHeader = StringUtils.isBlank(request.getHeader(HttpHeaders.ACCEPT))?MediaType.APPLICATION_XML_VALUE:request.getHeader(HttpHeaders.ACCEPT); + List accept = Arrays.asList(acceptHeader.split(",")); + + if (accept.contains(MediaType.TEXT_HTML_VALUE)) { + return feedsAsHtml(fileIdentifier, request); + } else{ + return feedsAsXml(fileIdentifier, request); + } + } + private Element feedsAsXml( + String fileIdentifier, + HttpServletRequest request) throws Exception { + ServiceContext context = ApiUtils.createServiceContext(request); boolean inspireEnable = sm.getValueAsBool(Settings.SYSTEM_INSPIRE_ENABLE); @@ -196,27 +216,10 @@ public Element feeds( } - @io.swagger.v3.oas.annotations.Operation( - summary = "Get ATOM feeds", - description = "") - @GetMapping( - value = "/feeds", - produces = MediaType.TEXT_HTML_VALUE - ) - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Get a list of feeds."), - @ApiResponse(responseCode = "204", description = "Not authenticated.") - }) - @ResponseStatus(OK) - public String feedsAsHtml( - @Parameter( - description = "fileIdentifier", - required = false) - @RequestParam(defaultValue = "") + private String feedsAsHtml( String fileIdentifier, - @Parameter(hidden = true) HttpServletRequest request) throws Exception { - Element feeds = feeds(fileIdentifier, request); + Element feeds = feedsAsXml(fileIdentifier, request); Locale locale = languageUtils.parseAcceptLanguage(request.getLocales()); String language = IsoLanguagesMapper.iso639_2T_to_iso639_2B(locale.getISO3Language()); diff --git a/pom.xml b/pom.xml index 5bec5a0e06a5..0d2ee1c5ac56 100644 --- a/pom.xml +++ b/pom.xml @@ -133,6 +133,14 @@ maven-clean-plugin 3.1.0 + + maven-install-plugin + 3.1.1 + + + maven-deploy-plugin + 3.1.1 + net.alchim31.maven yuicompressor-maven-plugin @@ -1516,6 +1524,12 @@ * allow only host registered in metadata link table --> DB_LINK_CHECK + + ^(localhost|127\..*|0\..*|255\.255\.255\.255|.*\.local|.*\.localhost|0:0:0:0:0:0:1|::1)$ + + + + 8080 diff --git a/release/README.md b/release/README.md index 899ac2f32516..656b84986a10 100644 --- a/release/README.md +++ b/release/README.md @@ -14,19 +14,33 @@ cd release mvn clean install -Drelease ``` -## Manual +This module is designed to be used as part of a full build. It copies files from web/target so gn-web-app must be built first. + +## Manual release Open a terminal window and execute the following steps from within the ``release`` folder. -* Once GeoNetwork has been built (run Maven in the repository root), download Jetty: +1. Once GeoNetwork has been built (run Maven in the repository root), download Jetty: + + ```bash + mvn clean install -Pjetty-download + ``` + + This will download the version of jetty indicated in dependency management, and rename to ``jetty`` folder + (adjusting ``jetty-deploy.xml`` configuration to use `web` rather than default ``webapps``). + +2. Next, create the ZIP distributions and copy the WAR: - ` - mvn clean install -Pjetty-download - ` + ``` + ant + ``` + + The build.xml file will check everything is available and assemble into a zip. -* Next, create the ZIP distributions and copy the WAR: +## Jetty download - ` - ant - ` +To clean up the ``jetty`` download, when switching between branches: +```bash +mvn clean:clean@reset +``` diff --git a/release/build.xml b/release/build.xml index bc2aa3a8743a..1b89d8c48e51 100644 --- a/release/build.xml +++ b/release/build.xml @@ -106,7 +106,7 @@ - + @@ -118,6 +118,7 @@ + diff --git a/release/pom.xml b/release/pom.xml index 2b6937d65a46..431521647d7f 100644 --- a/release/pom.xml +++ b/release/pom.xml @@ -12,8 +12,8 @@ gn-release pom - Release module - Use to create distribution packages. + GeoNetwork Release module + Use to create zip distribution packages (copies from files from web which must be built first). @@ -23,6 +23,35 @@ + + + ${project.groupId} + gn-web-app + ${project.version} + war + + + + + + maven-clean-plugin + + + reset + clean + + + + jetty + + + + + + + + + @@ -40,8 +69,9 @@ download-maven-plugin - download-jetty + jetty-download pre-integration-test + wget @@ -57,7 +87,9 @@ maven-antrun-plugin - install + jetty-rename + pre-integration-test + run @@ -91,6 +123,7 @@ post-integration-test + run diff --git a/schemas/iso19139/src/main/plugin/iso19139/index-fields/index.xsl b/schemas/iso19139/src/main/plugin/iso19139/index-fields/index.xsl index ab8df8449fa5..82b2fa89ece0 100644 --- a/schemas/iso19139/src/main/plugin/iso19139/index-fields/index.xsl +++ b/schemas/iso19139/src/main/plugin/iso19139/index-fields/index.xsl @@ -977,7 +977,7 @@ + select="(../../gmd:dateTime/gco:DateTime)[1]"/> { "name": "", diff --git a/schemas/iso19139/src/main/plugin/iso19139/loc/ger/codelists.xml b/schemas/iso19139/src/main/plugin/iso19139/loc/ger/codelists.xml index a715df4a283f..fdf6efea6602 100644 --- a/schemas/iso19139/src/main/plugin/iso19139/loc/ger/codelists.xml +++ b/schemas/iso19139/src/main/plugin/iso19139/loc/ger/codelists.xml @@ -1578,26 +1578,25 @@ Publication --> - map - + staticMap - + interactiveMap - + featureCatalog - + diff --git a/schemas/iso19139/src/main/plugin/iso19139/schematron/schematron-rules-iso.sch b/schemas/iso19139/src/main/plugin/iso19139/schematron/schematron-rules-iso.sch index 87bb01c4cb9f..fd1cb718fae1 100755 --- a/schemas/iso19139/src/main/plugin/iso19139/schematron/schematron-rules-iso.sch +++ b/schemas/iso19139/src/main/plugin/iso19139/schematron/schematron-rules-iso.sch @@ -64,6 +64,7 @@ USA. + @@ -348,21 +349,19 @@ USA. $loc/strings/report.M23 - - - + + $loc/strings/report.M23-dup - diff --git a/services/src/main/java/org/fao/geonet/api/ApiParams.java b/services/src/main/java/org/fao/geonet/api/ApiParams.java index d3c5825c26ca..55720ac75797 100644 --- a/services/src/main/java/org/fao/geonet/api/ApiParams.java +++ b/services/src/main/java/org/fao/geonet/api/ApiParams.java @@ -33,6 +33,10 @@ public class ApiParams { public static final String API_CLASS_CATALOG_TAG = "site"; public static final String API_CLASS_REGISTRIES_OPS = "Registries related operations"; public static final String API_CLASS_REGISTRIES_TAG = "registries"; + public static final String API_CLASS_TOOLS_TAG = "tools"; + public static final String API_CLASS_TOOLS_OPS = "Utility operations"; + public static final String API_CLASS_FORMATTERS_TAG = "formatters"; + public static final String API_CLASS_FORMATTERS_OPS = "Formatter operations"; public static final String API_PARAM_RECORD_UUID = "Record UUID."; diff --git a/services/src/main/java/org/fao/geonet/api/OpenApiConfig.java b/services/src/main/java/org/fao/geonet/api/OpenApiConfig.java index b2ab44c29144..e6633562b99a 100644 --- a/services/src/main/java/org/fao/geonet/api/OpenApiConfig.java +++ b/services/src/main/java/org/fao/geonet/api/OpenApiConfig.java @@ -1,6 +1,6 @@ /* * ============================================================================= - * === Copyright (C) 2001-2016 Food and Agriculture Organization of the + * === Copyright (C) 2001-2023 Food and Agriculture Organization of the * === United Nations (FAO-UN), United Nations World Food Programme (WFP) * === and United Nations Environment Programme (UNEP) * === @@ -36,7 +36,6 @@ import io.swagger.v3.oas.models.servers.ServerVariables; import org.fao.geonet.NodeInfo; import org.fao.geonet.kernel.setting.SettingManager; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.concurrent.ConcurrentMapCacheManager; @@ -51,7 +50,11 @@ @Configuration @EnableCaching @OpenAPIDefinition -public class OpenApiConfig { +public class OpenApiConfig { + + private static OpenAPI openAPI = null; + + private static SettingManager settingManager = null; @Bean(name = "cacheManager") public CacheManager cacheManager() { @@ -59,11 +62,77 @@ public CacheManager cacheManager() { } @Bean - @Autowired - public OpenAPI OpenApiConfig(SettingManager settingManager) { - List servers = new ArrayList<>(); + public OpenAPI openApi(final SettingManager settingManager) { + return OpenApiConfig.setupOpenApiConfig(settingManager); + } + + /** + * Setup OpenAPI configuration. + * + * Using static function so that should the bean be called twice (which it does), it will reuse the same static objects. + * During first call, the settingManager may not be properly setup (i.e. initial install) and will return null values for version and host information. + * Subsequent calls will update the related OpenAPI information with the settingManager values. + * + * It is also static so that when we update the object on second call, it will also update the object returned from the first call as they will be + * pointing to the same object. + * + * @param settingManager containing host and version information required. + * @return OpenAPI object. + */ + private static OpenAPI setupOpenApiConfig(final SettingManager settingManager) { + OpenApiConfig.settingManager = settingManager; + if (openAPI == null) { + openAPI = new OpenAPI().info(new Info() + .description("This is the description of the GeoNetwork OpenAPI. Use this API to manage your catalog.") + .contact(new Contact() + .email("geonetwork-users@lists.sourceforge.net") + .name("GeoNetwork user mailing list") + .url("https://sourceforge.net/p/geonetwork/mailman/geonetwork-users/") + ) + .license(new License() + .name("GPL 2.0") + .url("https://www.gnu.org/licenses/old-licenses/gpl-2.0.html"))) + .externalDocs(new ExternalDocumentation() + .description("Learn how to access the catalog using the GeoNetwork REST API.")); + + setVersionRelatedInfo(); + setHostRelatedInfo(); + + } else if (openAPI.getInfo() != null && openAPI.getInfo().getVersion() == null) { + // During initial install, the version will not be set when using the JeevesApplicationContext + // But it will be set afterward when creating WebApplicationContext. So if the version is null but our new version is not null + // then lets update data based on the version. + setVersionRelatedInfo(); + // If the version was not set then the hostUrl was also not set correctly so update that as well. + setHostRelatedInfo(); + } + + return openAPI; + } + + /** + * Update openAPI object with version related information. + */ + private static void setVersionRelatedInfo() { String version = settingManager.getValue(SYSTEM_PLATFORM_VERSION); + + openAPI.getInfo().setVersion(version); + openAPI.getInfo().setTitle(String.format( + "GeoNetwork %s OpenAPI Documentation", + version)); + } + + /** + * Update openAPI object with host related information. + */ + public static void setHostRelatedInfo() { + if (settingManager == null || openAPI == null) { + return; + } + + List servers = new ArrayList<>(); + String hostUrl = settingManager.getBaseURL().replaceAll("/+$", ""); ServerVariable catalogVariable = new ServerVariable() @@ -85,24 +154,7 @@ public OpenAPI OpenApiConfig(SettingManager settingManager) { .addServerVariable("portal", portalVariable) ) ); - - return new OpenAPI().info(new Info() - .title(String.format( - "GeoNetwork %s OpenAPI Documentation", - version)) - .description("This is the description of the GeoNetwork OpenAPI. Use this API to manage your catalog.") - .contact(new Contact() - .email("geonetwork-users@lists.sourceforge.net") - .name("GeoNetwork user mailing list") - .url("https://sourceforge.net/p/geonetwork/mailman/geonetwork-users/") - ) - .version(version) - .license(new License() - .name("GPL 2.0") - .url("http://www.gnu.org/licenses/old-licenses/gpl-2.0.html"))) - .externalDocs(new ExternalDocumentation() - .description("Learn how to access the catalog using the GeoNetwork REST API.") - .url(String.format("%s/doc/api", hostUrl))) - .servers(servers); + openAPI.setServers(servers); + openAPI.getExternalDocs().setUrl(String.format("%s/doc/api", hostUrl)); } } diff --git a/services/src/main/java/org/fao/geonet/api/OpenApiController.java b/services/src/main/java/org/fao/geonet/api/OpenApiController.java index b2a93bbf3674..cc585db0b654 100644 --- a/services/src/main/java/org/fao/geonet/api/OpenApiController.java +++ b/services/src/main/java/org/fao/geonet/api/OpenApiController.java @@ -21,9 +21,7 @@ package org.fao.geonet.api; import com.fasterxml.jackson.core.JsonProcessingException; -import io.swagger.v3.core.util.Json; import io.swagger.v3.core.util.PathUtils; -import io.swagger.v3.core.util.Yaml; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.models.OpenAPI; import org.springdoc.api.AbstractOpenApiResource; @@ -33,12 +31,9 @@ import org.springdoc.webmvc.core.RouterFunctionProvider; import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.ModelAndView; @@ -47,6 +42,7 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; import javax.servlet.http.HttpServletRequest; + import java.util.*; import static org.springdoc.core.Constants.*; @@ -64,6 +60,8 @@ * springdoc.api-docs.enabled=true * springdoc.api-docs.path=/api/doc * springdoc.cache.disabled=true + * springdoc.writer-with-order-by-keys=true + * springdoc.writer-with-default-pretty-printer=true */ @RestController public class OpenApiController extends AbstractOpenApiResource { @@ -95,6 +93,14 @@ public OpenApiController(ObjectFactory openAPIBuilderObjectFacto "org.fao.geonet.api", "org.fao.geonet.services.inspireatom", "org.fao.geonet.monitor.service"})); + + // Ensure open api document is consistently orders to make it easier to compare changes later. + springDocConfigProperties.setWriterWithOrderByKeys(true); + springDocConfigProperties.setWriterWithDefaultPrettyPrinter(true); + + // remove default response + springDocConfigProperties.setOverrideWithGenericResponse(false); + this.requestMappingHandlerMapping = requestMappingHandlerMapping; this.servletContextProvider = servletContextProvider; this.springSecurityOAuth2Provider = springSecurityOAuth2Provider; @@ -103,20 +109,20 @@ public OpenApiController(ObjectFactory openAPIBuilderObjectFacto @Operation(hidden = true) @GetMapping(value = "/{portal}/api/doc", produces = MediaType.APPLICATION_JSON_VALUE) - public String openapiJson(HttpServletRequest request, @Value(API_DOCS_URL) String apiDocsUrl) + public String openapiJson(HttpServletRequest request) throws JsonProcessingException { - calculateServerUrl(request, apiDocsUrl); + setServerBaseUrl(request); OpenAPI openAPI = this.getOpenApi(request.getLocale()); - return Json.mapper().writeValueAsString(openAPI); + return writeJsonValue(openAPI); } @Operation(hidden = true) @GetMapping(value = "/{portal}/api/doc.yml", produces = APPLICATION_OPENAPI_YAML) - public String openapiYaml(HttpServletRequest request, @Value(DEFAULT_API_DOCS_URL_YAML) String apiDocsUrl) + public String openapiYaml(HttpServletRequest request) throws JsonProcessingException { - calculateServerUrl(request, apiDocsUrl); + setServerBaseUrl(request); OpenAPI openAPI = this.getOpenApi(request.getLocale()); - return Yaml.mapper().writeValueAsString(openAPI); + return writeYamlValue(openAPI); } @Override @@ -180,9 +186,14 @@ protected boolean isRestController(Map restControllers, || !ModelAndView.class.isAssignableFrom(handlerMethod.getMethod().getReturnType())); } - protected void calculateServerUrl(HttpServletRequest request, String apiDocsUrl) { - String requestUrl = decode(request.getRequestURL().toString()); - String calculatedUrl = requestUrl.substring(0, requestUrl.length() - apiDocsUrl.length()); - this.openAPIService.setServerBaseUrl(calculatedUrl); + private String getServerBaseUrl(HttpServletRequest request) { + String contextPath = request.getContextPath(); + StringBuffer requestURL = request.getRequestURL(); + String serverBaseUrl = requestURL.substring(0, requestURL.indexOf(contextPath) + contextPath.length()); + return serverBaseUrl; + } + + protected void setServerBaseUrl(HttpServletRequest request) { + this.openAPIService.setServerBaseUrl(getServerBaseUrl(request)); } } diff --git a/services/src/main/java/org/fao/geonet/api/es/EsHTTPProxy.java b/services/src/main/java/org/fao/geonet/api/es/EsHTTPProxy.java index 7766e2053c50..c3f46034ae7e 100644 --- a/services/src/main/java/org/fao/geonet/api/es/EsHTTPProxy.java +++ b/services/src/main/java/org/fao/geonet/api/es/EsHTTPProxy.java @@ -37,7 +37,11 @@ import com.jayway.jsonpath.JsonPath; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jeeves.server.UserSession; import jeeves.server.context.ServiceContext; @@ -65,8 +69,8 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; @@ -273,9 +277,13 @@ private static boolean hasOperation(ObjectNode doc, ReservedGroup group, Reserve summary = "Search endpoint", description = "See https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html for search parameters details.") @RequestMapping(value = "/search/records/_search", - method = { - RequestMethod.POST - }) + method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Search results.", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(type = "string"))) + }) @ResponseStatus(value = HttpStatus.OK) @ResponseBody public void search( @@ -292,12 +300,14 @@ public void search( HttpServletRequest request, @Parameter(hidden = true) HttpServletResponse response, - @RequestBody(description = "JSON request based on Elasticsearch API.") - String body, - @Parameter(hidden = true) - HttpEntity httpEntity) throws Exception { + @RequestBody + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "JSON request based on Elasticsearch API.", + content = @Content(examples = { + @ExampleObject(value = "{\"query\":{\"match\":{\"_id\":\"catalogue_uuid\"}}}") + })) + String body) throws Exception { ServiceContext context = ApiUtils.createServiceContext(request); - call(context, httpSession, request, response, SEARCH_ENDPOINT, httpEntity.getBody(), bucket, relatedTypes); + call(context, httpSession, request, response, SEARCH_ENDPOINT, body, bucket, relatedTypes); } @@ -305,9 +315,13 @@ public void search( summary = "Search endpoint", description = "See https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html for search parameters details.") @RequestMapping(value = "/search/records/_msearch", - method = { - RequestMethod.POST - }) + method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Search results.", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(type = "string"))) + }) @ResponseStatus(value = HttpStatus.OK) @ResponseBody public void msearch( @@ -324,12 +338,14 @@ public void msearch( HttpServletRequest request, @Parameter(hidden = true) HttpServletResponse response, - @RequestBody(description = "JSON request based on Elasticsearch API.") - String body, - @Parameter(hidden = true) - HttpEntity httpEntity) throws Exception { + @RequestBody + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "JSON request based on Elasticsearch API.", + content = @Content(examples = { + @ExampleObject(value = "{\"query\":{\"match\":{\"_id\":\"catalogue_uuid\"}}}") + })) + String body) throws Exception { ServiceContext context = ApiUtils.createServiceContext(request); - call(context, httpSession, request, response, MULTISEARCH_ENDPOINT, httpEntity.getBody(), bucket, relatedTypes); + call(context, httpSession, request, response, MULTISEARCH_ENDPOINT, body, bucket, relatedTypes); } @@ -342,7 +358,13 @@ public void msearch( @RequestMapping(value = "/search/records/{endPoint}", method = { RequestMethod.POST, RequestMethod.GET - }) + }, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Search results.", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(type = "string"))) + }) @ResponseStatus(value = HttpStatus.OK) @PreAuthorize("hasAuthority('Administrator')") @ResponseBody @@ -357,16 +379,18 @@ public void call( HttpServletRequest request, @Parameter(hidden = true) HttpServletResponse response, - @RequestBody(description = "JSON request based on Elasticsearch API.") - String body, - @Parameter(hidden = true) - HttpEntity httpEntity) throws Exception { + @RequestBody + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "JSON request based on Elasticsearch API.", + content = @Content(examples = { + @ExampleObject(value = "{\"query\":{\"match\":{\"_id\":\"catalogue_uuid\"}}}") + })) + String body) throws Exception { ServiceContext context = ApiUtils.createServiceContext(request); - call(context, httpSession, request, response, endPoint, httpEntity.getBody(), bucket, null); + call(context, httpSession, request, response, endPoint, body, bucket, null); } - public void call(ServiceContext context, HttpSession httpSession, HttpServletRequest request, + private void call(ServiceContext context, HttpSession httpSession, HttpServletRequest request, HttpServletResponse response, String endPoint, String body, String selectionBucket, diff --git a/services/src/main/java/org/fao/geonet/api/groups/GroupsApi.java b/services/src/main/java/org/fao/geonet/api/groups/GroupsApi.java index 138208a134ff..0b0fb4980d2a 100644 --- a/services/src/main/java/org/fao/geonet/api/groups/GroupsApi.java +++ b/services/src/main/java/org/fao/geonet/api/groups/GroupsApi.java @@ -296,7 +296,7 @@ public List getGroups( produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.PUT ) - @ResponseStatus(value = HttpStatus.OK) + @ResponseStatus(value = HttpStatus.CREATED) @PreAuthorize("hasAuthority('UserAdmin')") @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Group created."), @@ -357,6 +357,7 @@ public ResponseEntity addGroup( method = RequestMethod.GET) @ResponseStatus(value = HttpStatus.OK) @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Group information for the group id supplied."), @ApiResponse(responseCode = "404", description = ApiParams.API_RESPONSE_RESOURCE_NOT_FOUND) }) @ResponseBody diff --git a/services/src/main/java/org/fao/geonet/api/links/LinksApi.java b/services/src/main/java/org/fao/geonet/api/links/LinksApi.java index aac2aa1342dc..46f174699778 100644 --- a/services/src/main/java/org/fao/geonet/api/links/LinksApi.java +++ b/services/src/main/java/org/fao/geonet/api/links/LinksApi.java @@ -25,6 +25,8 @@ import com.google.common.collect.Lists; import com.google.common.collect.Sets; +import com.google.gson.Gson; + import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; @@ -52,7 +54,6 @@ import org.fao.geonet.repository.specification.LinkSpecs; import org.jdom.JDOMException; import org.json.JSONException; -import org.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.data.domain.Page; @@ -64,6 +65,7 @@ import org.springframework.jmx.export.naming.SelfNaming; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Service; +import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @@ -72,6 +74,8 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; + +import java.beans.PropertyEditorSupport; import java.io.IOException; import java.sql.SQLException; import java.util.ArrayDeque; @@ -146,9 +150,9 @@ public void iniMBeansSlidingWindowWithEmptySlot() { @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("isAuthenticated()") public Page getRecordLinks( - @Parameter(description = "Filter, e.g. \"{url: 'png', lastState: 'ko', records: 'e421', groupId: 12}\", lastState being 'ok'/'ko'/'unknown'", required = false) + @Parameter(description = "Filter, e.g. \"{url: 'png', lastState: 'ko', records: 'e421'}\", lastState being 'ok'/'ko'/'unknown'", required = false) @RequestParam(required = false) - JSONObject filter, + LinkFilter filter, @Parameter(description = "Optional, filter links to records published in that group.", required = false) @RequestParam(required = false) Integer[] groupIdFilter, @@ -187,9 +191,9 @@ public Page getRecordLinks( produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("isAuthenticated()") public Page getRecordLinksPost( - @Parameter(description = "Filter, e.g. \"{url: 'png', lastState: 'ko', records: 'e421', groupId: 12}\", lastState being 'ok'/'ko'/'unknown'", required = false) + @Parameter(description = "Filter, e.g. \"{url: 'png', lastState: 'ko', records: 'e421'}\", lastState being 'ok'/'ko'/'unknown'", required = false) @RequestParam(required = false) - JSONObject filter, + LinkFilter filter, @Parameter(description = "Optional, filter links to records published in that group.", required = false) @RequestParam(required = false) Integer[] groupIdFilter, @@ -207,7 +211,7 @@ public Page getRecordLinksPost( return getLinks(filter, groupIdFilter, groupOwnerIdFilter, pageRequest, userSession); } private Page getLinks( - JSONObject filter, + LinkFilter filter, Integer[] groupIdFilter, Integer[] groupOwnerIdFilter, Pageable pageRequest, @@ -228,22 +232,22 @@ private Page getLinks( Integer stateToMatch = null; String url = null; List associatedRecords = null; - if (filter.has("lastState")) { + if (filter.getLastState() != null) { stateToMatch = 0; - if (filter.getString("lastState").equalsIgnoreCase("ok")) { + if (filter.getLastState().equalsIgnoreCase("ok")) { stateToMatch = 1; - } else if (filter.getString("lastState").equalsIgnoreCase("ko")) { + } else if (filter.getLastState().equalsIgnoreCase("ko")) { stateToMatch = -1; } } - if (filter.has("url")) { - url = filter.getString("url"); + if (filter.getUrl() != null) { + url = filter.getUrl(); } - if (filter.has("records")) { + if (filter.getRecords() != null) { associatedRecords = Arrays.stream( - filter.getString("records").split(" ") + filter.getRecords().split(" ") ).collect(Collectors.toList()); } @@ -277,9 +281,9 @@ private Page getLinks( @PreAuthorize("isAuthenticated()") @ResponseBody public void getRecordLinksAsCsv( - @Parameter(description = "Filter, e.g. \"{url: 'png', lastState: 'ko', records: 'e421', groupId: 12}\", lastState being 'ok'/'ko'/'unknown'", required = false) + @Parameter(description = "Filter, e.g. \"{url: 'png', lastState: 'ko', records: 'e421'}\", lastState being 'ok'/'ko'/'unknown'", required = false) @RequestParam(required = false) - JSONObject filter, + LinkFilter filter, @Parameter(description = "Optional, filter links to records published in that group.", required = false) @RequestParam(required = false) Integer[] groupIdFilter, @@ -445,4 +449,50 @@ private MAnalyseProcess getRegistredMAnalyseProcess() { mAnalyseProcesses.addFirst(mAnalyseProcess); return mAnalyseProcess; } + + @InitBinder + public void initBinder(WebDataBinder dataBinder) { + dataBinder.registerCustomEditor(LinkFilter.class, new PropertyEditorSupport() { + Object value; + @Override + public Object getValue() { + return value; + } + + @Override + public void setAsText(String text) throws IllegalArgumentException { + value = new Gson().fromJson(text, LinkFilter.class); + } + }); + } + + private static class LinkFilter { + private String url; + private String lastState; + private String records; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getLastState() { + return lastState; + } + + public void setLastState(String lastState) { + this.lastState = lastState; + } + + public String getRecords() { + return records; + } + + public void setRecords(String records) { + this.records = records; + } + } } diff --git a/services/src/main/java/org/fao/geonet/api/mapservers/MapServersApi.java b/services/src/main/java/org/fao/geonet/api/mapservers/MapServersApi.java index f09a537da729..f6a862622475 100644 --- a/services/src/main/java/org/fao/geonet/api/mapservers/MapServersApi.java +++ b/services/src/main/java/org/fao/geonet/api/mapservers/MapServersApi.java @@ -109,6 +109,7 @@ public class MapServersApi { @ResponseStatus(HttpStatus.OK) @PreAuthorize("hasAuthority('Editor')") @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "List of all mapservers."), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_EDITOR) }) List getMapservers() throws Exception { @@ -257,7 +258,7 @@ public void updateMapserver( @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_REVIEWER) }) @ResponseStatus(HttpStatus.NO_CONTENT) - public void updateMapserver( + public void updateMapserverAuth( @Parameter( description = API_PARAM_MAPSERVER_IDENTIFIER, required = true, diff --git a/services/src/main/java/org/fao/geonet/api/records/CatalogApi.java b/services/src/main/java/org/fao/geonet/api/records/CatalogApi.java index 77f6b2833ea8..48f27ababf55 100644 --- a/services/src/main/java/org/fao/geonet/api/records/CatalogApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/CatalogApi.java @@ -175,7 +175,7 @@ private static String paramsAsString(Map requestParams) { summary = "Get a set of metadata records as ZIP", description = "Metadata Exchange Format (MEF) is returned. MEF is a ZIP file containing " + "the metadata as XML and some others files depending on the version requested. " + - "See http://geonetwork-opensource.org/manuals/trunk/eng/users/annexes/mef-format.html.") + "See https://docs.geonetwork-opensource.org/latest/annexes/mef-format/.") @GetMapping(value = "/zip", consumes = { MediaType.ALL_VALUE diff --git a/services/src/main/java/org/fao/geonet/api/records/DoiApi.java b/services/src/main/java/org/fao/geonet/api/records/DoiApi.java index 69fa10aa7ada..ce59aa1d8e43 100644 --- a/services/src/main/java/org/fao/geonet/api/records/DoiApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/DoiApi.java @@ -48,6 +48,7 @@ import javax.servlet.http.HttpSession; import java.util.Map; +import static org.fao.geonet.api.ApiParams.API_CLASS_RECORD_OPS; import static org.fao.geonet.api.ApiParams.API_CLASS_RECORD_TAG; import static org.fao.geonet.api.ApiParams.API_PARAM_RECORD_UUID; @@ -57,7 +58,8 @@ @RequestMapping(value = { "/{portal}/api/records" }) -@Tag(name = API_CLASS_RECORD_TAG) +@Tag(name = API_CLASS_RECORD_TAG, + description = API_CLASS_RECORD_OPS) @Controller("doi") @PreAuthorize("hasAuthority('Editor')") @ReadWriteController diff --git a/services/src/main/java/org/fao/geonet/api/records/InspireValidationApi.java b/services/src/main/java/org/fao/geonet/api/records/InspireValidationApi.java index 9e3a015780a0..86fda4b1f6c9 100644 --- a/services/src/main/java/org/fao/geonet/api/records/InspireValidationApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/InspireValidationApi.java @@ -82,6 +82,7 @@ import java.util.HashMap; import java.util.Map; +import static org.fao.geonet.api.ApiParams.API_CLASS_RECORD_OPS; import static org.fao.geonet.api.ApiParams.API_CLASS_RECORD_TAG; import static org.fao.geonet.api.ApiParams.API_PARAM_RECORD_UUID; @@ -89,7 +90,8 @@ @RequestMapping(value = { "/{portal}/api/records" }) -@Tag(name = API_CLASS_RECORD_TAG) +@Tag(name = API_CLASS_RECORD_TAG, + description = API_CLASS_RECORD_OPS) @Controller("inspire") @PreAuthorize("hasAuthority('Editor')") @ReadWriteController diff --git a/services/src/main/java/org/fao/geonet/api/records/MetadataApi.java b/services/src/main/java/org/fao/geonet/api/records/MetadataApi.java index 2323f1439ab8..c81667b89bce 100644 --- a/services/src/main/java/org/fao/geonet/api/records/MetadataApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/MetadataApi.java @@ -106,7 +106,7 @@ public class MetadataApi { private ApplicationContext context; - public static RelatedResponse getAssociatedResources( + public static RelatedResponse getRelatedResources( String language, ServiceContext context, AbstractMetadata md, RelatedItemType[] type, int start, int rows) throws Exception { // TODO PERF: ByPass XSL processing and create response directly @@ -367,7 +367,7 @@ Object getRecordAs( summary = "Get a metadata record as ZIP", description = "Metadata Exchange Format (MEF) is returned. MEF is a ZIP file containing " + "the metadata as XML and some others files depending on the version requested. " + - "See http://geonetwork-opensource.org/manuals/trunk/eng/users/annexes/mef-format.html.") + "See https://docs.geonetwork-opensource.org/latest/annexes/mef-format/.") @RequestMapping(value = "/{metadataUuid}/formatters/zip", method = RequestMethod.GET, consumes = { @@ -471,7 +471,7 @@ void getRecordAsZip( if (withRelated) { // Adding children in MEF file - RelatedResponse related = getAssociatedResources( + RelatedResponse related = getRelatedResources( metadataUuid, null, approved, 0, 100, request); uuidsToExport.addAll(getUuidsOfAssociatedRecords(related.getParent())); uuidsToExport.addAll(getUuidsOfAssociatedRecords(related.getChildren())); @@ -521,7 +521,7 @@ private List getUuidsOfAssociatedRecords(IListOnlyClassToArray associate @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_VIEW), @ApiResponse(responseCode = "404", description = ApiParams.API_RESPONSE_RESOURCE_NOT_FOUND) }) - @ResponseStatus(HttpStatus.CREATED) + @ResponseStatus(HttpStatus.OK) public ResponseEntity getRecordPopularity( @Parameter(description = API_PARAM_RECORD_UUID, required = true) @@ -596,7 +596,7 @@ public ResponseEntity increaseRecordPopularity( summary = "Get record related resources", description = "Retrieve related services, datasets, onlines, thumbnails, sources, ... " + "to this records.
" + - "More info") + "More info") @RequestMapping(value = "/{metadataUuid:.+}/related", method = RequestMethod.GET, produces = { @@ -609,7 +609,7 @@ public ResponseEntity increaseRecordPopularity( @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_VIEW) }) @ResponseBody - public RelatedResponse getAssociatedResources( + public RelatedResponse getRelatedResources( @Parameter( description = API_PARAM_RECORD_UUID, required = true) @@ -644,7 +644,7 @@ public RelatedResponse getAssociatedResources( String language = languageUtils.getIso3langCode(request.getLocales()); final ServiceContext serviceContext = ApiUtils.createServiceContext(request); - return getAssociatedResources(language, serviceContext, md, type, start, rows); + return getRelatedResources(language, serviceContext, md, type, start, rows); } @io.swagger.v3.oas.annotations.Operation( @@ -680,7 +680,7 @@ public FeatureResponse getFeatureCatalog( Map decodeMap = new HashMap<>(); try { - RelatedResponse related = getAssociatedResources( + RelatedResponse related = getRelatedResources( metadataUuid, type, approved, 0, 100, request); if (isIncludedAttributeTable(related.getFcats())) { diff --git a/services/src/main/java/org/fao/geonet/api/records/MetadataAssociatedApi.java b/services/src/main/java/org/fao/geonet/api/records/MetadataAssociatedApi.java index 5859385b99c8..5a62a945f15d 100644 --- a/services/src/main/java/org/fao/geonet/api/records/MetadataAssociatedApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/MetadataAssociatedApi.java @@ -93,7 +93,7 @@ public synchronized void setApplicationContext(ApplicationContext context) { summary = "Get record associated resources", description = "Retrieve related services, datasets, sources, ... " + "to this records.
" + - "More info") + "More info") @RequestMapping(value = "/{metadataUuid:.+}/associated", method = RequestMethod.GET, produces = { diff --git a/services/src/main/java/org/fao/geonet/api/records/MetadataIndexApi.java b/services/src/main/java/org/fao/geonet/api/records/MetadataIndexApi.java index 57303c9be001..b26cfc4bd433 100644 --- a/services/src/main/java/org/fao/geonet/api/records/MetadataIndexApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/MetadataIndexApi.java @@ -29,13 +29,10 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jeeves.server.UserSession; -import jeeves.server.context.ServiceContext; import jeeves.services.ReadWriteController; -import net.sf.json.JSONObject; import org.fao.geonet.api.ApiParams; import org.fao.geonet.api.ApiUtils; import org.fao.geonet.kernel.DataManager; -import org.fao.geonet.kernel.SelectionManager; import org.fao.geonet.kernel.datamanager.IMetadataUtils; import org.fao.geonet.kernel.search.index.BatchOpsMetadataReindexer; import org.fao.geonet.kernel.setting.SettingManager; @@ -46,7 +43,6 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; -import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.util.Set; @@ -86,7 +82,7 @@ public class MetadataIndexApi { }) public @ResponseBody - JSONObject index( + IndexResponse index( @Parameter(description = API_PARAM_RECORD_UUIDS_OR_SELECTION, required = false, example = "") @@ -125,11 +121,30 @@ JSONObject index( new BatchOpsMetadataReindexer(dataManager, ids) .process(settingManager.getSiteId(), false); - JSONObject res = new JSONObject(); - res.put("success", true); - res.put("count", index); - - return res; + IndexResponse indexResponse = new IndexResponse(); + indexResponse.setSuccess(true); + indexResponse.setCount(index); + return indexResponse; } + private static class IndexResponse { + private boolean success; + private int count; + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + } } diff --git a/services/src/main/java/org/fao/geonet/api/records/MetadataInsertDeleteApi.java b/services/src/main/java/org/fao/geonet/api/records/MetadataInsertDeleteApi.java index 1c50daf45e39..0dfb298d2c3c 100644 --- a/services/src/main/java/org/fao/geonet/api/records/MetadataInsertDeleteApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/MetadataInsertDeleteApi.java @@ -253,7 +253,7 @@ public void deleteRecord( MediaType.APPLICATION_JSON_VALUE } ) - @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Report about deleted records."), + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Report about deleted records."), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_EDITOR)}) @PreAuthorize("hasAuthority('Editor')") @ResponseStatus(HttpStatus.OK) @@ -315,9 +315,9 @@ public SimpleMetadataProcessingReport deleteRecords( + "URL or file in a folder on the catalog server. When loading" + "from the catalog server folder, it might be faster to use a " + "local filesystem harvester.") - @RequestMapping(method = {RequestMethod.PUT}, produces = {MediaType.APPLICATION_JSON_VALUE}, consumes = { - MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE, - MediaType.APPLICATION_FORM_URLENCODED_VALUE}) + @RequestMapping(method = RequestMethod.PUT, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_XML_VALUE) @ApiResponses(value = {@ApiResponse(responseCode = "201", description = API_PARAM_REPORT_ABOUT_IMPORTED_RECORDS), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_EDITOR)}) @PreAuthorize("hasAuthority('Editor')") diff --git a/services/src/main/java/org/fao/geonet/api/records/MetadataProcessApi.java b/services/src/main/java/org/fao/geonet/api/records/MetadataProcessApi.java index f40d2e293b08..2392c7fc2aeb 100644 --- a/services/src/main/java/org/fao/geonet/api/records/MetadataProcessApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/MetadataProcessApi.java @@ -84,7 +84,7 @@ public class MetadataProcessApi { SettingManager sm; @io.swagger.v3.oas.annotations.Operation(summary = "Get suggestions", description ="Analyze the record an suggest processes to improve the quality of the record.
" - + "More info") + + "More info") @RequestMapping(value = "/{metadataUuid}/processes", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('Editor')") @ResponseStatus(HttpStatus.OK) @@ -162,7 +162,7 @@ ResponseEntity processRecordPreview( RequestMethod.POST,}, produces = MediaType.APPLICATION_XML_VALUE) @PreAuthorize("hasAuthority('Editor')") @ResponseStatus(HttpStatus.OK) - @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Record processed and saved."), + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Record processed and saved."), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_EDIT)}) public @ResponseBody ResponseEntity processRecord( diff --git a/services/src/main/java/org/fao/geonet/api/records/MetadataSharingApi.java b/services/src/main/java/org/fao/geonet/api/records/MetadataSharingApi.java index 70f0872e85d9..a417e5ec0ea6 100644 --- a/services/src/main/java/org/fao/geonet/api/records/MetadataSharingApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/MetadataSharingApi.java @@ -280,7 +280,7 @@ public void unpublish( "Clear first allows to unset all operations first before setting the new ones." + "Clear option does not remove reserved groups operation if user is not an " + "administrator, a reviewer or the owner of the record.
" + - "More info") + "More info") @RequestMapping( value = "/{metadataUuid}/sharing", method = RequestMethod.PUT @@ -357,7 +357,7 @@ public void share( @ResponseStatus(HttpStatus.CREATED) public @ResponseBody - MetadataProcessingReport publish( + MetadataProcessingReport publishMultipleRecords( @Parameter(description = ApiParams.API_PARAM_RECORD_UUIDS_OR_SELECTION, required = false) @RequestParam(required = false) String[] uuids, @@ -393,7 +393,7 @@ MetadataProcessingReport publish( @ResponseStatus(HttpStatus.CREATED) public @ResponseBody - MetadataProcessingReport unpublish( + MetadataProcessingReport unpublishMultipleRecords( @Parameter(description = ApiParams.API_PARAM_RECORD_UUIDS_OR_SELECTION, required = false) @RequestParam(required = false) String[] uuids, @@ -429,7 +429,7 @@ MetadataProcessingReport unpublish( @ResponseStatus(HttpStatus.CREATED) public @ResponseBody - MetadataProcessingReport share( + MetadataProcessingReport shareMultipleRecords( @Parameter(description = ApiParams.API_PARAM_RECORD_UUIDS_OR_SELECTION, required = false) @RequestParam(required = false) String[] uuids, diff --git a/services/src/main/java/org/fao/geonet/api/records/MetadataTagApi.java b/services/src/main/java/org/fao/geonet/api/records/MetadataTagApi.java index 11a2cd603d45..0e326143429d 100644 --- a/services/src/main/java/org/fao/geonet/api/records/MetadataTagApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/MetadataTagApi.java @@ -95,7 +95,7 @@ public class MetadataTagApi { @io.swagger.v3.oas.annotations.Operation( summary = "Get record tags", description = "Tags are used to classify information.
" + - "More info") + "More info") @GetMapping( value = "/{metadataUuid}/tags", produces = { @@ -389,9 +389,9 @@ public MetadataProcessingReport tagRecords( produces = { MediaType.APPLICATION_JSON_VALUE }) - @ResponseStatus(value = HttpStatus.NO_CONTENT) + @ResponseStatus(value = HttpStatus.OK) @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Report about removed records."), + @ApiResponse(responseCode = "200", description = "Report about removed records."), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_EDITOR) }) @PreAuthorize("hasAuthority('Editor')") diff --git a/services/src/main/java/org/fao/geonet/api/records/attachments/AttachmentsActionsApi.java b/services/src/main/java/org/fao/geonet/api/records/attachments/AttachmentsActionsApi.java index 3c2bef091c98..be90091b3184 100644 --- a/services/src/main/java/org/fao/geonet/api/records/attachments/AttachmentsActionsApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/attachments/AttachmentsActionsApi.java @@ -50,13 +50,15 @@ import javax.servlet.http.HttpServletRequest; import java.nio.file.Path; +import static org.fao.geonet.api.ApiParams.API_CLASS_RECORD_OPS; +import static org.fao.geonet.api.ApiParams.API_CLASS_RECORD_TAG; import static org.fao.geonet.api.ApiParams.API_PARAM_RECORD_UUID; @EnableWebMvc @Controller @Service -@Tag(name = "records", - description = "Metadata record operations") +@Tag(name = API_CLASS_RECORD_TAG, + description = API_CLASS_RECORD_OPS) public class AttachmentsActionsApi { private final ApplicationContext appContext = ApplicationContextHolder.get(); @Autowired @@ -91,7 +93,7 @@ public void init() { @io.swagger.v3.oas.annotations.Operation( summary = "Create an overview using the map print module", - description = "More info" + description = "More info" //response = MetadataResource.class) ) @RequestMapping( diff --git a/services/src/main/java/org/fao/geonet/api/records/attachments/AttachmentsApi.java b/services/src/main/java/org/fao/geonet/api/records/attachments/AttachmentsApi.java index 01830efc5f66..43f2dcdbf205 100644 --- a/services/src/main/java/org/fao/geonet/api/records/attachments/AttachmentsApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/attachments/AttachmentsApi.java @@ -25,6 +25,9 @@ package org.fao.geonet.api.records.attachments; +import static org.fao.geonet.api.ApiParams.API_CLASS_RECORD_OPS; +import static org.fao.geonet.api.ApiParams.API_CLASS_RECORD_TAG; + import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -80,7 +83,8 @@ @EnableWebMvc @Service @RequestMapping(value = {"/{portal}/api/records/{metadataUuid}/attachments"}) -@Tag(name = "records", description = "Metadata record operations") +@Tag(name = API_CLASS_RECORD_TAG, + description = API_CLASS_RECORD_OPS) public class AttachmentsApi { public static final Integer MIN_IMAGE_SIZE = 1; public static final Integer MAX_IMAGE_SIZE = 2048; @@ -149,7 +153,7 @@ public List getResources() { return null; } - @io.swagger.v3.oas.annotations.Operation(summary = "List all metadata attachments", description = "More info") + @io.swagger.v3.oas.annotations.Operation(summary = "List all metadata attachments", description = "More info") @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(value = HttpStatus.OK) @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Return the record attachments."), @@ -246,7 +250,7 @@ public MetadataResource putResourceFromURL( // @PreAuthorize("permitAll") @RequestMapping(value = "/{resourceId:.+}", method = RequestMethod.GET) @ResponseStatus(value = HttpStatus.OK) - @ApiResponses(value = {@ApiResponse(responseCode = "201", description = "Record attachment."), + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Record attachment."), @ApiResponse(responseCode = "403", description = "Operation not allowed. " + "User needs to be able to download the resource.")}) public void getResource( diff --git a/services/src/main/java/org/fao/geonet/api/records/editing/MetadataEditingApi.java b/services/src/main/java/org/fao/geonet/api/records/editing/MetadataEditingApi.java index e38140c08095..d77eea18e68a 100644 --- a/services/src/main/java/org/fao/geonet/api/records/editing/MetadataEditingApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/editing/MetadataEditingApi.java @@ -596,7 +596,7 @@ public void addElement(@Parameter(description = API_PARAM_RECORD_UUID, required @ApiResponses(value = {@ApiResponse(responseCode = "201", description = "Element reordered."), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_EDIT)}) @ResponseBody - public void addElement(@Parameter(description = API_PARAM_RECORD_UUID, required = true) @PathVariable String metadataUuid, + public void reorderElement(@Parameter(description = API_PARAM_RECORD_UUID, required = true) @PathVariable String metadataUuid, @Parameter(description = "Reference of the element to move.", required = true) @RequestParam String ref, @Parameter(description = "Direction", required = true) @PathVariable Direction direction, @Parameter(description = "Should attributes be shown on the editor snippet?", required = false) @RequestParam(defaultValue = "false") boolean displayAttributes, diff --git a/services/src/main/java/org/fao/geonet/api/records/extent/MapRenderer.java b/services/src/main/java/org/fao/geonet/api/records/extent/MapRenderer.java index 5fe0922a251b..21673b2aea0e 100644 --- a/services/src/main/java/org/fao/geonet/api/records/extent/MapRenderer.java +++ b/services/src/main/java/org/fao/geonet/api/records/extent/MapRenderer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2022 Food and Agriculture Organization of the + * Copyright (C) 2001-2023 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -24,7 +24,6 @@ package org.fao.geonet.api.records.extent; import jeeves.server.context.ServiceContext; -import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.fao.geonet.api.regions.GeomFormat; import org.fao.geonet.exceptions.BadParameterEx; @@ -33,15 +32,12 @@ import org.fao.geonet.kernel.region.RegionsDAO; import org.fao.geonet.kernel.setting.SettingManager; import org.fao.geonet.kernel.setting.Settings; -import org.fao.geonet.lib.Lib; import org.geotools.geometry.jts.JTS; -import org.geotools.geometry.jts.JTSFactoryFinder; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.referencing.CRS; import org.locationtech.jts.awt.ShapeWriter; import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.GeometryFactory; import org.geotools.api.metadata.extent.Extent; import org.geotools.api.metadata.extent.GeographicBoundingBox; import org.geotools.api.metadata.extent.GeographicExtent; @@ -49,15 +45,9 @@ import org.geotools.api.referencing.operation.MathTransform; import org.springframework.context.ApplicationContext; -import javax.imageio.ImageIO; import java.awt.*; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.net.URLConnection; import java.util.Collection; import java.util.Map; import java.util.SortedSet; @@ -84,35 +74,48 @@ public static AffineTransform worldToScreenTransform(Envelope mapExtent, Dimensi return new AffineTransform(scaleX, 0.0d, 0.0d, -scaleY, tx, ty); } + /** + * Returns a bounding box geometry. + * + * @param geom Bounding box geometry. + * @param srs Bounding box geometry srs. + * @param useGeodesicExtents false: returns the bounding box geometry as a rectangle (using min / max bounds). + * true: returns the bounding box geometry. + * @return bounding box geometry. + */ + public static Geometry getGeometryExtent(Geometry geom, String srs, boolean useGeodesicExtents) { + boolean isGlobalSrs = srs.equals("EPSG:4326") || srs.equals("EPSG:3857"); + + return (!isGlobalSrs && !useGeodesicExtents ? geom.getEnvelope() : geom); + } + /** * Check if a geometry is in the domain of validity of a projection and if not return the * intersection of the geometry with the coordinate system domain of validity. */ public static Geometry computeGeomInDomainOfValidity(Geometry geom, CoordinateReferenceSystem mapCRS) { - final GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory(null); final Extent domainOfValidity = mapCRS.getDomainOfValidity(); Geometry adjustedGeom = geom; if (domainOfValidity != null) { for (final GeographicExtent extent : domainOfValidity.getGeographicElements()) { - if (Boolean.FALSE.equals(extent.getInclusion())) { + if ((extent == null) || (Boolean.FALSE.equals(extent.getInclusion()))) { continue; } + if (extent instanceof GeographicBoundingBox) { - if (extent != null) { - GeographicBoundingBox box = (GeographicBoundingBox) extent; - - Envelope env = new Envelope( - box.getWestBoundLongitude(), - box.getEastBoundLongitude(), - box.getSouthBoundLatitude(), - box.getNorthBoundLatitude()); - if (env.contains(geom.getEnvelopeInternal())) { - return geom; - } else { - Geometry extentPolygon = JTS.toGeometry(env); - adjustedGeom = geom.intersection(extentPolygon); - } + GeographicBoundingBox box = (GeographicBoundingBox) extent; + + Envelope env = new Envelope( + box.getWestBoundLongitude(), + box.getEastBoundLongitude(), + box.getSouthBoundLatitude(), + box.getNorthBoundLatitude()); + if (env.contains(geom.getEnvelopeInternal())) { + return geom; + } else { + Geometry extentPolygon = JTS.toGeometry(env); + adjustedGeom = geom.intersection(extentPolygon); } } } @@ -123,12 +126,15 @@ public static Geometry computeGeomInDomainOfValidity(Geometry geom, CoordinateRe } public BufferedImage render(String id, String srs, Integer width, Integer height, - String background, String geomParam, String geomType, String geomSrs, String fillColor, String strokeColor) throws Exception { + String background, String geomParam, String geomType, + String geomSrs, String fillColor, String strokeColor) throws Exception { ApplicationContext appContext = context.getApplicationContext(); Map regionGetMapBackgroundLayers = appContext.getBean("regionGetMapBackgroundLayers", Map.class); SortedSet regionGetMapExpandFactors = appContext.getBean("regionGetMapExpandFactors", SortedSet.class); SettingManager settingManager = appContext.getBean(SettingManager.class); + boolean useGeodesicExtents = settingManager.getValueAsBool(Settings.REGION_GETMAP_GEODESIC_EXTENTS, false); + Geometry geom = null; if (id != null) { Collection daos = context.getApplicationContext().getBeansOfType(RegionsDAO.class).values(); @@ -192,7 +198,6 @@ public BufferedImage render(String id, String srs, Integer width, Integer height ; BufferedImage baseMapImage = baseMapRenderer.render(); - // ImageIO.write(baseMapImage, "png", new File("delme.png")); image = baseMapImage; } else { @@ -209,8 +214,9 @@ public BufferedImage render(String id, String srs, Integer width, Integer height Color geomStrokeColor = getColor(strokeColor, new Color(0, 0, 0, 255)); AffineTransform worldToScreenTransform = worldToScreenTransform(bboxOfImage, imageDimensions); for (int i = 0; i < geom.getNumGeometries(); i++) { + Geometry geomExtent = MapRenderer.getGeometryExtent(geom.getGeometryN(i), srs, useGeodesicExtents); // draw each included geometry separately to ensure they are filled correctly - Shape shape = worldToScreenTransform.createTransformedShape(shapeWriter.toShape(geom.getGeometryN(i))); + Shape shape = worldToScreenTransform.createTransformedShape(shapeWriter.toShape(geomExtent)); graphics.setColor(geomFillColor); graphics.fill(shape); diff --git a/services/src/main/java/org/fao/geonet/api/records/formatters/CacheApi.java b/services/src/main/java/org/fao/geonet/api/records/formatters/CacheApi.java index fa0ce65a3945..d90ea166d703 100644 --- a/services/src/main/java/org/fao/geonet/api/records/formatters/CacheApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/formatters/CacheApi.java @@ -23,6 +23,9 @@ package org.fao.geonet.api.records.formatters; +import static org.fao.geonet.api.ApiParams.API_CLASS_FORMATTERS_OPS; +import static org.fao.geonet.api.ApiParams.API_CLASS_FORMATTERS_TAG; + import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -40,8 +43,8 @@ @RequestMapping(value = { "/{portal}/api/formatters" }) -@Tag(name = "formatters", - description = "Formatter operations") +@Tag(name = API_CLASS_FORMATTERS_TAG, + description = API_CLASS_FORMATTERS_OPS) @Controller("formatters") @ReadWriteController public class CacheApi { diff --git a/services/src/main/java/org/fao/geonet/api/records/formatters/FormatterAdminApi.java b/services/src/main/java/org/fao/geonet/api/records/formatters/FormatterAdminApi.java index 2541576729a2..db2b6fb6eac0 100644 --- a/services/src/main/java/org/fao/geonet/api/records/formatters/FormatterAdminApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/formatters/FormatterAdminApi.java @@ -73,6 +73,8 @@ import java.util.List; import java.util.Set; +import static org.fao.geonet.api.ApiParams.API_CLASS_FORMATTERS_OPS; +import static org.fao.geonet.api.ApiParams.API_CLASS_FORMATTERS_TAG; import static org.fao.geonet.api.records.formatters.FormatterConstants.SCHEMA_PLUGIN_FORMATTER_DIR; import static org.fao.geonet.api.records.formatters.FormatterConstants.VIEW_XSL_FILENAME; import static org.springframework.web.bind.annotation.RequestMethod.GET; @@ -82,8 +84,8 @@ * * @author jeichar */ -@Tag(name = "formatters", - description = "Formatter admin operations") +@Tag(name = API_CLASS_FORMATTERS_TAG, + description = API_CLASS_FORMATTERS_OPS) @Controller("formattersList") public class FormatterAdminApi extends AbstractFormatService { diff --git a/services/src/main/java/org/fao/geonet/api/records/formatters/ImageReplacedElementFactory.java b/services/src/main/java/org/fao/geonet/api/records/formatters/ImageReplacedElementFactory.java index b078c626117a..d16513f6c922 100644 --- a/services/src/main/java/org/fao/geonet/api/records/formatters/ImageReplacedElementFactory.java +++ b/services/src/main/java/org/fao/geonet/api/records/formatters/ImageReplacedElementFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2016 Food and Agriculture Organization of the + * Copyright (C) 2001-2023 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -28,11 +28,14 @@ import com.lowagie.text.Image; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; +import org.fao.geonet.ApplicationContextHolder; import org.fao.geonet.api.ApiUtils; +import org.fao.geonet.api.records.attachments.Store; import org.fao.geonet.api.records.extent.MapRenderer; import org.fao.geonet.api.records.extent.MetadataExtentApi; import org.fao.geonet.constants.Geonet; import org.fao.geonet.constants.Params; +import org.fao.geonet.domain.MetadataResourceVisibility; import org.fao.geonet.utils.Log; import org.xhtmlrenderer.extend.FSImage; import org.xhtmlrenderer.extend.ReplacedElement; @@ -82,10 +85,11 @@ private static Set getSupportedExts() { if (imgFormatExts == null) { synchronized (ImageReplacedElementFactory.class) { if (imgFormatExts == null) { - imgFormatExts = Sets.newHashSet(); + Set tmpImgFormatExts = Sets.newHashSet(); for (String ext : ImageIO.getReaderFileSuffixes()) { - imgFormatExts.add(ext.toLowerCase()); + tmpImgFormatExts.add(ext.toLowerCase()); } + imgFormatExts = tmpImgFormatExts; } } } @@ -93,9 +97,11 @@ private static Set getSupportedExts() { return imgFormatExts; } - static private Pattern ONE_EXTENT_API_REGEX = Pattern.compile(".*/(.*)/extents/([0-9]+)\\.png.*"); - static private Pattern ALL_EXTENT_API_REGEX = Pattern.compile(".*/(.*)/extents\\.png.*"); - static private final String EXTENT_XPATH = ".//*[local-name() ='extent']/*/*[local-name() = 'geographicElement']/*"; + private static final Pattern ONE_EXTENT_API_REGEX = Pattern.compile(".*/(.*)/extents/(\\d+)\\.png.*"); + private static final Pattern ALL_EXTENT_API_REGEX = Pattern.compile(".*/(.*)/extents\\.png.*"); + private static final String EXTENT_XPATH = ".//*[local-name() ='extent']/*/*[local-name() = 'geographicElement']/*"; + + private static final String DEFAULT_SRS = "EPSG:4326"; @Override public ReplacedElement createReplacedElement(LayoutContext layoutContext, BlockBox box, @@ -109,15 +115,15 @@ public ReplacedElement createReplacedElement(LayoutContext layoutContext, BlockB if (!"img".equals(nodeName)) { try { return superFactory.createReplacedElement(layoutContext, box, userAgentCallback, cssWidth, cssHeight); - } catch (Throwable e) { + } catch (Exception e) { return new EmptyReplacedElement(cssWidth, cssHeight); } } - String src = element.getAttribute("src"); + String baseUrlNoLang = baseURL.substring(0, baseURL.length() - 4); - boolean useExtentApi = src.startsWith(baseURL.substring(0, baseURL.length() - 4)) + boolean useExtentApi = src.startsWith(baseUrlNoLang) && mapRenderer != null && (ALL_EXTENT_API_REGEX.matcher(src).matches() || ONE_EXTENT_API_REGEX.matcher(src).matches()); @@ -135,7 +141,7 @@ public ReplacedElement createReplacedElement(LayoutContext layoutContext, BlockB regionId = String.format("metadata:@id%s:@xpath(%s)[%s]", ApiUtils.getInternalId(oneMatcher.group(1), true), EXTENT_XPATH, oneMatcher.group(2)); } Map parameters = getParams(src); - String srs = parameters.get(MetadataExtentApi.MAP_SRS_PARAM) != null ? parameters.get(MetadataExtentApi.MAP_SRS_PARAM) : "EPSG:4326"; + String srs = parameters.get(MetadataExtentApi.MAP_SRS_PARAM) != null ? parameters.get(MetadataExtentApi.MAP_SRS_PARAM) : DEFAULT_SRS; Integer width = parameters.get(MetadataExtentApi.WIDTH_PARAM) != null ? Integer.parseInt(parameters.get(MetadataExtentApi.WIDTH_PARAM)) : null; Integer height = parameters.get(MetadataExtentApi.HEIGHT_PARAM) != null ? Integer.parseInt(parameters.get(MetadataExtentApi.HEIGHT_PARAM)) : null; String background = parameters.get(MetadataExtentApi.BACKGROUND_PARAM); @@ -145,20 +151,25 @@ public ReplacedElement createReplacedElement(LayoutContext layoutContext, BlockB } float factor = layoutContext.getDotsPerPixel(); return loadImage(layoutContext, box, userAgentCallback, cssWidth, cssHeight, new BufferedImageLoader(image), factor); - } else if (src.startsWith(baseURL + "region.getmap.png") | src.endsWith("/geom.png") && mapRenderer != null) { + } else if (src.startsWith(baseURL + "region.getmap.png") || src.endsWith("/geom.png") && mapRenderer != null) { BufferedImage image = null; try { Map parameters = getParams(src); String id = parameters.get(Params.ID); - String srs = parameters.get(MetadataExtentApi.MAP_SRS_PARAM) != null ? parameters.get(MetadataExtentApi.MAP_SRS_PARAM) : "EPSG:4326"; + String srs = parameters.get(MetadataExtentApi.MAP_SRS_PARAM) != null ? parameters.get(MetadataExtentApi.MAP_SRS_PARAM) : DEFAULT_SRS; Integer width = parameters.get(MetadataExtentApi.WIDTH_PARAM) != null ? Integer.parseInt(parameters.get(MetadataExtentApi.WIDTH_PARAM)) : null; Integer height = parameters.get(MetadataExtentApi.HEIGHT_PARAM) != null ? Integer.parseInt(parameters.get(MetadataExtentApi.HEIGHT_PARAM)) : null; String background = parameters.get(MetadataExtentApi.BACKGROUND_PARAM); String geomParam = parameters.get(MetadataExtentApi.GEOM_PARAM); String geomType = parameters.get(MetadataExtentApi.GEOM_TYPE_PARAM) != null ? parameters.get(MetadataExtentApi.GEOM_TYPE_PARAM) : "WKT"; - String geomSrs = parameters.get(MetadataExtentApi.GEOM_SRS_PARAM) != null ? parameters.get(MetadataExtentApi.GEOM_SRS_PARAM) : "EPSG:4326"; + String geomSrs = parameters.get(MetadataExtentApi.GEOM_SRS_PARAM) != null ? parameters.get(MetadataExtentApi.GEOM_SRS_PARAM) : DEFAULT_SRS; + if ((width == null) && (height == null)) { + // Width or height are required. If not set the default width with the same value + // as defined in MetadataExtentApi.getOneRecordExtentAsImage + width = 300; + } image = mapRenderer.render( id, srs, width, height, background, geomParam, geomType, geomSrs, null, null); } catch (Exception e) { @@ -189,6 +200,21 @@ public ReplacedElement createReplacedElement(LayoutContext layoutContext, BlockB } float factor = layoutContext.getDotsPerPixel(); return loadImage(layoutContext, box, userAgentCallback, cssWidth, cssHeight, new UrlImageLoader(builder.toString()), factor); + + } else if (src.startsWith(baseUrlNoLang) && src.contains("/attachments/")) { + // Process attachments urls to load the images from the data directory + Matcher m = Pattern.compile(baseUrlNoLang + "api/records/(.*)/attachments/(.*)$").matcher(src); + if (m.find()) { + String uuid = m.group(1); + String file = m.group(2); + + float factor = layoutContext.getDotsPerPixel(); + return loadImage(layoutContext, box, userAgentCallback, cssWidth, cssHeight, new DataDirectoryImageLoader(uuid, file), factor); + } else if (isSupportedImageFormat(src)) { + float factor = layoutContext.getDotsPerPixel(); + return loadImage(layoutContext, box, userAgentCallback, cssWidth, cssHeight, new UrlImageLoader(src), factor); + } + } else if (isSupportedImageFormat(src)) { float factor = layoutContext.getDotsPerPixel(); return loadImage(layoutContext, box, userAgentCallback, cssWidth, cssHeight, new UrlImageLoader(src), factor); @@ -196,7 +222,7 @@ public ReplacedElement createReplacedElement(LayoutContext layoutContext, BlockB try { return superFactory.createReplacedElement(layoutContext, box, userAgentCallback, cssWidth, cssHeight); - } catch (Throwable e) { + } catch (Exception e) { return new EmptyReplacedElement(cssWidth, cssHeight); } } @@ -247,7 +273,7 @@ private ReplacedElement loadImage(LayoutContext layoutContext, BlockBox box, Use try { return superFactory.createReplacedElement(layoutContext, box, userAgentCallback, cssWidth, cssHeight); - } catch (Throwable e2) { + } catch (Exception e2) { return new EmptyReplacedElement(cssWidth, cssHeight); } } @@ -296,6 +322,35 @@ public Image loadImage() throws Exception { } } + + /** + * Class to load images from the metadata data directory. + */ + private class DataDirectoryImageLoader implements ImageLoader { + private final String uuid; + private final String file; + public DataDirectoryImageLoader(String uuid, String file) { + this.uuid = uuid; + this.file = file; + } + + @Override + public Image loadImage() throws Exception { + Store store = ApplicationContextHolder.get().getBean("filesystemStore", Store.class); + BufferedImage bufferedImage; + try (Store.ResourceHolder imageFile = store.getResourceInternal( + this.uuid, + MetadataResourceVisibility.PUBLIC, + this.file, true)) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + bufferedImage = ImageIO.read(imageFile.getPath().toFile()); + ImageIO.write(bufferedImage, "png", baos); + return Image.getInstance(baos.toByteArray()); + } + } + } + + /* Define an AWT BufferedImage image loader */ private class BufferedImageLoader implements ImageLoader { diff --git a/services/src/main/java/org/fao/geonet/api/records/model/related/RelatedItemType.java b/services/src/main/java/org/fao/geonet/api/records/model/related/RelatedItemType.java index a2fc5e8df438..396f26d65135 100644 --- a/services/src/main/java/org/fao/geonet/api/records/model/related/RelatedItemType.java +++ b/services/src/main/java/org/fao/geonet/api/records/model/related/RelatedItemType.java @@ -1,6 +1,9 @@ package org.fao.geonet.api.records.model.related; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(enumAsRef = true) public enum RelatedItemType { /** diff --git a/services/src/main/java/org/fao/geonet/api/registries/vocabularies/KeywordsApi.java b/services/src/main/java/org/fao/geonet/api/registries/vocabularies/KeywordsApi.java index 93ad30ec0c23..d28d7507a6c2 100644 --- a/services/src/main/java/org/fao/geonet/api/registries/vocabularies/KeywordsApi.java +++ b/services/src/main/java/org/fao/geonet/api/registries/vocabularies/KeywordsApi.java @@ -327,10 +327,7 @@ public Object searchKeywords( ) @RequestMapping( path = "/keyword", - method = { - RequestMethod.GET, - RequestMethod.POST - }, + method = RequestMethod.GET, produces = { MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE @@ -382,6 +379,112 @@ public Object getKeywordById( @Parameter(hidden = true) HttpServletRequest request ) throws Exception { + return getKeyword(uri,sThesaurusName,langs, keywordOnly, transformation,langMapJson,allRequestParams, accept, request); + } + + /** + * Gets the keyword by id. + * + * @param uri the uri + * @param sThesaurusName the s thesaurus name + * @param langs the langs + * @param keywordOnly the keyword only + * @param transformation the transformation + * @param allRequestParams the all request params + * @param request the request + * @return the keyword by id + * @throws Exception the exception + */ + @io.swagger.v3.oas.annotations.Operation( + summary = "Get keyword by ids", + description = "Retrieve XML representation of keyword(s) from same thesaurus" + + "using different transformations. " + + "'to-iso19139-keyword' is the default and return an ISO19139 snippet." + + "'to-iso19139-keyword-as-xlink' return an XLinked element. Custom transformation " + + "can be create on a per schema basis." + + "This can be used instead of the GET method for cases where you need to submit large parameters list" + ) + @RequestMapping( + path = "/keyword", + method = RequestMethod.POST, + produces = { + MediaType.APPLICATION_XML_VALUE, + MediaType.APPLICATION_JSON_VALUE + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "XML snippet with requested keywords."), + }) + @ResponseBody + @ResponseStatus(HttpStatus.OK) + public Object getKeywordByIds( + @Parameter( + description = "Keyword identifier or list of keyword identifiers comma separated.", + required = true) + @RequestParam(name = "id") + String uri, + @Parameter( + description = "Thesaurus to look info for the keyword(s).", + required = true) + @RequestParam(name = "thesaurus") + String sThesaurusName, + @Parameter( + description = "Languages.", + required = false) + @RequestParam(name = "lang", required = false) + String[] langs, + @Parameter( + description = "Only print the keyword, no thesaurus information.", + required = false) + @RequestParam(required = false, defaultValue = "false") + boolean keywordOnly, + @Parameter( + description = "XSL template to use (ISO19139 keyword by default, see convert.xsl).", + required = false) + @RequestParam(required = false) + String transformation, + @Parameter( + description = "langMap, that converts the values in the 'lang' parameter to how they will be actually represented in the record. {'fre':'fra'} or {'fre':'fr'}. Missing/empty means to convert to iso 2 letter.", + required = false) + @RequestParam (name = "langMap", required = false) + String langMapJson, + @Parameter(hidden = true) + @RequestParam + Map allRequestParams, + @RequestHeader( + value = "Accept", + defaultValue = MediaType.APPLICATION_XML_VALUE + ) + String accept, + @Parameter(hidden = true) + HttpServletRequest request + ) throws Exception { + return getKeyword(uri,sThesaurusName,langs, keywordOnly, transformation,langMapJson,allRequestParams, accept, request); + } + + /** + * Gets the keyword by id. + * + * @param uri the uri + * @param sThesaurusName the s thesaurus name + * @param langs the langs + * @param keywordOnly the keyword only + * @param transformation the transformation + * @param allRequestParams the all request params + * @param request the request + * @return the keyword by id + * @throws Exception the exception + */ + private Object getKeyword( + String uri, + String sThesaurusName, + String[] langs, + boolean keywordOnly, + String transformation, + String langMapJson, + Map allRequestParams, + String accept, + HttpServletRequest request + ) throws Exception { final String SEPARATOR = ","; ServiceContext context = ApiUtils.createServiceContext(request); boolean isJson = MediaType.APPLICATION_JSON_VALUE.equals(accept); @@ -458,7 +561,7 @@ public Object getKeywordById( } } - Element langConversion = null; + Element langConversion = null; if ( (langMapJson != null) && (!langMapJson.isEmpty()) ){ JSONObject obj = JSONObject.fromObject(langMapJson); langConversion = new Element("languageConversions"); @@ -512,7 +615,6 @@ public Object getKeywordById( } } - /** * Gets the thesaurus. * diff --git a/services/src/main/java/org/fao/geonet/api/related/Related.java b/services/src/main/java/org/fao/geonet/api/related/Related.java index ae800982a45c..460ab60b6aa6 100644 --- a/services/src/main/java/org/fao/geonet/api/related/Related.java +++ b/services/src/main/java/org/fao/geonet/api/related/Related.java @@ -81,7 +81,7 @@ public synchronized void setApplicationContext(ApplicationContext context) { summary = "Get record related resources for all requested metadatas", description = "Retrieve related services, datasets, onlines, thumbnails, sources, ... " + "to all requested records.
" + - "More info") + "More info") @RequestMapping(value = "", method = RequestMethod.GET, produces = { diff --git a/services/src/main/java/org/fao/geonet/api/site/SiteApi.java b/services/src/main/java/org/fao/geonet/api/site/SiteApi.java index 00f478966be1..dcefa7dd5e05 100644 --- a/services/src/main/java/org/fao/geonet/api/site/SiteApi.java +++ b/services/src/main/java/org/fao/geonet/api/site/SiteApi.java @@ -42,6 +42,7 @@ import org.fao.geonet.SystemInfo; import org.fao.geonet.api.ApiParams; import org.fao.geonet.api.ApiUtils; +import org.fao.geonet.api.OpenApiConfig; import org.fao.geonet.api.exception.NotAllowedException; import org.fao.geonet.api.site.model.SettingSet; import org.fao.geonet.api.site.model.SettingsListResponse; @@ -92,6 +93,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.awt.image.BufferedImage; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -420,6 +422,7 @@ public void saveSettings( ApplicationContext applicationContext = ApplicationContextHolder.get(); String currentUuid = settingManager.getSiteId(); String oldSiteName = settingManager.getSiteName(); + String oldBaseUrl = settingManager.getBaseURL(); if (!settingManager.setValues(allRequestParams)) { throw new OperationAbortedEx("Cannot set all values"); @@ -439,6 +442,11 @@ public void saveSettings( sourceRepository.save(siteSource); } } + String newBaseUrl = settingManager.getBaseURL(); + // Update SpringDoc host information if the base url is changed. + if (!oldBaseUrl.equals(newBaseUrl)) { + OpenApiConfig.setHostRelatedInfo(); + } // Update the system default timezone. If the setting is blank use the timezone user.timezone property from command line or // TZ environment variable @@ -571,7 +579,7 @@ public boolean isIndexing( method = RequestMethod.PUT) @PreAuthorize("hasAuthority('Editor')") @ResponseBody - public HttpEntity index( + public HttpEntity indexSite( @Parameter(description = "Drop and recreate index", required = false) @RequestParam(required = false, defaultValue = "true") @@ -869,7 +877,7 @@ public List getXslTransformations( )) { for (Path sheet : sheets) { String id = sheet.toString(); - if (id != null && id.contains("convert/from") && id.endsWith(".xsl")) { + if (id != null && id.contains("convert" + File.separator + "from") && id.endsWith(".xsl")) { String name = com.google.common.io.Files.getNameWithoutExtension( sheet.getFileName().toString()); list.add(IMPORT_STYLESHEETS_SCHEMA_PREFIX + schema + ":convert/" + name); diff --git a/services/src/main/java/org/fao/geonet/api/site/SiteInformation.java b/services/src/main/java/org/fao/geonet/api/site/SiteInformation.java index 7041658c6dcc..c188f63c6a33 100644 --- a/services/src/main/java/org/fao/geonet/api/site/SiteInformation.java +++ b/services/src/main/java/org/fao/geonet/api/site/SiteInformation.java @@ -32,6 +32,9 @@ import org.fao.geonet.GeonetContext; import org.fao.geonet.constants.Geonet; import org.fao.geonet.kernel.search.EsSearchManager; +import org.fao.geonet.kernel.setting.SettingInfo; +import org.fao.geonet.kernel.setting.SettingManager; +import org.fao.geonet.kernel.setting.Settings; import org.fao.geonet.utils.Env; import org.fao.geonet.utils.Log; import org.fao.geonet.utils.TransformerFactoryFactory; @@ -70,7 +73,7 @@ public SiteInformation(final ServiceContext context, final GeonetContext gc) { Log.error(Geonet.GEONETWORK, e.getMessage(), e); } loadEnvInfo(); - loadVersionInfo(); + loadVersionInfo(context); loadSystemInfo(); } } @@ -266,12 +269,17 @@ private void loadDatabaseInfo(ServiceContext context) throws SQLException { } /** - * Compute information about git commit. + * Compute information about the application and git commit. */ - private void loadVersionInfo() { + private void loadVersionInfo(ServiceContext context) { Properties prop = new Properties(); try (InputStream input = getClass().getResourceAsStream("/git.properties")) { + SettingManager settingManager = context.getBean(SettingManager.class); + + versionProperties.put("app.version", settingManager.getValue(Settings.SYSTEM_PLATFORM_VERSION)); + versionProperties.put("app.subVersion", settingManager.getValue(Settings.SYSTEM_PLATFORM_SUBVERSION)); + prop.load(input); Enumeration e = prop.propertyNames(); diff --git a/services/src/main/java/org/fao/geonet/api/sld/SldApi.java b/services/src/main/java/org/fao/geonet/api/sld/SldApi.java index 27b766679cfe..fc8cd2dfc475 100644 --- a/services/src/main/java/org/fao/geonet/api/sld/SldApi.java +++ b/services/src/main/java/org/fao/geonet/api/sld/SldApi.java @@ -1,5 +1,8 @@ package org.fao.geonet.api.sld; +import static org.fao.geonet.api.ApiParams.API_CLASS_TOOLS_OPS; +import static org.fao.geonet.api.ApiParams.API_CLASS_TOOLS_TAG; + import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jeeves.transaction.TransactionManager; @@ -47,8 +50,8 @@ @RequestMapping(value = { "/{portal}/api/tools/ogc" }) -@Tag(name = "tools", - description = "Utility operations") +@Tag(name = API_CLASS_TOOLS_TAG, + description = API_CLASS_TOOLS_OPS) public class SldApi { public static final String LOGGER = Geonet.GEONETWORK + ".api.sld"; diff --git a/services/src/main/java/org/fao/geonet/api/sources/SourcesApi.java b/services/src/main/java/org/fao/geonet/api/sources/SourcesApi.java index 2c268bab6680..ce6013e3f7fb 100644 --- a/services/src/main/java/org/fao/geonet/api/sources/SourcesApi.java +++ b/services/src/main/java/org/fao/geonet/api/sources/SourcesApi.java @@ -272,7 +272,7 @@ public ResponseEntity updateSource( @PreAuthorize("hasAuthority('Administrator')") @ResponseStatus(HttpStatus.NO_CONTENT) @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Source deleted."), + @ApiResponse(responseCode = "204", description = "Source deleted."), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_ADMIN) }) @ResponseBody diff --git a/services/src/main/java/org/fao/geonet/api/tools/i18n/TranslationApi.java b/services/src/main/java/org/fao/geonet/api/tools/i18n/TranslationApi.java index 0ed2fce83f42..1d667c47d1f7 100644 --- a/services/src/main/java/org/fao/geonet/api/tools/i18n/TranslationApi.java +++ b/services/src/main/java/org/fao/geonet/api/tools/i18n/TranslationApi.java @@ -52,6 +52,8 @@ import java.util.stream.Collectors; import static java.util.stream.Collectors.groupingBy; +import static org.fao.geonet.api.ApiParams.API_CLASS_TOOLS_OPS; +import static org.fao.geonet.api.ApiParams.API_CLASS_TOOLS_TAG; import static org.springframework.http.HttpStatus.CREATED; import static org.springframework.http.HttpStatus.OK; @@ -61,7 +63,8 @@ @RequestMapping(value = { "/{portal}/api/i18n" }) -@Tag(name = "tools") +@Tag(name = API_CLASS_TOOLS_TAG, + description = API_CLASS_TOOLS_OPS) @RestController public class TranslationApi { @@ -313,7 +316,7 @@ public Map getDbTranslations( MediaType.APPLICATION_JSON_VALUE }) @ResponseBody - public Map> getTranslationsPackage() { + public Map> getTranslationsPackages() { return translationPackBuilder.getPackages(); } diff --git a/services/src/main/java/org/fao/geonet/api/tools/mail/MailApi.java b/services/src/main/java/org/fao/geonet/api/tools/mail/MailApi.java index a5f2bf6519f8..2ff5f7f9e9be 100644 --- a/services/src/main/java/org/fao/geonet/api/tools/mail/MailApi.java +++ b/services/src/main/java/org/fao/geonet/api/tools/mail/MailApi.java @@ -45,6 +45,9 @@ //=== Rome - Italy. email: geonetwork@osgeo.org //============================================================================== +import static org.fao.geonet.api.ApiParams.API_CLASS_TOOLS_OPS; +import static org.fao.geonet.api.ApiParams.API_CLASS_TOOLS_TAG; + import io.swagger.v3.oas.annotations.tags.Tag; import org.fao.geonet.api.API; import org.fao.geonet.api.tools.i18n.LanguageUtils; @@ -73,8 +76,8 @@ @RequestMapping(value = { "/{portal}/api/tools/mail" }) -@Tag(name = "tools", - description = "Utility operations") +@Tag(name = API_CLASS_TOOLS_TAG, + description = API_CLASS_TOOLS_OPS) @Controller("mail") public class MailApi { diff --git a/services/src/main/java/org/fao/geonet/api/tools/migration/MigrationApi.java b/services/src/main/java/org/fao/geonet/api/tools/migration/MigrationApi.java index c5a5a8f8568f..63c72bb3ae59 100644 --- a/services/src/main/java/org/fao/geonet/api/tools/migration/MigrationApi.java +++ b/services/src/main/java/org/fao/geonet/api/tools/migration/MigrationApi.java @@ -23,6 +23,9 @@ package org.fao.geonet.api.tools.migration; +import static org.fao.geonet.api.ApiParams.API_CLASS_TOOLS_OPS; +import static org.fao.geonet.api.ApiParams.API_CLASS_TOOLS_TAG; + import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.fao.geonet.ApplicationContextHolder; @@ -43,7 +46,8 @@ @RequestMapping(value = { "/{portal}/api/tools/migration" }) -@Tag(name = "tools") +@Tag(name = API_CLASS_TOOLS_TAG, + description = API_CLASS_TOOLS_OPS) @RestController public class MigrationApi { diff --git a/services/src/main/java/org/fao/geonet/api/usersearches/UserSearchesApi.java b/services/src/main/java/org/fao/geonet/api/usersearches/UserSearchesApi.java index 8a59adf9cb73..517744f9ce70 100644 --- a/services/src/main/java/org/fao/geonet/api/usersearches/UserSearchesApi.java +++ b/services/src/main/java/org/fao/geonet/api/usersearches/UserSearchesApi.java @@ -290,7 +290,7 @@ public UserSearchDto getUserCustomSearch( @RequestMapping( produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.PUT) - @ResponseStatus(value = HttpStatus.OK) + @ResponseStatus(value = HttpStatus.CREATED) @PreAuthorize("isAuthenticated()") @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "User search created.") diff --git a/services/src/main/java/org/fao/geonet/config/PublicationOption.java b/services/src/main/java/org/fao/geonet/config/PublicationOption.java index 6be7da114b3c..5c2e53783c53 100644 --- a/services/src/main/java/org/fao/geonet/config/PublicationOption.java +++ b/services/src/main/java/org/fao/geonet/config/PublicationOption.java @@ -30,6 +30,8 @@ import java.util.List; import java.util.Map; +import io.swagger.v3.oas.annotations.media.Schema; + /** * Defines a publication configuration. * @@ -42,12 +44,15 @@ public class PublicationOption { private String name; // Group to publish + @Schema(enumAsRef = true) private ReservedGroup publicationGroup; // List of operations to activate in the group to publish/unpublish. - List publicationOperations; + @Schema(enumAsRef = true) + private List publicationOperations; // Additional group(s)/operations(s) to publish/unpublish when the publication is selected. + @Schema(enumAsRef = true) private EnumMap> additionalPublications = new EnumMap<>(ReservedGroup.class); PublicationOption(String name, ReservedGroup publicationGroup, List publicationOperations) { diff --git a/services/src/test/java/org/fao/geonet/api/records/extent/MapRendererTest.java b/services/src/test/java/org/fao/geonet/api/records/extent/MapRendererTest.java index 47ca774eea0c..4a1762325202 100644 --- a/services/src/test/java/org/fao/geonet/api/records/extent/MapRendererTest.java +++ b/services/src/test/java/org/fao/geonet/api/records/extent/MapRendererTest.java @@ -1,3 +1,26 @@ +/* + * Copyright (C) 2001-2023 Food and Agriculture Organization of the + * United Nations (FAO-UN), United Nations World Food Programme (WFP) + * and United Nations Environment Programme (UNEP) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + * + * Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2, + * Rome - Italy. email: geonetwork@osgeo.org + */ + package org.fao.geonet.api.records.extent; import org.fao.geonet.kernel.region.Region; @@ -48,4 +71,55 @@ public void domainIntersectsBoundingPolygon() throws Exception { String result = wktWriter.write(MapRenderer.computeGeomInDomainOfValidity(bbox, Region.decodeCRS("EPSG:3857"))); assertEquals("POLYGON ((159.70909090909092 -85.06, 135 -76, 161 -76, 153 -81, 161.12 -85.06, 159.70909090909092 -85.06))", result); } + + @Test + public void extentGeodesicLocalProjection() throws Exception { + String test = "POLYGON ((646.3563610491983 308975.2885578188, 10545.528150276805 637111.0281460616, 276050.8102636032 636456.3084312443, 284347.2430806639 308289.5622343737, 646.3563610491983 308975.2885578188))"; + String localSrs = "EPSG:28992"; + Geometry geometry = wktReader.read(test); + geometry.setSRID(28992); + + Geometry geometryExtent = MapRenderer.getGeometryExtent(geometry, localSrs, true); + assertEquals(geometry, geometryExtent); + } + + @Test + public void extentNonGeodesicLocalProjection() throws Exception { + String test = "POLYGON ((646.3563610491983 308975.2885578188, 10545.528150276805 637111.0281460616, 276050.8102636032 636456.3084312443, 284347.2430806639 308289.5622343737, 646.3563610491983 308975.2885578188))"; + String localSrs = "EPSG:28992"; + Geometry geometry = wktReader.read(test); + geometry.setSRID(28992); + + Geometry geometryExtent = MapRenderer.getGeometryExtent(geometry, localSrs, false); + assertEquals(geometry.getEnvelope(), geometryExtent); + } + + @Test + public void extentGlobal3857Projection() throws Exception { + String test = "POLYGON ((159.70909090909092 -85.06, 135 -76, 161 -76, 153 -81, 161.12 -85.06, 159.70909090909092 -85.06))"; + String globalSrs = "EPSG:3857"; + Geometry geometry = wktReader.read(test); + geometry.setSRID(3857); + + // For global projections + Geometry geometryExtent = MapRenderer.getGeometryExtent(geometry, globalSrs, true); + assertEquals(geometry, geometryExtent); + + geometryExtent = MapRenderer.getGeometryExtent(geometry, globalSrs, false); + assertEquals(geometry, geometryExtent); + } + + @Test + public void extentGlobal4326Projection() throws Exception { + String test = "POLYGON ((165 -87, 135 -76, 161 -76, 153 -81, 165 -87))"; + String globalSrs = "EPSG:4326"; + Geometry geometry = wktReader.read(test); + geometry.setSRID(4326); + + Geometry geometryExtent = MapRenderer.getGeometryExtent(geometry, globalSrs, true); + assertEquals(geometry, geometryExtent); + + geometryExtent = MapRenderer.getGeometryExtent(geometry, globalSrs, false); + assertEquals(geometry, geometryExtent); + } } diff --git a/software_development/GITHUB.md b/software_development/GITHUB.md index defd00d6e850..e12e6ba0af1a 100644 --- a/software_development/GITHUB.md +++ b/software_development/GITHUB.md @@ -34,3 +34,23 @@ GeoNetwork uses feature branches for development, and a pull-request workflow fo * [Contributing](../CONTRIBUTING.md). * [Making a pull request](https://docs.geonetwork-opensource.org/latest/contributing/making-a-pull-request/). + +## Automation + +### Quality Assurance + +A number of [workflows]((../.github/workflows/) are setup to ensure each PR compiles, passes tests and so forth. + +* [linux.yml](../.github/workflows/linux.yml): build and QA, including -Drelease check +* [docs.yml](../.github/workflows/docs.yml): publish docs to gh-pages branch +* [backport.yml](../.github/workflows/backport.yml): backport tagged pull requests + +### Tags + +Use backport tags to take advantage of the [backport.yml](https://github.com/m-kuhn/backport) automation: + +> Backport is a JavaScript GitHub Action to backport a pull request by simply adding a label to it. +> +> It can backport rebased and merged pull requests with a single commit and squashed and merged pull requests. I + +This agrees with our CONTRIBUTING policy of using rebase and squash and merge. diff --git a/web-ui/src/main/resources/WEB-INF/classes/web-ui-wro-sources.xml b/web-ui/src/main/resources/WEB-INF/classes/web-ui-wro-sources.xml index 8d7932387c30..723897c06876 100644 --- a/web-ui/src/main/resources/WEB-INF/classes/web-ui-wro-sources.xml +++ b/web-ui/src/main/resources/WEB-INF/classes/web-ui-wro-sources.xml @@ -36,7 +36,7 @@ - + @@ -106,7 +106,7 @@ - + diff --git a/web-ui/src/main/resources/catalog/components/admin/uiconfig/partials/uiconfig.html b/web-ui/src/main/resources/catalog/components/admin/uiconfig/partials/uiconfig.html index ee1f2c513f36..786d13aabfc6 100644 --- a/web-ui/src/main/resources/catalog/components/admin/uiconfig/partials/uiconfig.html +++ b/web-ui/src/main/resources/catalog/components/admin/uiconfig/partials/uiconfig.html @@ -333,21 +333,27 @@

{{('ui-mod-' + m) | translate}}

- Icon + {{('ui-' + key + '-icon') | translate}} - Template URL + {{('ui-' + key + '-tplUrl') | translate}} - Tooltip + {{('ui-' + key + '-tooltip') | translate}} {{('ui-mod-' + m) | translate}} data-ng-model="opt.tooltip" />
+ +
+ + +
+
+ + +
-
@@ -9,7 +12,7 @@ data-typename="{{::isLayerProtocol(r) ? r.locTitle : ''}}" data-url="{{::r.locUrl}}" >
+ +
diff --git a/web-ui/src/main/resources/catalog/components/search/mdview/partials/contact.html b/web-ui/src/main/resources/catalog/components/search/mdview/partials/contact.html index 0d11fcf1bda5..d322f972f176 100644 --- a/web-ui/src/main/resources/catalog/components/search/mdview/partials/contact.html +++ b/web-ui/src/main/resources/catalog/components/search/mdview/partials/contact.html @@ -34,7 +34,8 @@ {{c.address}}
- call {{c.phone}} + + {{c.phone}} @@ -91,7 +92,8 @@ {{c.address}} - call {{c.phone}} + + {{c.phone}} @@ -147,7 +149,8 @@ {{c.address}} - call {{c.phone}} + + {{c.phone}} @@ -187,7 +190,8 @@

{{c.address}}
- call {{c.phone}} + + {{c.phone}} @@ -226,7 +230,8 @@

{{c.address}}
- call {{c.phone}} + + {{c.phone}}
diff --git a/web-ui/src/main/resources/catalog/components/search/resultsview/ResultsviewDirective.js b/web-ui/src/main/resources/catalog/components/search/resultsview/ResultsviewDirective.js index ad65780f064a..5ccb5887051f 100644 --- a/web-ui/src/main/resources/catalog/components/search/resultsview/ResultsviewDirective.js +++ b/web-ui/src/main/resources/catalog/components/search/resultsview/ResultsviewDirective.js @@ -47,11 +47,11 @@ return { require: "^ngSearchForm", templateUrl: - "../../catalog/components/search/resultsview/partials/" + "templateswitcher.html", + "../../catalog/components/search/resultsview/partials/templateswitcher.html", restrict: "A", link: function ($scope, element, attrs, searchFormCtrl) { $scope.setResultTemplate = function (t) { - $scope.resultTemplate = t.tplUrl; + $scope.resultTemplate = t; searchFormCtrl.triggerSearch(true); }; } @@ -91,18 +91,6 @@ link: function (scope, element, attrs, controller) { scope.mdService = gnMetadataActions; scope.map = scope.$eval(attrs.map); - //scope.searchResults = scope.$eval(attrs.searchResults); - - /** Display fa icons for categories - * TODO: Move to configuration */ - scope.catIcons = { - featureCatalogs: "fa-table", - services: "fa-cog", - maps: "fa-globe", - staticMaps: "fa-globe", - datasets: "fa-file", - interactiveResources: "fa-rss" - }; if (scope.map) { scope.hoverOL = new ol.layer.Vector({ @@ -177,13 +165,13 @@ } }); - scope.$watch("resultTemplate", function (templateUrl) { - if (angular.isUndefined(templateUrl)) { + scope.$watch("resultTemplate", function (templateConfig) { + if (angular.isUndefined(templateConfig)) { return; } var template = angular.element(document.createElement("div")); template.attr({ - "ng-include": "resultTemplate" + "ng-include": "resultTemplate.tplUrl" }); element.empty(); element.append(template); diff --git a/web-ui/src/main/resources/catalog/components/search/resultsview/partials/templateswitcher.html b/web-ui/src/main/resources/catalog/components/search/resultsview/partials/templateswitcher.html index 4a8f13fc238c..e6d22123552a 100644 --- a/web-ui/src/main/resources/catalog/components/search/resultsview/partials/templateswitcher.html +++ b/web-ui/src/main/resources/catalog/components/search/resultsview/partials/templateswitcher.html @@ -8,7 +8,7 @@ > @@ -17,7 +17,7 @@