Application.java
/*
* Copyright (C) 2023 B3Partners B.V.
*
* SPDX-License-Identifier: MIT
*/
package org.tailormap.api.persistence;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.AttributeOverride;
import jakarta.persistence.AttributeOverrides;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Version;
import jakarta.validation.constraints.NotNull;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import org.geotools.referencing.CRS;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.envers.Audited;
import org.hibernate.type.SqlTypes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.tailormap.api.persistence.json.AppContent;
import org.tailormap.api.persistence.json.AppLayerSettings;
import org.tailormap.api.persistence.json.AppSettings;
import org.tailormap.api.persistence.json.AppTreeLayerNode;
import org.tailormap.api.persistence.json.AuthorizationRule;
import org.tailormap.api.persistence.json.Bounds;
import org.tailormap.api.persistence.listener.EntityEventPublisher;
import org.tailormap.api.viewer.model.AppStyling;
import org.tailormap.api.viewer.model.Component;
@Audited
@Entity
@EntityListeners({EntityEventPublisher.class, AuditingEntityListener.class})
public class Application extends AuditMetadata {
private static final Logger logger =
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Version
private Long version;
@Column(unique = true)
@NotNull private String name;
private String title;
@Column(columnDefinition = "text")
private String adminComments;
@Column(columnDefinition = "text")
private String previewText;
@NotNull private String crs;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "minx", column = @Column(name = "initial_minx")),
@AttributeOverride(name = "maxx", column = @Column(name = "initial_maxx")),
@AttributeOverride(name = "miny", column = @Column(name = "initial_miny")),
@AttributeOverride(name = "maxy", column = @Column(name = "initial_maxy"))
})
private Bounds initialExtent;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "minx", column = @Column(name = "max_minx")),
@AttributeOverride(name = "maxx", column = @Column(name = "max_maxx")),
@AttributeOverride(name = "miny", column = @Column(name = "max_miny")),
@AttributeOverride(name = "maxy", column = @Column(name = "max_maxy"))
})
private Bounds maxExtent;
@JdbcTypeCode(SqlTypes.JSON)
@Column(columnDefinition = "jsonb")
@NotNull private AppContent contentRoot = new AppContent();
@JsonIgnore
private transient AppContent oldContentRoot;
@JdbcTypeCode(SqlTypes.JSON)
@Column(columnDefinition = "jsonb")
@NotNull private AppSettings settings = new AppSettings();
@JdbcTypeCode(SqlTypes.JSON)
@Column(columnDefinition = "jsonb")
@NotNull private List<Component> components = new ArrayList<>();
@JdbcTypeCode(SqlTypes.JSON)
@Column(columnDefinition = "jsonb")
@NotNull private AppStyling styling = new AppStyling();
@JdbcTypeCode(SqlTypes.JSON)
@Column(columnDefinition = "jsonb")
@NotNull private List<AuthorizationRule> authorizationRules = new ArrayList<>();
// <editor-fold desc="getters and setters">
public Long getId() {
return id;
}
public Application setId(Long id) {
this.id = id;
return this;
}
public Long getVersion() {
return version;
}
public Application setVersion(Long version) {
this.version = version;
return this;
}
public String getName() {
return name;
}
public Application setName(String name) {
this.name = name;
return this;
}
public String getTitle() {
return title;
}
public Application setTitle(String title) {
this.title = title;
return this;
}
public String getAdminComments() {
return adminComments;
}
public Application setAdminComments(String adminComments) {
this.adminComments = adminComments;
return this;
}
public String getPreviewText() {
return previewText;
}
public Application setPreviewText(String previewText) {
this.previewText = previewText;
return this;
}
public String getCrs() {
return crs;
}
public Application setCrs(String crs) {
this.crs = crs;
return this;
}
public Bounds getInitialExtent() {
return initialExtent;
}
public Application setInitialExtent(Bounds initialExtent) {
this.initialExtent = initialExtent;
return this;
}
public Bounds getMaxExtent() {
return maxExtent;
}
public Application setMaxExtent(Bounds maxExtent) {
this.maxExtent = maxExtent;
return this;
}
public AppContent getContentRoot() {
return contentRoot;
}
public Application setContentRoot(AppContent contentRoot) {
this.oldContentRoot = this.contentRoot;
this.contentRoot = contentRoot;
return this;
}
/**
* This method is used to get the old content root before it is updated. It is used in the ApplicationEventHandler
* to compare the old and new content roots, more specifically to get the old AppTreeLayerNode nodes.
*
* @see #getAllOldAppTreeLayerNode()
*/
public AppContent getOldContentRoot() {
return this.oldContentRoot;
}
public AppSettings getSettings() {
return settings;
}
public Application setSettings(AppSettings layerSettings) {
this.settings = layerSettings;
return this;
}
public List<Component> getComponents() {
return components;
}
public Application setComponents(List<Component> components) {
this.components = components;
return this;
}
public AppStyling getStyling() {
return styling;
}
public Application setStyling(AppStyling styling) {
this.styling = styling;
return this;
}
public List<AuthorizationRule> getAuthorizationRules() {
return authorizationRules;
}
public Application setAuthorizationRules(List<AuthorizationRule> authorizationRules) {
this.authorizationRules = authorizationRules;
return this;
}
// </editor-fold>
@JsonIgnore
public Stream<AppTreeLayerNode> getAllOldAppTreeLayerNode() {
return this.getAppTreeLayerNodeStream(this.getOldContentRoot());
}
@JsonIgnore
public Stream<AppTreeLayerNode> getAllAppTreeLayerNode() {
return this.getAppTreeLayerNodeStream(this.getContentRoot());
}
@JsonIgnore
private Stream<AppTreeLayerNode> getAppTreeLayerNodeStream(AppContent contentRoot) {
if (contentRoot == null) {
return Stream.empty();
}
Stream<AppTreeLayerNode> baseLayers = Stream.empty();
if (contentRoot.getBaseLayerNodes() != null) {
baseLayers = contentRoot.getBaseLayerNodes().stream()
.filter(n -> "AppTreeLayerNode".equals(n.getObjectType()))
.map(n -> (AppTreeLayerNode) n);
}
Stream<AppTreeLayerNode> layers = Stream.empty();
if (contentRoot.getLayerNodes() != null) {
layers = contentRoot.getLayerNodes().stream()
.filter(n -> "AppTreeLayerNode".equals(n.getObjectType()))
.map(n -> (AppTreeLayerNode) n);
}
return Stream.concat(baseLayers, layers);
}
/**
* Return a GeoTools CoordinateReferenceSystem from this entities' CRS code or null if there is an error decoding
* it, which will be logged (only with stacktrace if loglevel is DEBUG).
*
* @return CoordinateReferenceSystem
*/
@JsonIgnore
public org.geotools.api.referencing.crs.CoordinateReferenceSystem getGeoToolsCoordinateReferenceSystem() {
org.geotools.api.referencing.crs.CoordinateReferenceSystem gtCrs = null;
try {
if (getCrs() != null) {
gtCrs = CRS.decode(getCrs());
}
} catch (Exception e) {
String message = "Application %d: error decoding CRS from code \"%s\": %s: %s"
.formatted(getId(), getCrs(), e.getClass(), e.getMessage());
if (logger.isDebugEnabled()) {
logger.error(message, e);
} else {
logger.error(message);
}
}
return gtCrs;
}
@NotNull public AppLayerSettings getAppLayerSettings(@NotNull AppTreeLayerNode node) {
return getAppLayerSettings(node.getId());
}
@NotNull public AppLayerSettings getAppLayerSettings(@NotNull String appLayerId) {
return Optional.ofNullable(getSettings())
.map(AppSettings::getLayerSettings)
.map(layerSettingsMap -> layerSettingsMap.get(appLayerId))
.orElseGet(AppLayerSettings::new);
}
}