View Javadoc
1   /*
2    * Copyright (C) 2023 B3Partners B.V.
3    *
4    * SPDX-License-Identifier: MIT
5    */
6   package org.tailormap.api.persistence;
7   
8   import com.fasterxml.jackson.annotation.JsonIgnore;
9   import jakarta.persistence.AttributeOverride;
10  import jakarta.persistence.AttributeOverrides;
11  import jakarta.persistence.Column;
12  import jakarta.persistence.Embedded;
13  import jakarta.persistence.Entity;
14  import jakarta.persistence.EntityListeners;
15  import jakarta.persistence.GeneratedValue;
16  import jakarta.persistence.GenerationType;
17  import jakarta.persistence.Id;
18  import jakarta.persistence.Version;
19  import jakarta.validation.constraints.NotNull;
20  import java.lang.invoke.MethodHandles;
21  import java.util.ArrayList;
22  import java.util.List;
23  import java.util.Optional;
24  import java.util.stream.Stream;
25  import org.geotools.referencing.CRS;
26  import org.hibernate.annotations.JdbcTypeCode;
27  import org.hibernate.envers.Audited;
28  import org.hibernate.type.SqlTypes;
29  import org.slf4j.Logger;
30  import org.slf4j.LoggerFactory;
31  import org.springframework.data.jpa.domain.support.AuditingEntityListener;
32  import org.tailormap.api.persistence.json.AppContent;
33  import org.tailormap.api.persistence.json.AppLayerSettings;
34  import org.tailormap.api.persistence.json.AppSettings;
35  import org.tailormap.api.persistence.json.AppTreeLayerNode;
36  import org.tailormap.api.persistence.json.AuthorizationRule;
37  import org.tailormap.api.persistence.json.Bounds;
38  import org.tailormap.api.persistence.listener.EntityEventPublisher;
39  import org.tailormap.api.viewer.model.AppStyling;
40  import org.tailormap.api.viewer.model.Component;
41  
42  @Audited
43  @Entity
44  @EntityListeners({EntityEventPublisher.class, AuditingEntityListener.class})
45  public class Application extends AuditMetadata {
46    private static final Logger logger =
47        LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
48  
49    @Id
50    @GeneratedValue(strategy = GenerationType.IDENTITY)
51    private Long id;
52  
53    @Version
54    private Long version;
55  
56    @Column(unique = true)
57    @NotNull private String name;
58  
59    private String title;
60  
61    @Column(columnDefinition = "text")
62    private String adminComments;
63  
64    @Column(columnDefinition = "text")
65    private String previewText;
66  
67    @NotNull private String crs;
68  
69    @Embedded
70    @AttributeOverrides({
71      @AttributeOverride(name = "minx", column = @Column(name = "initial_minx")),
72      @AttributeOverride(name = "maxx", column = @Column(name = "initial_maxx")),
73      @AttributeOverride(name = "miny", column = @Column(name = "initial_miny")),
74      @AttributeOverride(name = "maxy", column = @Column(name = "initial_maxy"))
75    })
76    private Bounds initialExtent;
77  
78    @Embedded
79    @AttributeOverrides({
80      @AttributeOverride(name = "minx", column = @Column(name = "max_minx")),
81      @AttributeOverride(name = "maxx", column = @Column(name = "max_maxx")),
82      @AttributeOverride(name = "miny", column = @Column(name = "max_miny")),
83      @AttributeOverride(name = "maxy", column = @Column(name = "max_maxy"))
84    })
85    private Bounds maxExtent;
86  
87    @JdbcTypeCode(SqlTypes.JSON)
88    @Column(columnDefinition = "jsonb")
89    @NotNull private AppContent contentRoot = new AppContent();
90  
91    @JsonIgnore
92    private transient AppContent oldContentRoot;
93  
94    @JdbcTypeCode(SqlTypes.JSON)
95    @Column(columnDefinition = "jsonb")
96    @NotNull private AppSettings settings = new AppSettings();
97  
98    @JdbcTypeCode(SqlTypes.JSON)
99    @Column(columnDefinition = "jsonb")
100   @NotNull private List<Component> components = new ArrayList<>();
101 
102   @JdbcTypeCode(SqlTypes.JSON)
103   @Column(columnDefinition = "jsonb")
104   @NotNull private AppStyling styling = new AppStyling();
105 
106   @JdbcTypeCode(SqlTypes.JSON)
107   @Column(columnDefinition = "jsonb")
108   @NotNull private List<AuthorizationRule> authorizationRules = new ArrayList<>();
109 
110   // <editor-fold desc="getters and setters">
111   public Long getId() {
112     return id;
113   }
114 
115   public Application setId(Long id) {
116     this.id = id;
117     return this;
118   }
119 
120   public Long getVersion() {
121     return version;
122   }
123 
124   public Application setVersion(Long version) {
125     this.version = version;
126     return this;
127   }
128 
129   public String getName() {
130     return name;
131   }
132 
133   public Application setName(String name) {
134     this.name = name;
135     return this;
136   }
137 
138   public String getTitle() {
139     return title;
140   }
141 
142   public Application setTitle(String title) {
143     this.title = title;
144     return this;
145   }
146 
147   public String getAdminComments() {
148     return adminComments;
149   }
150 
151   public Application setAdminComments(String adminComments) {
152     this.adminComments = adminComments;
153     return this;
154   }
155 
156   public String getPreviewText() {
157     return previewText;
158   }
159 
160   public Application setPreviewText(String previewText) {
161     this.previewText = previewText;
162     return this;
163   }
164 
165   public String getCrs() {
166     return crs;
167   }
168 
169   public Application setCrs(String crs) {
170     this.crs = crs;
171     return this;
172   }
173 
174   public Bounds getInitialExtent() {
175     return initialExtent;
176   }
177 
178   public Application setInitialExtent(Bounds initialExtent) {
179     this.initialExtent = initialExtent;
180     return this;
181   }
182 
183   public Bounds getMaxExtent() {
184     return maxExtent;
185   }
186 
187   public Application setMaxExtent(Bounds maxExtent) {
188     this.maxExtent = maxExtent;
189     return this;
190   }
191 
192   public AppContent getContentRoot() {
193     return contentRoot;
194   }
195 
196   public Application setContentRoot(AppContent contentRoot) {
197     this.oldContentRoot = this.contentRoot;
198     this.contentRoot = contentRoot;
199     return this;
200   }
201   /**
202    * This method is used to get the old content root before it is updated. It is used in the ApplicationEventHandler
203    * to compare the old and new content roots, more specifically to get the old AppTreeLayerNode nodes.
204    *
205    * @see #getAllOldAppTreeLayerNode()
206    */
207   public AppContent getOldContentRoot() {
208     return this.oldContentRoot;
209   }
210 
211   public AppSettings getSettings() {
212     return settings;
213   }
214 
215   public Application setSettings(AppSettings layerSettings) {
216     this.settings = layerSettings;
217     return this;
218   }
219 
220   public List<Component> getComponents() {
221     return components;
222   }
223 
224   public Application setComponents(List<Component> components) {
225     this.components = components;
226     return this;
227   }
228 
229   public AppStyling getStyling() {
230     return styling;
231   }
232 
233   public Application setStyling(AppStyling styling) {
234     this.styling = styling;
235     return this;
236   }
237 
238   public List<AuthorizationRule> getAuthorizationRules() {
239     return authorizationRules;
240   }
241 
242   public Application setAuthorizationRules(List<AuthorizationRule> authorizationRules) {
243     this.authorizationRules = authorizationRules;
244     return this;
245   }
246 
247   // </editor-fold>
248   @JsonIgnore
249   public Stream<AppTreeLayerNode> getAllOldAppTreeLayerNode() {
250     return this.getAppTreeLayerNodeStream(this.getOldContentRoot());
251   }
252 
253   @JsonIgnore
254   public Stream<AppTreeLayerNode> getAllAppTreeLayerNode() {
255     return this.getAppTreeLayerNodeStream(this.getContentRoot());
256   }
257 
258   @JsonIgnore
259   private Stream<AppTreeLayerNode> getAppTreeLayerNodeStream(AppContent contentRoot) {
260     if (contentRoot == null) {
261       return Stream.empty();
262     }
263     Stream<AppTreeLayerNode> baseLayers = Stream.empty();
264     if (contentRoot.getBaseLayerNodes() != null) {
265       baseLayers = contentRoot.getBaseLayerNodes().stream()
266           .filter(n -> "AppTreeLayerNode".equals(n.getObjectType()))
267           .map(n -> (AppTreeLayerNode) n);
268     }
269     Stream<AppTreeLayerNode> layers = Stream.empty();
270     if (contentRoot.getLayerNodes() != null) {
271       layers = contentRoot.getLayerNodes().stream()
272           .filter(n -> "AppTreeLayerNode".equals(n.getObjectType()))
273           .map(n -> (AppTreeLayerNode) n);
274     }
275     return Stream.concat(baseLayers, layers);
276   }
277 
278   /**
279    * Return a GeoTools CoordinateReferenceSystem from this entities' CRS code or null if there is an error decoding
280    * it, which will be logged (only with stacktrace if loglevel is DEBUG).
281    *
282    * @return CoordinateReferenceSystem
283    */
284   @JsonIgnore
285   public org.geotools.api.referencing.crs.CoordinateReferenceSystem getGeoToolsCoordinateReferenceSystem() {
286     org.geotools.api.referencing.crs.CoordinateReferenceSystem gtCrs = null;
287     try {
288       if (getCrs() != null) {
289         gtCrs = CRS.decode(getCrs());
290       }
291     } catch (Exception e) {
292       String message = "Application %d: error decoding CRS from code \"%s\": %s: %s"
293           .formatted(getId(), getCrs(), e.getClass(), e.getMessage());
294       if (logger.isDebugEnabled()) {
295         logger.error(message, e);
296       } else {
297         logger.error(message);
298       }
299     }
300     return gtCrs;
301   }
302 
303   @NotNull public AppLayerSettings getAppLayerSettings(@NotNull AppTreeLayerNode node) {
304     return getAppLayerSettings(node.getId());
305   }
306 
307   @NotNull public AppLayerSettings getAppLayerSettings(@NotNull String appLayerId) {
308     return Optional.ofNullable(getSettings())
309         .map(AppSettings::getLayerSettings)
310         .map(layerSettingsMap -> layerSettingsMap.get(appLayerId))
311         .orElseGet(AppLayerSettings::new);
312   }
313 }