View Javadoc
1   /*
2    * Copyright (C) 2022 B3Partners B.V.
3    *
4    * SPDX-License-Identifier: MIT
5    */
6   package org.tailormap.api.geotools.featuresources;
7   
8   import static org.geotools.jdbc.JDBCDataStore.JDBC_PRIMARY_KEY_COLUMN;
9   import static org.geotools.jdbc.JDBCDataStore.JDBC_READ_ONLY;
10  import static org.tailormap.api.persistence.helper.GeoToolsHelper.crsToString;
11  
12  import java.io.IOException;
13  import java.lang.invoke.MethodHandles;
14  import java.util.Arrays;
15  import java.util.HashMap;
16  import java.util.List;
17  import java.util.Map;
18  import org.apache.commons.lang3.StringUtils;
19  import org.geotools.api.data.DataStore;
20  import org.geotools.api.data.DataStoreFinder;
21  import org.geotools.api.data.ResourceInfo;
22  import org.geotools.api.data.ServiceInfo;
23  import org.geotools.api.data.SimpleFeatureSource;
24  import org.geotools.api.feature.simple.SimpleFeatureType;
25  import org.geotools.api.feature.type.AttributeDescriptor;
26  import org.geotools.api.feature.type.AttributeType;
27  import org.geotools.jdbc.JDBCFeatureStore;
28  import org.slf4j.Logger;
29  import org.slf4j.LoggerFactory;
30  import org.tailormap.api.persistence.TMFeatureSource;
31  import org.tailormap.api.persistence.TMFeatureType;
32  import org.tailormap.api.persistence.helper.GeoToolsHelper;
33  import org.tailormap.api.persistence.json.TMAttributeDescriptor;
34  import org.tailormap.api.persistence.json.TMAttributeType;
35  import org.tailormap.api.persistence.json.TMFeatureTypeInfo;
36  import org.tailormap.api.persistence.json.TMServiceCaps;
37  import org.tailormap.api.persistence.json.TMServiceInfo;
38  
39  public abstract class FeatureSourceHelper {
40    private static final Logger logger =
41        LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
42  
43    public DataStore createDataStore(TMFeatureSource tmfs) throws IOException {
44      return createDataStore(tmfs, null);
45    }
46  
47    /**
48     * Create a GeoTools DataStore for the given TMFeatureSource.
49     *
50     * @param tmfs The feature source for which to create the datastore
51     * @param timeout Optional timeout in milliseconds for datastore operations, if supported by the datastore
52     *     implementation
53     * @return The created DataStore
54     * @throws IOException If an error occurs while creating the DataStore
55     */
56    public abstract DataStore createDataStore(TMFeatureSource tmfs, Integer timeout) throws IOException;
57  
58    public SimpleFeatureSource openGeoToolsFeatureSource(TMFeatureType tmft, Integer timeout) throws IOException {
59      DataStore ds = createDataStore(tmft.getFeatureSource(), timeout);
60      return ds.getFeatureSource(tmft.getName());
61    }
62  
63    public void loadCapabilities(TMFeatureSource tmfs) throws IOException {
64      loadCapabilities(tmfs, null);
65    }
66  
67    public DataStore openDatastore(Map<String, Object> params, String passwordKey) throws IOException {
68      Map<String, Object> logParams = new HashMap<>(params);
69      String passwd = (String) params.get(passwordKey);
70      if (passwd != null) {
71        logParams.put(passwordKey, String.valueOf(new char[passwd.length()]).replace("\0", "*"));
72      }
73      logger.debug("Opening datastore using parameters: {}", logParams);
74      DataStore ds;
75      try {
76        ds = DataStoreFinder.getDataStore(params);
77      } catch (Exception e) {
78        throw new IOException("Cannot open datastore using parameters: " + logParams, e);
79      }
80      if (ds == null) {
81        throw new IOException("No datastore found using parameters " + logParams);
82      }
83      return ds;
84    }
85  
86    public void loadCapabilities(TMFeatureSource tmfs, Integer timeout) throws IOException {
87      DataStore ds = createDataStore(tmfs, timeout);
88      try {
89        if (StringUtils.isBlank(tmfs.getTitle())) {
90          tmfs.setTitle(ds.getInfo().getTitle());
91        }
92  
93        ServiceInfo si = ds.getInfo();
94        tmfs.setServiceCapabilities(new TMServiceCaps()
95            .serviceInfo(new TMServiceInfo()
96                .title(si.getTitle())
97                .keywords(si.getKeywords())
98                .description(si.getDescription())
99                .publisher(si.getPublisher())
100               .schema(si.getSchema())
101               .source(si.getSource())));
102 
103       List<String> typeNames = Arrays.asList(ds.getTypeNames());
104       logger.debug(
105           "Type names for {} {}: {}",
106           tmfs.getProtocol().getValue(),
107           tmfs.getProtocol() == TMFeatureSource.Protocol.WFS ? tmfs.getUrl() : tmfs.getJdbcConnection(),
108           typeNames);
109 
110       tmfs.getFeatureTypes().removeIf(tmft -> {
111         if (!typeNames.contains(tmft.getName())) {
112           logger.debug("Feature type removed: {}", tmft.getName());
113           return true;
114         } else {
115           return false;
116         }
117       });
118 
119       for (String typeName : typeNames) {
120         TMFeatureType pft = tmfs.getFeatureTypes().stream()
121             .filter(ft -> ft.getName().equals(typeName))
122             .findFirst()
123             .orElseGet(() -> new TMFeatureType().setName(typeName).setFeatureSource(tmfs));
124         if (!tmfs.getFeatureTypes().contains(pft)) {
125           tmfs.getFeatureTypes().add(pft);
126         }
127         try {
128           logger.debug("Get feature source from GeoTools datastore for type \"{}\"", typeName);
129           SimpleFeatureSource gtFs = ds.getFeatureSource(typeName);
130           ResourceInfo info = gtFs.getInfo();
131           if (info != null) {
132             pft.setTitle(info.getTitle());
133             pft.setInfo(getFeatureTypeInfo(pft, info, gtFs));
134             pft.getAttributes().clear();
135 
136             SimpleFeatureType gtFt = gtFs.getSchema();
137             pft.setWriteable(gtFs instanceof JDBCFeatureStore
138                 && !Boolean.TRUE.equals(gtFt.getUserData().get(JDBC_READ_ONLY)));
139             String primaryKeyName = null;
140             for (AttributeDescriptor gtAttr : gtFt.getAttributeDescriptors()) {
141               AttributeType type = gtAttr.getType();
142               if (Boolean.TRUE.equals(gtAttr.getUserData().get(JDBC_PRIMARY_KEY_COLUMN))) {
143                 if (primaryKeyName == null) {
144                   logger.debug(
145                       "Found primary key attribute \"{}\" for type \"{}\"",
146                       gtAttr.getLocalName(),
147                       typeName);
148                   primaryKeyName = gtAttr.getLocalName();
149                 } else {
150                   logger.warn(
151                       "Multiple primary key attributes found for type \"{}\": \"{}\" and \"{}\". Composite primary keys are not supported for writing, setting as read-only.",
152                       typeName,
153                       primaryKeyName,
154                       gtAttr.getLocalName());
155                   pft.setWriteable(false);
156                 }
157               }
158               TMAttributeDescriptor tmAttr = new TMAttributeDescriptor()
159                   .name(gtAttr.getLocalName())
160                   .type(GeoToolsHelper.toAttributeType(type))
161                   .nullable(gtAttr.isNillable())
162                   .defaultValue(
163                       gtAttr.getDefaultValue() == null
164                           ? null
165                           : gtAttr.getDefaultValue().toString())
166                   .description(
167                       type.getDescription() == null
168                           ? null
169                           : type.getDescription().toString());
170               if (tmAttr.getType() == TMAttributeType.OBJECT) {
171                 tmAttr.setUnknownTypeClassName(type.getBinding().getName());
172               }
173               pft.getAttributes().add(tmAttr);
174             }
175             pft.setPrimaryKeyAttribute(primaryKeyName);
176             pft.setDefaultGeometryAttribute(pft.findDefaultGeometryAttribute());
177           }
178         } catch (Exception e) {
179           logger.error("Exception reading feature type \"{}\"", typeName, e);
180         }
181       }
182     } finally {
183       ds.dispose();
184     }
185   }
186 
187   protected TMFeatureTypeInfo getFeatureTypeInfo(TMFeatureType pft, ResourceInfo info, SimpleFeatureSource gtFs) {
188     return new TMFeatureTypeInfo()
189         .keywords(info.getKeywords())
190         .description(info.getDescription())
191         .bounds(GeoToolsHelper.fromEnvelope(info.getBounds()))
192         .crs(crsToString(info.getCRS()));
193   }
194 }