Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.geoserver.featurestemplating.builders.visitors;

import org.geoserver.featurestemplating.builders.AbstractTemplateBuilder;
import org.geoserver.featurestemplating.builders.SourceBuilder;
import org.geoserver.featurestemplating.builders.impl.CompositeBuilder;
import org.geoserver.featurestemplating.builders.impl.DynamicValueBuilder;
import org.geoserver.featurestemplating.builders.impl.IteratingBuilder;
import org.geoserver.featurestemplating.builders.impl.RootBuilder;
import org.geoserver.featurestemplating.builders.impl.StaticBuilder;

/**
* Visitor with {@code visit} methods to be called by {@link
* org.geoserver.featurestemplating.builders.TemplateBuilder#accept}
*/
public interface TemplateVisitor {

/**
* Used to visit a {@link RootBuilder}.
*
* @param rootBuilder the root builder.
* @param extradata
* @return the eventual result of the visiting process.
*/
Object visit(RootBuilder rootBuilder, Object extradata);

/**
* Used to visit a {@link IteratingBuilder}.
*
* @param iteratingBuilder the iterating builder to be visited.
* @param extradata
* @return the eventual result of the visiting process.
*/
Object visit(IteratingBuilder iteratingBuilder, Object extradata);

/**
* Used to visit a {@link CompositeBuilder}.
*
* @param compositeBuilder the composite builder to be visited.
* @param extradata
* @return the eventual result of the visiting process.
*/
Object visit(CompositeBuilder compositeBuilder, Object extradata);

/**
* Used to visit a {@link DynamicValueBuilder}.
*
* @param dynamicBuilder the dynamic builder to be visited.
* @param extradata
* @return the eventual result of the visiting process.
*/
Object visit(DynamicValueBuilder dynamicBuilder, Object extradata);

/**
* Used to visit a {@link StaticBuilder}.
*
* @param staticBuilder the static builder to be visited.
* @param extradata
* @return the eventual result of the visiting process.
*/
Object visit(StaticBuilder staticBuilder, Object extradata);

/**
* Used to visit a {@link SourceBuilder}.
*
* @param sourceBuilder the source builder to be visited.
* @param extradata
* @return the eventual result of the visiting process.
*/
Object visit(SourceBuilder sourceBuilder, Object extradata);

/**
* Used to visit a {@link AbstractTemplateBuilder}
*
* @param abstractTemplateBuilder the abstract builder to be visited.
* @param extradata
* @return the eventual result of the visiting process.
*/
Object visit(AbstractTemplateBuilder abstractTemplateBuilder, Object extradata);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* (c) 2021 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.configuration;

import java.util.Optional;
import java.util.Set;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.platform.GeoServerExtensions;

/**
* A Template event listener that handle Template rules update when a TemplateInfo is deleted or
* modified.
*/
public class FeatureTypeTemplateDAOListener implements TemplateDAOListener {

private FeatureTypeInfo fti;

public FeatureTypeTemplateDAOListener(FeatureTypeInfo featureTypeInfo) {
this.fti = featureTypeInfo;
}

@Override
public void handleDeleteEvent(TemplateInfoEvent deleteEvent) {
TemplateLayerConfig layerConfig =
fti.getMetadata().get(TemplateLayerConfig.METADATA_KEY, TemplateLayerConfig.class);
TemplateInfo ti = deleteEvent.getSource();
if (layerConfig != null) {
Set<TemplateRule> rules = layerConfig.getTemplateRules();
if (!rules.isEmpty()) {
if (rules.removeIf(
r ->
r.getTemplateIdentifier()
.equals(deleteEvent.getSource().getIdentifier()))) {
fti.getMetadata().put(TemplateLayerConfig.METADATA_KEY, layerConfig);
saveFeatureTypeInfo();
updateCache(ti);
}
}
}
}

@Override
public void handleUpdateEvent(TemplateInfoEvent updateEvent) {
TemplateLayerConfig layerConfig =
fti.getMetadata().get(TemplateLayerConfig.METADATA_KEY, TemplateLayerConfig.class);
if (layerConfig != null) {
Set<TemplateRule> rules = layerConfig.getTemplateRules();
if (!rules.isEmpty()) {
TemplateInfo info = updateEvent.getSource();
Optional<TemplateRule> rule =
rules.stream()
.filter(r -> r.getTemplateIdentifier().equals(info.getIdentifier()))
.findFirst();
if (rule.isPresent()) {
TemplateRule r = rule.get();
if (!r.getTemplateName().equals(info.getFullName()))
r.setTemplateName(info.getFullName());
updateCache(info);
rules.removeIf(tr -> tr.getTemplateIdentifier().equals(info.getIdentifier()));
rules.add(r);
layerConfig.setTemplateRules(rules);
fti.getMetadata().put(TemplateLayerConfig.METADATA_KEY, layerConfig);
saveFeatureTypeInfo();
}
}
}
}

private void saveFeatureTypeInfo() {
Catalog catalog = (Catalog) GeoServerExtensions.bean("catalog");
catalog.save(fti);
}

private void updateCache(TemplateInfo info) {
TemplateLoader loader = TemplateLoader.get();
loader.cleanCache(fti, info.getIdentifier());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* (c) 2021 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.configuration;

import java.util.logging.Logger;
import org.geotools.util.logging.Logging;

public class FileTemplateDAOListener implements TemplateDAOListener {

private static final Logger LOGGER = Logging.getLogger(FileTemplateDAOListener.class);

@Override
public void handleDeleteEvent(TemplateInfoEvent deleteEvent) {
try {
TemplateFileManager.get().delete(deleteEvent.getSource());
} catch (Exception e) {
LOGGER.warning(
"Exception while deleting template file in a TemplateInfo delete event scope. Execption is: "
+ e.getMessage());
}
}

@Override
public void handleUpdateEvent(TemplateInfoEvent updateEvent) {
// do nothing
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* (c) 2021 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.configuration;

import java.util.Arrays;
import java.util.List;

public enum SupportedFormat {
JSONLD("JSON-LD"),
GML("GML"),
GEOJSON("GeoJSON"),
HTML("HTML");

private String format;

SupportedFormat(String format) {
this.format = format;
}

public String getFormat() {
return this.format;
}

public static List<SupportedFormat> getByExtension(String extension) {
if (extension == null) return Arrays.asList(SupportedFormat.values());
else if (extension.equals("xml")) return Arrays.asList(GML);
else if (extension.equals("xhtml")) return Arrays.asList(HTML);
else return Arrays.asList(JSONLD, GEOJSON);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,19 @@ public Template(Resource templateFile, TemplateReaderConfiguration configuration
try {
this.builderTree = watcher.read();
} catch (IOException ioe) {
throw new RuntimeException(ioe);
throw new RuntimeException("Failure parsing " + templateFile, ioe);
}
}

/** Check if the template file has benn modified and eventually reload it. */
public boolean checkTemplate() {
if (watcher != null && watcher.isModified()) {
if (needsReload()) {
LOGGER.log(
Level.INFO,
"Reloading json-ld template for Feature Type {0}",
templateFile.name());
synchronized (this) {
if (watcher != null && watcher.isModified()) {
if (needsReload()) {
try {
RootBuilder root = watcher.read();
this.builderTree = root;
Expand All @@ -58,6 +58,11 @@ public boolean checkTemplate() {
return false;
}

private boolean needsReload() {
return watcher != null
&& (watcher.isModified() || (builderTree != null && builderTree.needsReload()));
}

public void reloadTemplate() {
synchronized (this) {
if (watcher != null) {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* (c) 2021 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.configuration;

/** Base interface for listeners that can handle event issues by {@link TemplateInfoDAO} */
public interface TemplateDAOListener {

/**
* Handle a TemplateInfo delete event.
*
* @param deleteEvent the delete event.
*/
void handleDeleteEvent(TemplateInfoEvent deleteEvent);

/**
* Handle a TemplateInfo update event.
*
* @param updateEvent
*/
void handleUpdateEvent(TemplateInfoEvent updateEvent);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/* (c) 2021 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.configuration;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.WorkspaceInfo;
import org.geoserver.config.GeoServerDataDirectory;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.resource.Paths;
import org.geoserver.platform.resource.Resource;

/** Helper class that provides methods to manage the template file. */
public class TemplateFileManager {

private Catalog catalog;
private GeoServerDataDirectory dd;

public TemplateFileManager(Catalog catalog, GeoServerDataDirectory dd) {
this.catalog = catalog;
this.dd = dd;
}

/** @return the singleton instance of this class. */
public static TemplateFileManager get() {
return GeoServerExtensions.bean(TemplateFileManager.class);
}

/**
* Return a {@link Resource} from a template info.
*
* @param templateInfo the template info for which we want to retrieve the corresponding
* resource.
* @return the resource that corresponds to the template info.
*/
public Resource getTemplateResource(TemplateInfo templateInfo) {
String featureType = templateInfo.getFeatureType();
String workspace = templateInfo.getWorkspace();
String templateName = templateInfo.getTemplateName();
String extension = templateInfo.getExtension();
Resource resource;
if (featureType != null) {
FeatureTypeInfo fti = catalog.getFeatureTypeByName(featureType);
resource = dd.get(fti, templateName + "." + extension);
} else if (workspace != null) {
WorkspaceInfo ws = catalog.getWorkspaceByName(workspace);
resource = dd.get(ws, templateName + "." + extension);
} else {
resource =
dd.get(
Paths.path(
TemplateInfoDAOImpl.TEMPLATE_DIR,
templateName + "." + extension));
}
return resource;
}

/**
* Delete the template file associated to the template info passed as an argument.
*
* @param templateInfo the templateInfo for which we want to delete the corresponding template
* file.
* @return true if the delete process was successful false otherwise.
*/
public boolean delete(TemplateInfo templateInfo) {
return getTemplateResource(templateInfo).delete();
}

/**
* Return the directory where the template file is as a File object.
*
* @param templateInfo the template info to which the desired template file is associated.
* @return the directoryu where the template file associated to the templateInfo is placed.
*/
public File getTemplateLocation(TemplateInfo templateInfo) {
String featureType = templateInfo.getFeatureType();
String workspace = templateInfo.getWorkspace();
Resource resource = null;
if (featureType != null) {
FeatureTypeInfo fti = catalog.getFeatureTypeByName(featureType);
resource = dd.get(fti);
} else if (workspace != null) {
WorkspaceInfo ws = catalog.getWorkspaceByName(workspace);
resource = dd.get(ws);
} else {
resource = dd.get(TemplateInfoDAOImpl.TEMPLATE_DIR);
}
File destDir = resource.dir();
if (!destDir.exists() || !destDir.isDirectory()) {
destDir.mkdir();
}
return destDir;
}

/**
* Save a template in string form to the directory defined for the template info object.
*
* @param templateInfo the template info object.
* @param rawTemplate the template content to save to a file.
*/
public void saveTemplateFile(TemplateInfo templateInfo, String rawTemplate) {
File destDir = getTemplateLocation(templateInfo);
try {
File file =
new File(
destDir,
templateInfo.getTemplateName() + "." + templateInfo.getExtension());
if (!file.exists()) file.createNewFile();
synchronized (this) {
try (FileOutputStream fos = new FileOutputStream(file, false)) {
fos.write(rawTemplate.getBytes());
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@

package org.geoserver.featurestemplating.configuration;

import org.geoserver.ows.Dispatcher;
import org.geoserver.ows.Request;

/** This enum provides constants which match output formats with template names */
public enum TemplateIdentifier {
JSON("application/json", "geojson-template.json"),
GEOJSON("application/geo+json", "geojson-template.json"),
JSONLD("application/ld+json", "json-ld-template.json"),
GML32("application/gml+xml;version=3.2", "gml32-template.xml"),
GML31("gml3", "gml31-template.xml"),
GML2("GML2text/xml;subtype=gml/2.1.2", "gml2-template.xml");
GML2("GML2text/xml;subtype=gml/2.1.2", "gml2-template.xml"),
HTML("text/html", "html-template.xhtml");

private String outputFormat;
private String filename;
Expand All @@ -36,7 +40,7 @@ public String getFilename() {
* @param outputFormat the outputFormat for which to find a TemplateIdentifier.
* @return the TemplateIdentifier matching the outputFormat.
*/
public static TemplateIdentifier getTemplateIdentifierFromOutputFormat(String outputFormat) {
public static TemplateIdentifier fromOutputFormat(String outputFormat) {
TemplateIdentifier identifier = null;
String trimOutputFormat = outputFormat.trim().replaceAll(" ", "");
if (trimOutputFormat.equalsIgnoreCase(TemplateIdentifier.JSON.getOutputFormat()))
Expand All @@ -45,12 +49,31 @@ else if (trimOutputFormat.equalsIgnoreCase(TemplateIdentifier.JSONLD.getOutputFo
identifier = TemplateIdentifier.JSONLD;
else if (trimOutputFormat.equalsIgnoreCase(TemplateIdentifier.GEOJSON.getOutputFormat()))
identifier = TemplateIdentifier.GEOJSON;
else if (trimOutputFormat.equalsIgnoreCase(TemplateIdentifier.GML32.getOutputFormat()))
identifier = TemplateIdentifier.GML32;
else if (trimOutputFormat.equalsIgnoreCase(TemplateIdentifier.GML31.getOutputFormat()))
identifier = TemplateIdentifier.GML31;
else if (TemplateIdentifier.GML2.getOutputFormat().contains(trimOutputFormat))
identifier = TemplateIdentifier.GML2;
else if (isGML32(trimOutputFormat)) identifier = TemplateIdentifier.GML32;
else if (isGML31(trimOutputFormat)) identifier = TemplateIdentifier.GML31;
else if (isGML2(trimOutputFormat)) identifier = TemplateIdentifier.GML2;
else if (TemplateIdentifier.HTML.getOutputFormat().equals(trimOutputFormat))
identifier = TemplateIdentifier.HTML;
return identifier;
}

private static boolean isGML2(String outputFormat) {
Request request = Dispatcher.REQUEST.get();
boolean isFeatureInfo =
request != null && "GetFeatureInfo".equalsIgnoreCase(request.getRequest());
boolean result = false;
if (TemplateIdentifier.GML2.getOutputFormat().contains(outputFormat)) result = true;
else if (isFeatureInfo && "text/xml".equals(outputFormat)) result = true;
return result;
}

private static boolean isGML32(String outputFormat) {
return TemplateIdentifier.GML32.getOutputFormat().contains(outputFormat);
}

private static boolean isGML31(String outputFormat) {
return outputFormat.equalsIgnoreCase(TemplateIdentifier.GML31.getOutputFormat())
|| outputFormat.equalsIgnoreCase("text/xml;subtype=gml/3.1.1")
|| outputFormat.equalsIgnoreCase("application/vnd.ogc.gml/3.1.1");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/* (c) 2021 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.configuration;

import java.io.Serializable;
import java.util.Objects;
import java.util.UUID;

/**
* This class holds the template metadata, such as the name, the identifier, the template file
* extension, workspace and featureType.
*/
public class TemplateInfo implements Serializable, Comparable<TemplateInfo> {

private String identifier;

private String description;

protected String templateName;

protected String workspace;

protected String featureType;

protected String extension;

public TemplateInfo() {
super();
this.identifier = UUID.randomUUID().toString();
}

public TemplateInfo(
String identifier,
String templateName,
String workspace,
String featureType,
String extension) {
this.identifier = identifier;
this.templateName = templateName;
this.workspace = workspace;
this.featureType = featureType;
this.extension = extension;
}

public TemplateInfo(TemplateInfo info) {
this(
info.getIdentifier(),
info.getTemplateName(),
info.getWorkspace(),
info.getFeatureType(),
info.getExtension());
}

public String getTemplateName() {
return templateName;
}

public void setTemplateName(String templateName) {
this.templateName = templateName;
}

public String getWorkspace() {
return workspace;
}

public void setWorkspace(String workspace) {
this.workspace = workspace;
}

public String getFeatureType() {
return featureType;
}

public void setFeatureType(String featureType) {
this.featureType = featureType;
}

public String getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

public String getExtension() {
return extension;
}

public void setExtension(String extension) {
this.extension = extension;
}

public String getIdentifier() {
return identifier;
}

public void setIdentifier(String identifier) {
this.identifier = identifier;
}

/**
* Return the full name of the Template file. By full name is meant the templateName preceded by
* the workspace name and featureTypeInfo name if defined for this instance.
*
* @return
*/
public String getFullName() {
String fullName = "";
if (workspace != null) fullName += workspace + ":";
if (featureType != null) fullName += featureType + ":";
fullName += templateName;
return fullName;
}

@Override
public boolean equals(Object info) {
if (!super.equals(info)) return false;
if (!lenientEquals(info)) return false;
TemplateInfo templateInfo = (TemplateInfo) info;
return Objects.equals(identifier, templateInfo.identifier)
&& Objects.equals(description, templateInfo.description);
}

public boolean lenientEquals(Object o) {
if (o == null) return false;
TemplateInfo that = (TemplateInfo) o;
return Objects.equals(templateName, that.templateName)
&& Objects.equals(workspace, that.workspace)
&& Objects.equals(featureType, that.featureType)
&& Objects.equals(extension, that.extension);
}

@Override
public int hashCode() {
return Objects.hash(
identifier, templateName, description, workspace, featureType, extension);
}

@Override
public int compareTo(TemplateInfo o) {
return this.templateName.compareTo(o.getTemplateName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/* (c) 2021 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.configuration;

import java.util.List;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.platform.GeoServerExtensions;

/** Base interface for TemplateInfo Data Access. */
public interface TemplateInfoDAO {

static TemplateInfoDAOImpl get() {
return GeoServerExtensions.bean(TemplateInfoDAOImpl.class);
}

public static final String TEMPLATE_DIR = "features-templating";

/** @return all the saved template info. */
public List<TemplateInfo> findAll();

/**
* Find all the template info that can be used for the FeatureTypeInfo. It means that all the
* templates that are in the global directory plus all the templates in the workspace directory
* to which the FeatureTypeInfo belongs plus all the templates in the FeatureTypeInfo directory
* will be returned.
*
* @param featureTypeInfo
* @return
*/
public List<TemplateInfo> findByFeatureTypeInfo(FeatureTypeInfo featureTypeInfo);

/**
* @param id the identifier of the template info to retrieve.
* @return the TemplateInfo object.
*/
public TemplateInfo findById(String id);

/**
* Save or update the template info.
*
* @param templateData the template to save or update.
* @return the template save or updated.
*/
public TemplateInfo saveOrUpdate(TemplateInfo templateData);

/**
* Delete all the template info in the list.
*
* @param templateInfos list of template info to delete.
*/
public void delete(List<TemplateInfo> templateInfos);

/** @param templateData the template info to delete. */
public void delete(TemplateInfo templateData);

/** Deletes all the template info. */
public void deleteAll();

/**
* Add a listener.
*
* @param listener the listener to add.
*/
public void addTemplateListener(TemplateDAOListener listener);

/**
* Find a TemplateInfo from full name. By full name is meant the name of the template file
* preceded by the workspace name and the feature type name if defined for the template. Format
* for a full name is like the following: templateName or workspaceName:templateName or
* workspaceName:featureTypeName:templateName
*
* @param fullName the full name of the template.
* @return the corresponding TemplateInfo.
*/
public TemplateInfo findByFullName(String fullName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,332 @@
/* (c) 2021 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.configuration;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.stream.Collectors;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.CatalogException;
import org.geoserver.catalog.CatalogInfo;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.WorkspaceInfo;
import org.geoserver.catalog.event.CatalogAddEvent;
import org.geoserver.catalog.event.CatalogBeforeAddEvent;
import org.geoserver.catalog.event.CatalogListener;
import org.geoserver.catalog.event.CatalogModifyEvent;
import org.geoserver.catalog.event.CatalogPostModifyEvent;
import org.geoserver.catalog.event.CatalogRemoveEvent;
import org.geoserver.config.GeoServerDataDirectory;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.resource.Paths;
import org.geoserver.platform.resource.Resource;
import org.geoserver.security.PropertyFileWatcher;

/** A template info DAO that use a property file for persistence. */
public class TemplateInfoDAOImpl implements TemplateInfoDAO {

private SortedSet<TemplateInfo> templateDataSet;

private PropertyFileWatcher fileWatcher;

private GeoServerDataDirectory dd;

private Set<TemplateDAOListener> listeners;

private static final String PROPERTY_FILE_NAME = "features-templates-data.properties";

public TemplateInfoDAOImpl(GeoServerDataDirectory dd) {
this.dd = dd;
Resource templateDir = dd.get(TEMPLATE_DIR);
File dir = templateDir.dir();
if (!dir.exists()) dir.mkdir();
Resource prop = dd.get(Paths.path(TEMPLATE_DIR, PROPERTY_FILE_NAME));
prop.file();
this.fileWatcher = new PropertyFileWatcher(prop);
this.templateDataSet = Collections.synchronizedSortedSet(new TreeSet<>());
this.listeners = new HashSet<>();
Catalog catalog = (Catalog) GeoServerExtensions.bean("catalog");
catalog.addListener(new CatalogListenerTemplateInfo());
}

@Override
public List<TemplateInfo> findAll() {
reloadIfNeeded();
return new ArrayList<>(templateDataSet);
}

@Override
public TemplateInfo saveOrUpdate(TemplateInfo templateData) {
reloadIfNeeded();
boolean isUpdate =
templateDataSet
.stream()
.anyMatch(ti -> ti.getIdentifier().equals(templateData.getIdentifier()));
if (isUpdate) {
fireTemplateUpdateEvent(templateData);
templateDataSet.removeIf(ti -> ti.getIdentifier().equals(templateData.getIdentifier()));
}

templateDataSet.add(templateData);
storeProperties();
return templateData;
}

@Override
public void delete(TemplateInfo templateData) {
reloadIfNeeded();
templateDataSet.remove(templateData);
fireTemplateInfoRemoveEvent(templateData);
storeProperties();
}

@Override
public void delete(List<TemplateInfo> templateInfos) {
reloadIfNeeded();
templateDataSet.removeAll(templateInfos);
storeProperties();
for (TemplateInfo ti : templateInfos) fireTemplateInfoRemoveEvent(ti);
}

@Override
public void deleteAll() {
reloadIfNeeded();
Set<TemplateInfo> templateInfos = templateDataSet;
templateDataSet = Collections.synchronizedSortedSet(new TreeSet<>());
storeProperties();
for (TemplateInfo ti : templateInfos) fireTemplateInfoRemoveEvent(ti);
}

@Override
public TemplateInfo findById(String id) {
reloadIfNeeded();
Optional<TemplateInfo> optional =
templateDataSet.stream().filter(ti -> ti.getIdentifier().equals(id)).findFirst();
if (optional.isPresent()) return optional.get();
else return null;
}

@Override
public TemplateInfo findByFullName(String fullName) {
reloadIfNeeded();
Optional<TemplateInfo> templateInfo =
templateDataSet
.stream()
.filter(ti -> ti.getFullName().equals(fullName))
.findFirst();
if (templateInfo.isPresent()) return templateInfo.get();
return null;
}

@Override
public List<TemplateInfo> findByFeatureTypeInfo(FeatureTypeInfo featureTypeInfo) {
reloadIfNeeded();
String workspace = featureTypeInfo.getStore().getWorkspace().getName();
String name = featureTypeInfo.getName();
return templateDataSet
.stream()
.filter(
ti ->
(ti.getWorkspace() == null && ti.getFeatureType() == null)
|| ti.getFeatureType() == null
&& ti.getWorkspace().equals(workspace)
|| (ti.getWorkspace().equals(workspace)
&& ti.getFeatureType().equals(name)))
.collect(Collectors.toList());
}

private void fireTemplateUpdateEvent(TemplateInfo templateInfo) {
for (TemplateDAOListener listener : listeners) {
listener.handleUpdateEvent(new TemplateInfoEvent(templateInfo));
}
}

private void fireTemplateInfoRemoveEvent(TemplateInfo templateInfo) {
for (TemplateDAOListener listener : listeners) {
listener.handleDeleteEvent(new TemplateInfoEvent(templateInfo));
}
}

@Override
public void addTemplateListener(TemplateDAOListener listener) {
this.listeners.add(listener);
}

private TemplateInfo parseProperty(String key, String value) {
TemplateInfo templateData = new TemplateInfo();
templateData.setIdentifier(key);
String[] values = value.split(";");
for (String v : values) {
String[] attribute = v.split("=");
String attrName = attribute[0];
String attrValue = attribute[1];
if (attrName.equals("templateName")) templateData.setTemplateName(attrValue);
else if (attrName.equals("extension")) templateData.setExtension(attrValue);
else if (attrName.equals("workspace")) templateData.setWorkspace(attrValue);
else if (attrName.equals("featureTypeInfo")) templateData.setFeatureType(attrValue);
}
templateData.setIdentifier(key);
return templateData;
}

private Properties toProperties() {
Properties properties = new Properties();
for (TemplateInfo td : templateDataSet) {
StringBuilder sb = new StringBuilder();
sb.append("templateName=")
.append(td.getTemplateName())
.append(";extension=")
.append(td.getExtension());
String ws = td.getWorkspace();
if (ws != null) sb.append(";workspace=").append(td.getWorkspace());
String fti = td.getFeatureType();
if (fti != null) sb.append(";featureTypeInfo=").append(td.getFeatureType());
properties.put(td.getIdentifier(), sb.toString());
}
return properties;
}

public void storeProperties() {
Properties p = toProperties();
Resource propFile = dd.get(Paths.path(TEMPLATE_DIR, PROPERTY_FILE_NAME));
try (OutputStream os = propFile.out()) {
p.store(os, null);
} catch (Exception e) {
throw new RuntimeException("Could not write rules to " + PROPERTY_FILE_NAME);
}
}

private boolean isModified() {
return fileWatcher != null && fileWatcher.isStale();
}

private void loadTemplateInfo() {
try {
Properties properties = fileWatcher.getProperties();
this.templateDataSet = Collections.synchronizedSortedSet(new TreeSet<>());
for (Object k : properties.keySet()) {
TemplateInfo td = parseProperty(k.toString(), properties.getProperty(k.toString()));
this.templateDataSet.add(td);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private void reloadIfNeeded() {
if (isModified() || templateDataSet.isEmpty()) loadTemplateInfo();
}

public static class CatalogListenerTemplateInfo implements CatalogListener {
@Override
public void handlePreAddEvent(CatalogBeforeAddEvent event) throws CatalogException {}

@Override
public void handleAddEvent(CatalogAddEvent event) throws CatalogException {}

@Override
public void handleRemoveEvent(CatalogRemoveEvent event) throws CatalogException {
CatalogInfo source = event.getSource();
if (source instanceof FeatureTypeInfo) {
removeFtTemplates((FeatureTypeInfo) source);

} else if (source instanceof WorkspaceInfo) {
removeWSTemplates((WorkspaceInfo) source);
}
}

private void removeFtTemplates(FeatureTypeInfo ft) {
TemplateInfoDAO dao = TemplateInfoDAO.get();
List<TemplateInfo> templateInfos = dao.findByFeatureTypeInfo(ft);
dao.delete(
templateInfos
.stream()
.filter(ti -> ti.getFeatureType() != null)
.collect(Collectors.toList()));
}

private void removeWSTemplates(WorkspaceInfo ws) {
TemplateInfoDAO dao = TemplateInfoDAO.get();
List<TemplateInfo> templateInfos =
dao.findAll()
.stream()
.filter(ti -> ti.getWorkspace().equals(ws.getName()))
.collect(Collectors.toList());
dao.delete(templateInfos);
}

@Override
public void handleModifyEvent(CatalogModifyEvent event) throws CatalogException {
final CatalogInfo source = event.getSource();
if (source instanceof FeatureTypeInfo) {
int nameIdx = event.getPropertyNames().indexOf("name");
if (nameIdx != -1) {
String newName = (String) event.getNewValues().get(nameIdx);
updateTemplateInfoLayerName((FeatureTypeInfo) source, newName);
}
} else if (source instanceof WorkspaceInfo) {
int nameIdx = event.getPropertyNames().indexOf("name");
if (nameIdx != -1) {
String oldName = (String) event.getOldValues().get(nameIdx);
String newName = (String) event.getNewValues().get(nameIdx);
updateWorkspaceNames(oldName, newName);
}
}
}

private void updateTemplateInfoLayerName(FeatureTypeInfo fti, String newName) {
TemplateInfoDAO dao = TemplateInfoDAO.get();
List<TemplateInfo> templateInfo = dao.findByFeatureTypeInfo(fti);
for (TemplateInfo ti : templateInfo) {
ti.setFeatureType(newName);
}
((TemplateInfoDAOImpl) dao).storeProperties();
}

private void updateTemplateInfoWorkspace(WorkspaceInfo wi, FeatureTypeInfo fti) {
TemplateInfoDAO dao = TemplateInfoDAO.get();
List<TemplateInfo> templateInfo = dao.findByFeatureTypeInfo(fti);
for (TemplateInfo ti : templateInfo) {
ti.setWorkspace(wi.getName());
}
((TemplateInfoDAOImpl) dao).storeProperties();
}

private void updateWorkspaceNames(String oldName, String newName) {
TemplateInfoDAO dao = TemplateInfoDAO.get();
List<TemplateInfo> infos = dao.findAll();
for (TemplateInfo ti : infos) {
if (ti.getWorkspace().equals(oldName)) ti.setWorkspace(newName);
}
((TemplateInfoDAOImpl) dao).storeProperties();
}

@Override
public void handlePostModifyEvent(CatalogPostModifyEvent event) throws CatalogException {
CatalogInfo source = event.getSource();
if (source instanceof FeatureTypeInfo) {
FeatureTypeInfo info = (FeatureTypeInfo) source;
int wsIdx = event.getPropertyNames().indexOf("workspace");
if (wsIdx != -1) {
WorkspaceInfo newWorkspace = (WorkspaceInfo) event.getNewValues().get(wsIdx);
updateTemplateInfoWorkspace(newWorkspace, info);
}
}
}

@Override
public void reloaded() {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* (c) 2021 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.configuration;

/** A TemplateInfo event. Simply hold the TemplateInfo object affected by the event. */
public class TemplateInfoEvent {

private TemplateInfo ti;

public TemplateInfoEvent(TemplateInfo templateInfo) {
this.ti = templateInfo;
}

public TemplateInfo getSource() {
return ti;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* (c) 2021 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.configuration;

import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

/**
* A class that holds a list of {@link TemplateRule}. Is meant to be stored in the FeatureTypeInfo
* metadata map.
*/
@XmlRootElement(name = "TemplateLayerConfig")
public class TemplateLayerConfig implements Serializable {

public static final String METADATA_KEY = "FEATURES_TEMPLATING_LAYER_CONF";

@XmlElement(name = "Rule")
private Set<TemplateRule> templateRules;

public TemplateLayerConfig(Set<TemplateRule> templateRules) {
this.templateRules = templateRules;
}

public TemplateLayerConfig() {
templateRules = new HashSet<>();
}

public void addTemplateRule(TemplateRule rule) {
if (this.templateRules == null) templateRules = new HashSet<>();
this.templateRules.add(rule);
}

public Set<TemplateRule> getTemplateRules() {
if (this.templateRules == null) this.templateRules = new HashSet<>();
return templateRules;
}

public void setTemplateRules(Set<TemplateRule> templateRules) {
this.templateRules = templateRules;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* (c) 2021 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.configuration;

import org.geoserver.config.util.XStreamPersister;
import org.geoserver.config.util.XStreamPersisterInitializer;

/** XStreamPersisterInitializer for TemplateLayerConfig class and TemplateRule list. */
public class TemplateLayerConfigXStreamPersisterInitializer implements XStreamPersisterInitializer {

@Override
public void init(XStreamPersister persister) {
persister.getXStream().alias("Rule", TemplateRule.class);
persister.getXStream().alias("TemplateLayerConfig", TemplateLayerConfig.class);
persister.registerBreifMapComplexType("TemplateRuleType", TemplateRule.class);
persister.registerBreifMapComplexType("LayerConfigType", TemplateLayerConfig.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
/* (c) 2019 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.configuration;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.eclipse.emf.common.util.URI;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.config.GeoServerDataDirectory;
import org.geoserver.featurestemplating.builders.impl.RootBuilder;
import org.geoserver.featurestemplating.builders.visitors.SimplifiedPropertyReplacer;
import org.geoserver.featurestemplating.readers.TemplateReaderConfiguration;
import org.geoserver.featurestemplating.validation.TemplateValidator;
import org.geoserver.ows.Dispatcher;
import org.geoserver.ows.Request;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.resource.Resource;
import org.geotools.data.complex.AppSchemaDataAccessRegistry;
import org.geotools.data.complex.DataAccessRegistry;
import org.geotools.data.complex.FeatureTypeMapping;
import org.geotools.data.complex.feature.type.ComplexFeatureTypeImpl;
import org.geotools.data.complex.feature.type.Types;
import org.opengis.feature.type.FeatureType;
import org.xml.sax.helpers.NamespaceSupport;

/** Manage the cache and the retrieving for all templates files */
public class TemplateLoader {

private final LoadingCache<CacheKey, Template> templateCache;
private GeoServerDataDirectory dataDirectory;

public TemplateLoader(GeoServerDataDirectory dd) {
this.dataDirectory = dd;
templateCache =
CacheBuilder.newBuilder()
.maximumSize(100)
.initialCapacity(1)
.expireAfterAccess(120, TimeUnit.MINUTES)
.build(new TemplateCacheLoader());
}

/**
* Get the template related to the featureType. If template has been modified updates the cache
* with the new Template
*/
public RootBuilder getTemplate(FeatureTypeInfo typeInfo, String outputFormat)
throws ExecutionException {
String templateIdentifier = evaluatesTemplateRule(typeInfo);
if (templateIdentifier == null)
templateIdentifier = TemplateIdentifier.fromOutputFormat(outputFormat).getFilename();
CacheKey key = new CacheKey(typeInfo, templateIdentifier);
Template template = templateCache.get(key);
boolean updateCache = false;
if (template.checkTemplate()) updateCache = true;

RootBuilder root = template.getRootBuilder();

if (updateCache) {
replaceSimplifiedPropertiesIfNeeded(key.getResource(), template.getRootBuilder());
templateCache.put(key, template);
}

if (root != null) {
TemplateValidator validator = new TemplateValidator(typeInfo);
boolean isValid = validator.validateTemplate(root);
if (!isValid) {
throw new RuntimeException(
"Failed to validate template for feature type "
+ typeInfo.getName()
+ ". Failing attribute is "
+ URI.decode(validator.getFailingAttribute()));
}
}
return root;
}

/**
* Extract Namespaces from given FeatureType
*
* @return Namespaces if found for the given FeatureType
*/
private NamespaceSupport declareNamespaces(FeatureType type) {
NamespaceSupport namespaceSupport = null;
if (type instanceof ComplexFeatureTypeImpl) {
Map namespaces = (Map) type.getUserData().get(Types.DECLARED_NAMESPACES_MAP);
if (namespaces != null) {
namespaceSupport = new NamespaceSupport();
for (Iterator it = namespaces.entrySet().iterator(); it.hasNext(); ) {
Map.Entry entry = (Map.Entry) it.next();
String prefix = (String) entry.getKey();
String namespace = (String) entry.getValue();
namespaceSupport.declarePrefix(prefix, namespace);
}
}
}
return namespaceSupport;
}

GeoServerDataDirectory getDataDirectory() {
return dataDirectory;
}

private class CacheKey {
private FeatureTypeInfo resource;
private String templateIdentifier;

public CacheKey(FeatureTypeInfo resource, String templateIdentifier) {
this.resource = resource;
this.templateIdentifier = templateIdentifier;
}

public FeatureTypeInfo getResource() {
return resource;
}

public String getTemplateIdentifier() {
return templateIdentifier;
}

@Override
public boolean equals(Object o) {
if (!(o instanceof CacheKey)) return false;
CacheKey other = (CacheKey) o;
if (!other.getTemplateIdentifier().equals(templateIdentifier)) return false;
else if (!(other.getResource().getName().equals(resource.getName()))) return false;
else if (!(other.getResource().getNamespace().equals(resource.getNamespace())))
return false;
return true;
}

@Override
public int hashCode() {
return Objects.hash(resource, templateIdentifier);
}
}

private void replaceSimplifiedPropertiesIfNeeded(
FeatureTypeInfo featureTypeInfo, RootBuilder rootBuilder) {
try {
if (featureTypeInfo.getFeatureType() instanceof ComplexFeatureTypeImpl
&& rootBuilder != null) {

DataAccessRegistry registry = AppSchemaDataAccessRegistry.getInstance();
FeatureTypeMapping featureTypeMapping =
registry.mappingByElement(featureTypeInfo.getQualifiedNativeName());
if (featureTypeMapping != null) {
SimplifiedPropertyReplacer visitor =
new SimplifiedPropertyReplacer(featureTypeMapping);
rootBuilder.accept(visitor, null);
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}

// evaluates the template rule associated to the featureTypeInfo and return the TemplateInfo id.
private String evaluatesTemplateRule(FeatureTypeInfo featureTypeInfo) {
List<TemplateRule> matching = new ArrayList<>();
TemplateLayerConfig config =
featureTypeInfo
.getMetadata()
.get(TemplateLayerConfig.METADATA_KEY, TemplateLayerConfig.class);
if (config == null || config.getTemplateRules().isEmpty()) return null;
else {
Set<TemplateRule> rules = config.getTemplateRules();
Request request = Dispatcher.REQUEST.get();
for (TemplateRule r : rules) {
if (r.applyRule(request)) matching.add(r);
}
}
int size = matching.size();
if (size > 0) {
if (size > 1) {
TemplateRule.TemplateRuleComparator comparator =
new TemplateRule.TemplateRuleComparator();
matching.sort(comparator);
}
return matching.get(0).getTemplateIdentifier();
}

return null;
}

/**
* Remove from the cache the entry with the specified identifier and Feature Type
*
* @param fti the FeatureType to which is associated the entry.
* @param templateIdentifier the templateIdentifier of the cached template.
*/
public void cleanCache(FeatureTypeInfo fti, String templateIdentifier) {
CacheKey key = new CacheKey(fti, templateIdentifier);
if (templateCache.getIfPresent(key) != null) this.templateCache.invalidate(key);
}

/**
* Remove all the cached entries with the specified templateIdentifier.
*
* @param templateIdentifier the templateIdentifier used to identify the cache entries to
* remove.
*/
public void removeAllWithIdentifier(String templateIdentifier) {
Set<CacheKey> keys = templateCache.asMap().keySet();
for (CacheKey key : keys) {
if (key.getTemplateIdentifier().equals(templateIdentifier)) {
templateCache.invalidate(key);
}
}
}

private TemplateFileManager getTemplateFileManager() {
return TemplateFileManager.get();
}

private class TemplateCacheLoader extends CacheLoader<CacheKey, Template> {
@Override
public Template load(CacheKey key) {
NamespaceSupport namespaces = null;
try {
FeatureType type = key.getResource().getFeatureType();
namespaces = declareNamespaces(type);
} catch (IOException e) {
throw new RuntimeException(
"Error retrieving FeatureType "
+ key.getResource().getName()
+ "Exception is: "
+ e.getMessage());
}
TemplateInfo templateInfo = TemplateInfoDAO.get().findById(key.getTemplateIdentifier());
Resource resource;
if (templateInfo != null)
resource = getTemplateFileManager().getTemplateResource(templateInfo);
else resource = getDataDirectory().get(key.getResource(), key.getTemplateIdentifier());
Template template = new Template(resource, new TemplateReaderConfiguration(namespaces));
RootBuilder builder = template.getRootBuilder();
if (builder != null) {
replaceSimplifiedPropertiesIfNeeded(key.getResource(), builder);
}
return template;
}
}

/** Invalidate all the cache entries. */
public void reset() {
templateCache.invalidateAll();
}

public static TemplateLoader get() {
return GeoServerExtensions.bean(TemplateLoader.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* (c) 2021 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.configuration;

import org.geoserver.config.impl.GeoServerLifecycleHandler;

/**
* Cleans the cache whenever the reset/reload config is triggered. Mostly useful for tests and REST
* automation, as the file watcher won't reload files checked less than a second ago.
*/
public class TemplateReloader implements GeoServerLifecycleHandler {

TemplateLoader loader;

public TemplateReloader(TemplateLoader configuration) {
this.loader = configuration;
}

@Override
public void onReset() {
loader.reset();
}

@Override
public void onDispose() {}

@Override
public void beforeReload() {}

@Override
public void onReload() {
loader.reset();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
/* (c) 2021 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.configuration;

import java.io.Serializable;
import java.util.Comparator;
import java.util.Objects;
import java.util.UUID;
import javax.xml.bind.annotation.XmlRootElement;
import org.geoserver.ows.Request;
import org.geoserver.util.XCQL;
import org.geotools.filter.text.cql2.CQLException;

/**
* A template rule associated to a FeatureTypeInfo. Its evaluation determines if a specific template
* should be applied for a Request.
*/
@XmlRootElement(name = "Rule")
public class TemplateRule implements Serializable {

private String ruleId;

private Integer priority;

private String templateIdentifier;

private String templateName;

private String outputFormat;

private String service;

private String cqlFilter;

private String profileFilter;

// use to force a rule to be applied regardless of priority
// currently used only from the preview mechanism in the web module.
private boolean forceRule;

public TemplateRule() {
this.priority = 0;
this.ruleId = UUID.randomUUID().toString();
}

public TemplateRule(TemplateRule rule) {
this.ruleId = rule.ruleId == null ? UUID.randomUUID().toString() : rule.ruleId;
this.priority = rule.priority;
this.outputFormat = rule.outputFormat;
this.cqlFilter = rule.cqlFilter;
this.service = rule.service;
this.forceRule = rule.forceRule;
this.templateName = rule.templateName;
this.templateIdentifier = rule.templateIdentifier;
this.profileFilter = rule.profileFilter;
}

public String getTemplateName() {
return templateName;
}

/**
* Apply the rule to the Request to see if it matches it.
*
* @param request the request against which evaluate the rule.
* @return
*/
public boolean applyRule(Request request) {
boolean result = true;
if (outputFormat != null) result = matchOutputFormat(getOutputFormat(request));

if (result && cqlFilter != null) {
result = evaluateCQLFilter(cqlFilter);
}
if (result && profileFilter != null) result = evaluateCQLFilter(profileFilter);

return result;
}

private boolean evaluateCQLFilter(String filter) {
try {
return XCQL.toFilter(filter).evaluate(null);
} catch (CQLException e) {
throw new RuntimeException(e);
}
}

public void setTemplateName(String templateName) {
this.templateName = templateName;
}

public SupportedFormat getOutputFormat() {
if (outputFormat != null) return SupportedFormat.valueOf(outputFormat);
return null;
}

public void setOutputFormat(SupportedFormat outputFormat) {
this.outputFormat = outputFormat.name();
}

public String getService() {
return service;
}

public void setService(String service) {
this.service = service;
}

public String getCqlFilter() {
return cqlFilter;
}

public void setCqlFilter(String cqlFilter) {
this.cqlFilter = cqlFilter;
}

public String getTemplateIdentifier() {
return templateIdentifier;
}

public void setTemplateIdentifier(String templateIdentifier) {
this.templateIdentifier = templateIdentifier;
}

private boolean matchOutputFormat(String outputFormat) {
TemplateIdentifier identifier = TemplateIdentifier.fromOutputFormat(outputFormat);
if (identifier == null) return false;
String nameIdentifier = identifier.name();
if (this.outputFormat.equals(SupportedFormat.GML.name()))
return nameIdentifier.startsWith(this.outputFormat);
else if (this.outputFormat.equals(SupportedFormat.GEOJSON.name()))
return nameIdentifier.equals(TemplateIdentifier.GEOJSON.name())
|| nameIdentifier.equals(TemplateIdentifier.JSON.name());
else if (this.outputFormat.equals(SupportedFormat.HTML.name()))
return nameIdentifier.equals(TemplateIdentifier.HTML.name());
else return nameIdentifier.equals(this.outputFormat);
}

public void setTemplateInfo(TemplateInfo templateInfo) {
if (templateInfo != null) {
this.templateName = templateInfo.getFullName();
this.templateIdentifier = templateInfo.getIdentifier();
}
}

/**
* Return the TemplateInfo to which this rule refers to.
*
* @return the TemplateInfo associated to the rule.
*/
public TemplateInfo getTemplateInfo() {
TemplateInfo ti = new TemplateInfo();
if (templateName != null && templateName.indexOf(":") != -1) {
String[] nameSplit = templateName.split(":");
if (nameSplit.length == 3) {
ti.setWorkspace(nameSplit[0]);
ti.setFeatureType(nameSplit[1]);
ti.setTemplateName(nameSplit[2]);
} else {
ti.setWorkspace(nameSplit[0]);
ti.setTemplateName(nameSplit[1]);
}
}
ti.setIdentifier(templateIdentifier);
return ti;
}

private String getOutputFormat(Request request) {
String outputFormat = request.getOutputFormat();
if (outputFormat == null)
outputFormat = request.getKvp() != null ? (String) request.getKvp().get("f") : null;
if (outputFormat == null)
outputFormat =
request.getKvp() != null ? (String) request.getKvp().get("INFO_FORMAT") : null;
return outputFormat;
}

public String getRuleId() {
return ruleId;
}

public void setRuleId(String ruleId) {
this.ruleId = ruleId;
}

public boolean isForceRule() {
return forceRule;
}

public void setForceRule(boolean forceRule) {
this.forceRule = forceRule;
}

public Integer getPriority() {
return priority;
}

public void setPriority(Integer priority) {
this.priority = priority;
}

public String getProfileFilter() {
return profileFilter;
}

public void setProfileFilter(String profileFilter) {
this.profileFilter = profileFilter;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TemplateRule that = (TemplateRule) o;
return Objects.equals(templateIdentifier, that.templateIdentifier)
&& Objects.equals(templateName, that.templateName)
&& Objects.equals(outputFormat, that.outputFormat)
&& Objects.equals(service, that.service)
&& Objects.equals(profileFilter, that.profileFilter)
&& Objects.equals(cqlFilter, that.cqlFilter)
&& Objects.equals(priority, that.priority);
}

@Override
public int hashCode() {
return Objects.hash(
templateIdentifier, templateName, outputFormat, service, cqlFilter, priority);
}

/**
* Rule comparator to sort the TemplateRules in order to get the one with higher priority or the
* one that is forced.
*/
public static class TemplateRuleComparator implements Comparator<TemplateRule> {

@Override
public int compare(TemplateRule o1, TemplateRule o2) {
int result;
if (o1.isForceRule()) result = -1;
else if (o2.isForceRule()) result = 1;
else {
int p1 = o1.getPriority();
int p2 = o2.getPriority();
if (p1 < p2) result = -1;
else if (p2 < p1) result = 1;
else result = 0;
}
return result;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/* (c) 2021 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.configuration;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.platform.GeoServerExtensions;

/** Class that provides methods to add, update or delete Template Rules */
public class TemplateRuleService {

private FeatureTypeInfo featureTypeInfo;

public TemplateRuleService(FeatureTypeInfo featureTypeInfo) {
this.featureTypeInfo = featureTypeInfo;
}

/**
* Remove the template rule having the specified id.
*
* @param ruleId the id of the rule to delete.
* @return true if the rule was deleted false if the rule was not found.
*/
public boolean removeRule(String ruleId) {
boolean result = false;
Set<TemplateRule> rules = getRules();
if (rules != null && !rules.isEmpty()) {
result = rules.removeIf(r -> r.getRuleId().equals(ruleId));
if (result) {
TemplateLayerConfig config = getTemplateLayerConfig();
config.setTemplateRules(rules);
featureTypeInfo.getMetadata().put(TemplateLayerConfig.METADATA_KEY, config);
getCatalog().save(featureTypeInfo);
}
}
return result;
}

/**
* Replace the rule with the one passed as an argument if they have the same id.
*
* @param rule the rule to use as a replacement for the one with same id.
*/
public void replaceRule(TemplateRule rule) {
Set<TemplateRule> rules = getRules();
if (rules != null) {
if (rules.removeIf((r -> r.getRuleId().equals(rule.getRuleId())))) {
Set<TemplateRule> ruleset = updatePriorities(new ArrayList<>(rules), rule);
TemplateLayerConfig config = getTemplateLayerConfig();
config.setTemplateRules(ruleset);
featureTypeInfo.getMetadata().put(TemplateLayerConfig.METADATA_KEY, config);
getCatalog().save(featureTypeInfo);
}
}
}

/**
* Save a rule.
*
* @param rule the rule to save.
*/
public void saveRule(TemplateRule rule) {
TemplateLayerConfig config = getTemplateLayerConfig();
if (config == null) config = new TemplateLayerConfig();
Set<TemplateRule> rules = config.getTemplateRules();
Set<TemplateRule> ruleset = updatePriorities(new ArrayList<>(rules), rule);
config.setTemplateRules(ruleset);
featureTypeInfo.getMetadata().put(TemplateLayerConfig.METADATA_KEY, config);
getCatalog().save(featureTypeInfo);
}

private TemplateLayerConfig getTemplateLayerConfig() {
return featureTypeInfo
.getMetadata()
.get(TemplateLayerConfig.METADATA_KEY, TemplateLayerConfig.class);
}

public Set<TemplateRule> getRules() {
TemplateLayerConfig layerConfig = getTemplateLayerConfig();
if (layerConfig != null) return layerConfig.getTemplateRules();
return Collections.emptySet();
}

/**
* Get the template rule having the specified id.
*
* @param ruleId the id of the rule to find.
* @return the rule or null if not found.
*/
public TemplateRule getRule(String ruleId) {
Set<TemplateRule> rules = getRules();
if (rules != null && !rules.isEmpty()) {
Optional<TemplateRule> opRule =
rules.stream().filter(r -> r.getRuleId().equals(ruleId)).findFirst();
if (opRule.isPresent()) return opRule.get();
}
return null;
}

private Catalog getCatalog() {
return (Catalog) GeoServerExtensions.bean("catalog");
}

/**
* Update the priorities of the existing rules based on the priority of the new Rule. It shift
* by one position the priority value of the rules having it >= the priority value of the new
* Rule.
*
* @param rules the already existing rule.
* @param newRule the new Rule to be added.
* @return a Set of rules having the priority fields updated.
*/
public static Set<TemplateRule> updatePriorities(
List<TemplateRule> rules, TemplateRule newRule) {
Set<TemplateRule> set = new HashSet<>(rules.size());
int updatedPriority = newRule.getPriority();
boolean newRuleAdded = false;
for (TemplateRule rule : rules) {
boolean isUpdating = rule.getRuleId().equals(newRule.getRuleId());
int priority = rule.getPriority();
if (priority == updatedPriority) {
if (!newRuleAdded) {
set.add(newRule);
newRuleAdded = true;
}
priority++;
if (!isUpdating) {
rule.setPriority(priority);
updatedPriority = priority;
}
}
if (!isUpdating) set.add(rule);
}
if (set.isEmpty() || !newRuleAdded) set.add(newRule);
return set;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* (c) 2021 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.configuration;

/**
* This class provides the business logic to handle save, update and delete operation on a template.
*/
public class TemplateService {

private TemplateLoader loader;
private TemplateFileManager templateFileManager;
private TemplateInfoDAO templateInfoDAO;

public TemplateService() {
this.loader = TemplateLoader.get();
this.templateFileManager = TemplateFileManager.get();
this.templateInfoDAO = TemplateInfoDAO.get();
}

/**
* Save or update a Template. Clean the cache from the previous template if necessary.
*
* @param templateInfo the Template Info to save or update.
* @param rawTemplate the raw Template to save or update.
*/
public void saveOrUpdate(TemplateInfo templateInfo, String rawTemplate) {
templateFileManager.saveTemplateFile(templateInfo, rawTemplate);
TemplateInfo current = templateInfoDAO.findById(templateInfo.getIdentifier());
if (current != null && !current.getFullName().equals(templateInfo.getFullName())) {
if (templateFileManager.delete(current)) {
loader.removeAllWithIdentifier(templateInfo.getIdentifier());
}
}
templateInfoDAO.saveOrUpdate(templateInfo);
}

/**
* Delete a Template and removes it from the cache.
*
* @param templateInfo the Template Info object to delete.
*/
public void delete(TemplateInfo templateInfo) {
templateFileManager.delete(templateInfo);
loader.removeAllWithIdentifier(templateInfo.getIdentifier());
templateInfoDAO.delete(templateInfo);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
*/
package org.geoserver.featurestemplating.configuration;

import static org.geoserver.platform.resource.Resource.Type.RESOURCE;

import java.io.IOException;
import java.io.InputStream;
import org.apache.commons.io.FilenameUtils;
Expand All @@ -27,15 +29,40 @@ public TemplateWatcher(Resource resource, TemplateReaderConfiguration configurat
this.configuration = configuration;
}

/**
* Reads the file updating the last check timestamp.
*
* <p>Subclasses can override {@link #parseFileContents(InputStream)} to do something when the
* file is read.
*
* @return parsed file contents
*/
public RootBuilder read() throws IOException {
RootBuilder result = null;

if (resource.getType() == RESOURCE) {

try (InputStream is = resource.in()) {
result = parseResource(resource);

lastModified = resource.lastmodified();
lastCheck = System.currentTimeMillis();
stale = false;
}
}

return result;
}

/**
* Parse template file and return a builder tree as a {@link RootBuilder}
*
* @return builderTree as a RootBuilder
*/
@Override
public RootBuilder parseFileContents(InputStream in) throws IOException {
public RootBuilder parseResource(Resource resource) throws IOException {
String extension = FilenameUtils.getExtension(resource.name());
TemplateReader reader = TemplateReaderProvider.findReader(extension, in, configuration);
TemplateReader reader =
TemplateReaderProvider.findReader(extension, resource, configuration);
return reader.getRootBuilder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.geoserver.featurestemplating.expressions;

import static org.geotools.filter.capability.FunctionNameImpl.parameter;

import org.geoserver.ows.Request;
import org.geotools.filter.capability.FunctionNameImpl;
import org.geotools.util.Converters;
import org.opengis.filter.capability.FunctionName;

/** Returns the value of request header with the name specified in the parameter. */
public class HeaderFunction extends RequestFunction {

public static FunctionName NAME =
new FunctionNameImpl(
"header", parameter("result", String.class), parameter("name", String.class));

public HeaderFunction() {
super(NAME);
}

@Override
protected Object evaluateInternal(Request request, Object object) {
String parameter = getParameters().get(0).evaluate(null, String.class);
Object value = request.getHttpRequest().getHeader(parameter);
return Converters.convert(value, String.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* (c) 2021 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.expressions;

import static org.geotools.filter.capability.FunctionNameImpl.parameter;

import java.lang.reflect.Array;
import org.geotools.filter.FunctionExpressionImpl;
import org.geotools.filter.capability.FunctionNameImpl;
import org.opengis.feature.Attribute;
import org.opengis.filter.capability.FunctionName;

/**
* Allows extraction of a given item from an array (as it's hard to do with a xpath, since the array
* value is not quite the same as having an attribute with multiple repetitions)
*/
public class ItemFunction extends FunctionExpressionImpl {
public static FunctionName NAME =
new FunctionNameImpl(
"item",
Object.class,
parameter("array", Object.class),
parameter("idx", Integer.class));

public ItemFunction() {
super(NAME);
}

@Override
public Object evaluate(Object feature) {
Object array = getExpression(0).evaluate(feature, Object.class);
if (array instanceof Attribute) {
array = ((Attribute) array).getValue();
}
Integer idx = getExpression(1).evaluate(feature, Integer.class);

if (array == null) return null;
if (!array.getClass().isArray())
throw new IllegalArgumentException("First argument is not an array");

if (idx == null) return null;

return Array.get(array, idx);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* (c) 2021 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.expressions;

import static org.geotools.filter.capability.FunctionNameImpl.parameter;

import org.geoserver.ows.Request;
import org.geotools.filter.capability.FunctionNameImpl;
import org.opengis.filter.capability.FunctionName;

/** Returns the Mime Type of the current {@link Request}. */
public class MimeTypeFunction extends RequestFunction {

public static FunctionName NAME =
new FunctionNameImpl("mimeType", parameter("result", String.class));

public MimeTypeFunction() {
super(NAME);
}

@Override
protected Object evaluateInternal(Request request, Object object) {
String outputFormat = request.getOutputFormat();
if (outputFormat == null) {
outputFormat = request.getKvp() != null ? (String) request.getKvp().get("f") : null;
}
return outputFormat;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.geoserver.featurestemplating.expressions;

import static org.geotools.filter.capability.FunctionNameImpl.parameter;

import org.geotools.filter.AttributeExpressionImpl;
import org.geotools.filter.FunctionExpressionImpl;
import org.geotools.filter.capability.FunctionNameImpl;
import org.opengis.filter.capability.FunctionName;
import org.opengis.filter.expression.ExpressionVisitor;
import org.opengis.filter.expression.PropertyName;
import org.xml.sax.helpers.NamespaceSupport;

public class PropertyPathFunction extends FunctionExpressionImpl implements PropertyName {

public static FunctionName NAME =
new FunctionNameImpl(
"propertyPath",
parameter("result", Object.class),
parameter("domainProperty", String.class));

protected String propertyPath;

protected NamespaceSupport namespaceSupport;

public PropertyPathFunction(String propertyPath) {
super(NAME);
this.propertyPath = propertyPath;
}

public PropertyPathFunction() {
super(NAME);
}

@Override
public Object evaluate(Object object) {
String strPropertyPath = (String) getParameters().get(0).evaluate(object);
AttributeExpressionImpl attributeExpression =
new AttributeExpressionImpl(strPropertyPath, namespaceSupport);
return attributeExpression.evaluate(object);
}

@Override
public String getPropertyName() {
if (getParameters() != null && getParameters().size() > 0)
return getParameters().get(0).evaluate(null, String.class);
else return propertyPath;
}

public void setPropertyName(String propertyPath) {
this.propertyPath = propertyPath;
}

@Override
public NamespaceSupport getNamespaceContext() {
return namespaceSupport;
}

public void setNamespaceContext(NamespaceSupport namespaceContext) {
this.namespaceSupport = namespaceContext;
}

@Override
public Object accept(ExpressionVisitor visitor, Object extraData) {
// we explicitly handle the attribute extractor filter
return visitor.visit((PropertyName) this, extraData);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* (c) 2021 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.expressions;

import java.util.logging.Logger;
import org.geoserver.ows.Dispatcher;
import org.geoserver.ows.Request;
import org.geotools.filter.FunctionExpressionImpl;
import org.geotools.util.logging.Logging;
import org.opengis.filter.capability.FunctionName;

/**
* Abstract function that evaluate against a {@link Request} object. Subclasses must implement
* evaluate internal.
*/
public abstract class RequestFunction extends FunctionExpressionImpl {

private static final Logger LOGGER = Logging.getLogger(RequestFunction.class);

protected RequestFunction(FunctionName functionName) {
super(functionName);
}

@Override
public Object evaluate(Object object) {
Request request = Dispatcher.REQUEST.get();
if (request == null) {
LOGGER.info("Found a null Request object. Returning null");
return null;
} else return evaluateInternal(request, object);
}

protected abstract Object evaluateInternal(Request request, Object object);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* (c) 2021 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.expressions;

import static org.geotools.filter.capability.FunctionNameImpl.parameter;

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.geoserver.ows.Request;
import org.geoserver.ows.util.ResponseUtils;
import org.geotools.filter.capability.FunctionNameImpl;
import org.opengis.filter.capability.FunctionName;

/** Check if the current {@link Request} matches the regex passed as an argument of the Function. */
public class RequestMatchRegex extends RequestFunction {

public static FunctionName NAME =
new FunctionNameImpl(
"requestMatchRegex",
parameter("result", Boolean.class),
parameter("regex", String.class));

public RequestMatchRegex() {
super(NAME);
}

@Override
protected Object evaluateInternal(Request request, Object object) {
String regex = getParameters().get(0).evaluate(null, String.class);
Pattern pattern = Pattern.compile(regex);
String url = getFullURL(request.getHttpRequest());
Matcher matcher = pattern.matcher(url);
return matcher.matches();
}

private String getFullURL(HttpServletRequest request) {
StringBuilder requestURL = new StringBuilder(ResponseUtils.baseURL(request));
String pathInfo = request.getPathInfo();
String queryString = request.getQueryString();
if (pathInfo != null) {
if (pathInfo.startsWith("/")) pathInfo = pathInfo.substring(1);
requestURL.append(pathInfo);
}
if (queryString != null) {
requestURL.append("?").append(queryString);
}
return requestURL.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* (c) 2021 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.featurestemplating.expressions;

import static org.geotools.filter.capability.FunctionNameImpl.parameter;

import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.geoserver.ows.Request;
import org.geotools.filter.capability.FunctionNameImpl;
import org.geotools.util.Converters;
import org.opengis.filter.capability.FunctionName;

/** Returns the value of the request parameter with the name specified in the function parameter. */
public class RequestParameterFunction extends RequestFunction {

public static FunctionName NAME =
new FunctionNameImpl(
"requestParam",
parameter("result", String.class),
parameter("name", String.class));

public RequestParameterFunction() {
super(NAME);
}

@Override
protected Object evaluateInternal(Request request, Object object) {
String parameter = getParameters().get(0).evaluate(null, String.class);
HttpServletRequest req = request.getHttpRequest();
Object value = null;
if (req != null) {
value = req.getParameter(parameter);
}
Map<String, Object> rawKvp = request.getRawKvp();
if (rawKvp != null && value == null) value = request.getRawKvp().get(parameter);

return Converters.convert(value, String.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,21 @@ public TemplateCQLManager(String strCql, NamespaceSupport namespaces) {

public static final String XPATH_FUN_START = "xpath(";

public static final String PROPERTY_FUN_START = "propertyPath(";

/**
* Create a PropertyName from a String
*
* @return
*/
public AttributeExpressionImpl getAttributeExpressionFromString() {
String strXpath = extractXpath(this.strCql);
this.contextPos = determineContextPos(strXpath);
strXpath = removeBackDots(strXpath);
return new AttributeExpressionImpl(strXpath, namespaces);
String property = extractProperty(this.strCql);
if (property.indexOf(".") != -1 && property.indexOf("/") == -1)
property = replaceDotSeparatorWithSlash(property);
this.contextPos = determineContextPos(property);
property = removeBackDots(property);
if (property.indexOf(".") != -1) property = property.replaceAll("\\.", "/");
return new AttributeExpressionImpl(property, namespaces);
}

/**
Expand All @@ -65,15 +70,25 @@ public AttributeExpressionImpl getAttributeExpressionFromString() {
* @return
*/
public Expression getExpressionFromString() {
// TODO XPath and PropertyPath function are used here as pattern to recognize strings
// in filter to be managed and converted to real PropertyName.
// This is due the reference to previous context through ../ that is handled in the
// DynamicValueBuilder
// and due backwards mapping integration with OGCApi filter handling. This should be
// changed the two function should be used as function and be able to evaluate the reference
// to previous context avoiding this string manipulation that is done here.

// takes xpath fun from cql
String strXpathFun = extractXpathFromCQL(this.strCql);
boolean isXpath = strXpathFun.indexOf(XPATH_FUN_START) != -1;
if (isXpath) this.contextPos = determineContextPos(strXpathFun);
String strPropertyNameFun = extractPropertyNameFunction(this.strCql);
String propertyWithSlash = replaceDotSeparatorWithSlashInFunction(strPropertyNameFun);
if (containsAPropertyNameFunction(propertyWithSlash))
this.contextPos = determineContextPos(propertyWithSlash);
// takes the literal argument of xpathFun
String literalXpath = removeBackDots(strXpathFun);
String literalXpath = removeBackDots(propertyWithSlash);

// clean the function to obtain a cql expression without xpath() syntax
Expression cql = extractCqlExpressions(cleanCQL(this.strCql, strXpathFun, literalXpath));
Expression cql =
extractCqlExpressions(cleanCQL(this.strCql, strPropertyNameFun, literalXpath));
TemplatingExpressionVisitor visitor = new TemplatingExpressionVisitor();
cql.accept(visitor, null);
return cql;
Expand All @@ -86,11 +101,12 @@ public Expression getExpressionFromString() {
* @throws CQLException
*/
public Filter getFilterFromString() throws CQLException {
String xpathFunction = extractXpathFromCQL(this.strCql);
if (xpathFunction.indexOf(XPATH_FUN_START) != -1)
contextPos = determineContextPos(xpathFunction);
String literalXpath = removeBackDots(xpathFunction);
String cleanedCql = cleanCQL(this.strCql, xpathFunction, literalXpath);
String propertyNameFun = extractPropertyNameFunction(this.strCql);
String propertyWithSlash = replaceDotSeparatorWithSlashInFunction(propertyNameFun);
if (containsAPropertyNameFunction(propertyWithSlash))
contextPos = determineContextPos(propertyWithSlash);
String literalPn = removeBackDots(propertyWithSlash);
String cleanedCql = cleanCQL(this.strCql, propertyNameFun, literalPn);
Filter templateFilter = XCQL.toFilter(cleanedCql);
TemplatingExpressionVisitor visitor = new TemplatingExpressionVisitor();
return (Filter) templateFilter.accept(visitor, null);
Expand Down Expand Up @@ -123,13 +139,13 @@ public Object visit(Literal expression, Object extraData) {
return result;
}

private String extractXpath(String xpath) {
private String extractProperty(String property) {
boolean inXpath = false;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < xpath.length(); i++) {
final char curr = xpath.charAt(i);
final boolean isLast = (i == xpath.length() - 1);
final char next = isLast ? 0 : xpath.charAt(i + 1);
for (int i = 0; i < property.length(); i++) {
final char curr = property.charAt(i);
final boolean isLast = (i == property.length() - 1);
final char next = isLast ? 0 : property.charAt(i + 1);

if (curr == '\\') {
if (isLast)
Expand Down Expand Up @@ -277,26 +293,14 @@ private Expression extractCqlExpressions(String expression) {
return catenateExpressions(splitCqlExpressions(expression));
}

public static String removeQuotes(String cqlFilter) {
cqlFilter = cqlFilter.replaceFirst("\"", "");
StringBuilder strBuilder = new StringBuilder();
for (int i = 0; i < cqlFilter.length(); i++) {
char curr = cqlFilter.charAt(i);
if (curr != '\"') {
strBuilder.append(curr);
}
}
return strBuilder.toString();
}

/**
* Clean a CQL from the xpath function syntax to make the xpath suitable to be encoded as a
* PropertyName
*/
private String cleanCQL(String cql, String toReplace, String replacement) {
if (cql.indexOf(XPATH_FUN_START) != -1)
return cql.replace(toReplace, replacement).replaceAll("\\.\\./", "");
else return cql;
if (containsAPropertyNameFunction(cql))
cql = cql.replace(toReplace, replacement).replaceAll("\\.\\./", "");
return cql;
}

public static String removeBackDots(String xpath) {
Expand All @@ -305,12 +309,13 @@ public static String removeBackDots(String xpath) {
}

/** Extract the xpath function from CQL Expression if present */
private String extractXpathFromCQL(String expression) {
int xpathI = expression.indexOf(XPATH_FUN_START);
if (xpathI != -1) {
int xpathI2 = expression.indexOf(")", xpathI);
String strXpath = expression.substring(xpathI, xpathI2 + 1);
return strXpath;
private String extractPropertyNameFunction(String expression) {
int propertyI = expression.indexOf(XPATH_FUN_START);
if (propertyI == -1) propertyI = expression.indexOf(PROPERTY_FUN_START);
if (propertyI != -1) {
int propertyI2 = expression.indexOf(")", propertyI);
String strProperty = expression.substring(propertyI, propertyI2 + 1);
return strProperty;
}
return expression;
}
Expand Down Expand Up @@ -366,6 +371,10 @@ public static String quoteXpathAttribute(String xpath) {
return xpath;
}

public Expression getThis() {
return ff.function("xpath", ff.literal("."));
}

/** Can be used to force namespace support into parsed CQL expressions */
private final class TemplatingExpressionVisitor extends DuplicatingFilterVisitor {

Expand All @@ -379,4 +388,40 @@ public Object visit(PropertyName expression, Object extraData) {
return getFactory(extraData).property(expression.getPropertyName(), namespaces);
}
}

private String replaceDotSeparatorWithSlashInFunction(String propertyName) {
if (propertyName.indexOf(PROPERTY_FUN_START) != -1
&& propertyName.indexOf(".") != -1
&& propertyName.indexOf("/") == -1) {
propertyName = propertyName.replaceAll(" ", "");
int startPath =
propertyName.indexOf(PROPERTY_FUN_START) + (PROPERTY_FUN_START + "'").length();
int endPath = propertyName.lastIndexOf("')");
String content = propertyName.substring(startPath, endPath);
String replaced = replaceDotSeparatorWithSlash(content);
propertyName = propertyName.replace(content, replaced);
}
return propertyName;
}

private String replaceDotSeparatorWithSlash(String propertyName) {
char[] chars = propertyName.toCharArray();
StringBuilder sb = new StringBuilder("");
for (int i = 0; i < chars.length; i++) {
char current = chars[i];
char prev = ' ';
char next = ' ';
if (i > 0) prev = chars[i - 1];
if (i < chars.length - 1) next = chars[i + 1];
if (current == '.') {
if (((i + 1) % 3) == 0 || (prev != '.' && next != '.')) sb.append("/");
else sb.append(current);
} else sb.append(current);
}
return sb.toString();
}

private boolean containsAPropertyNameFunction(String cql) {
return cql.indexOf(XPATH_FUN_START) != -1 || cql.indexOf(PROPERTY_FUN_START) != -1;
}
}
Loading