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.admin.model.CapabilitiesLoadingEvent;
31  import org.tailormap.api.persistence.TMFeatureSource;
32  import org.tailormap.api.persistence.TMFeatureType;
33  import org.tailormap.api.persistence.helper.CapabilitiesLoadingProgressReporting;
34  import org.tailormap.api.persistence.helper.GeoToolsHelper;
35  import org.tailormap.api.persistence.json.TMAttributeDescriptor;
36  import org.tailormap.api.persistence.json.TMAttributeType;
37  import org.tailormap.api.persistence.json.TMFeatureTypeInfo;
38  import org.tailormap.api.persistence.json.TMServiceCaps;
39  import org.tailormap.api.persistence.json.TMServiceInfo;
40  
41  public abstract class FeatureSourceHelper {
42    private static final Logger logger =
43        LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
44  
45    public DataStore createDataStore(TMFeatureSource tmfs) throws IOException {
46      return createDataStore(tmfs, null);
47    }
48  
49    /**
50     * Create a GeoTools DataStore for the given TMFeatureSource.
51     *
52     * @param tmfs The feature source for which to create the datastore
53     * @param timeout Optional timeout in milliseconds for datastore operations, if supported by the datastore
54     *     implementation
55     * @return The created DataStore
56     * @throws IOException If an error occurs while creating the DataStore
57     */
58    public abstract DataStore createDataStore(TMFeatureSource tmfs, Integer timeout) throws IOException;
59  
60    public SimpleFeatureSource openGeoToolsFeatureSource(TMFeatureType tmft, Integer timeout) throws IOException {
61      DataStore ds = createDataStore(tmft.getFeatureSource(), timeout);
62      try {
63        return ds.getFeatureSource(tmft.getName());
64      } catch (IOException | RuntimeException e) {
65        // The caller can't dispose the datastore when no feature source is returned
66        ds.dispose();
67        throw e;
68      }
69    }
70  
71    public DataStore openDatastore(Map<String, Object> params, String passwordKey) throws IOException {
72      Map<String, Object> logParams = new HashMap<>(params);
73      String passwd = (String) params.get(passwordKey);
74      if (passwd != null) {
75        logParams.put(passwordKey, String.valueOf(new char[passwd.length()]).replace("\0", "*"));
76      }
77      logger.debug("Opening datastore using parameters: {}", logParams);
78      DataStore ds;
79      try {
80        ds = DataStoreFinder.getDataStore(params);
81      } catch (IOException e) {
82        throw new IOException("Cannot open datastore using parameters: " + logParams, e);
83      }
84      if (ds == null) {
85        throw new IOException("No datastore found using parameters " + logParams);
86      }
87      return ds;
88    }
89  
90    public void loadCapabilities(
91        TMFeatureSource tmfs,
92        Integer timeout,
93        CapabilitiesLoadingProgressReporting reporter,
94        CapabilitiesLoadingEvent event)
95        throws IOException {
96  
97      reporter.reportCapabilitiesLoadingProgress(
98          event.message("Opening datastore for feature source " + tmfs.getTitle()));
99  
100     DataStore ds = createDataStore(tmfs, timeout);
101     try {
102       if (StringUtils.isBlank(tmfs.getTitle())) {
103         tmfs.setTitle(ds.getInfo().getTitle());
104       }
105 
106       ServiceInfo si = ds.getInfo();
107       tmfs.setServiceCapabilities(new TMServiceCaps()
108           .serviceInfo(new TMServiceInfo()
109               .title(si.getTitle())
110               .keywords(si.getKeywords())
111               .description(si.getDescription())
112               .publisher(si.getPublisher())
113               .schema(si.getSchema())
114               .source(si.getSource())));
115 
116       List<String> typeNames = Arrays.asList(ds.getTypeNames());
117       int progress = 0;
118       reporter.reportCapabilitiesLoadingProgress(
119           event.message("Retrieved service information for feature source " + tmfs.getTitle())
120               .total(typeNames.size())
121               .progress(progress));
122       logger.debug(
123           "Type names for {} {}: {}",
124           tmfs.getProtocol().getValue(),
125           tmfs.getProtocol() == TMFeatureSource.Protocol.WFS ? tmfs.getUrl() : tmfs.getJdbcConnection(),
126           typeNames);
127 
128       tmfs.getFeatureTypes().removeIf(tmft -> {
129         if (!typeNames.contains(tmft.getName())) {
130           logger.debug("Feature type removed: {}", tmft.getName());
131           return true;
132         } else {
133           return false;
134         }
135       });
136 
137       for (String typeName : typeNames) {
138         TMFeatureType pft = tmfs.getFeatureTypes().stream()
139             .filter(ft -> ft.getName().equals(typeName))
140             .findFirst()
141             .orElseGet(() -> new TMFeatureType().setName(typeName).setFeatureSource(tmfs));
142         if (!tmfs.getFeatureTypes().contains(pft)) {
143           tmfs.getFeatureTypes().add(pft);
144         }
145         try {
146           logger.debug("Get feature source from GeoTools datastore for type \"{}\"", typeName);
147           SimpleFeatureSource gtFs = ds.getFeatureSource(typeName);
148           ResourceInfo info = gtFs.getInfo();
149           if (info != null) {
150             pft.setTitle(info.getTitle());
151             pft.setInfo(getFeatureTypeInfo(pft, info, gtFs));
152             pft.getAttributes().clear();
153 
154             SimpleFeatureType gtFt = gtFs.getSchema();
155 
156             reporter.reportCapabilitiesLoadingProgress(event.message("Retrieved feature type " + typeName)
157                 .progress(++progress));
158 
159             pft.setWriteable(gtFs instanceof JDBCFeatureStore
160                 && !Boolean.TRUE.equals(gtFt.getUserData().get(JDBC_READ_ONLY)));
161             String primaryKeyName = null;
162             for (AttributeDescriptor gtAttr : gtFt.getAttributeDescriptors()) {
163               AttributeType type = gtAttr.getType();
164               if (Boolean.TRUE.equals(gtAttr.getUserData().get(JDBC_PRIMARY_KEY_COLUMN))) {
165                 if (primaryKeyName == null) {
166                   logger.debug(
167                       "Found primary key attribute \"{}\" for type \"{}\"",
168                       gtAttr.getLocalName(),
169                       typeName);
170                   primaryKeyName = gtAttr.getLocalName();
171                 } else {
172                   logger.warn(
173                       "Multiple primary key attributes found for type \"{}\": \"{}\" and \"{}\". Composite primary keys are not supported for writing, setting as read-only.",
174                       typeName,
175                       primaryKeyName,
176                       gtAttr.getLocalName());
177                   pft.setWriteable(false);
178                 }
179               }
180               TMAttributeDescriptor tmAttr = new TMAttributeDescriptor()
181                   .name(gtAttr.getLocalName())
182                   .type(GeoToolsHelper.toAttributeType(type))
183                   .nullable(gtAttr.isNillable())
184                   .defaultValue(
185                       gtAttr.getDefaultValue() == null
186                           ? null
187                           : gtAttr.getDefaultValue().toString())
188                   .description(
189                       type.getDescription() == null
190                           ? null
191                           : type.getDescription().toString());
192               if (tmAttr.getType() == TMAttributeType.OBJECT) {
193                 tmAttr.setUnknownTypeClassName(type.getBinding().getName());
194               }
195               pft.getAttributes().add(tmAttr);
196             }
197             pft.setPrimaryKeyAttribute(primaryKeyName);
198             pft.setDefaultGeometryAttribute(pft.findDefaultGeometryAttribute());
199           }
200         } catch (Exception e) {
201           logger.error("Exception reading feature type \"{}\"", typeName, e);
202         }
203       }
204     } finally {
205       ds.dispose();
206     }
207   }
208 
209   protected TMFeatureTypeInfo getFeatureTypeInfo(TMFeatureType pft, ResourceInfo info, SimpleFeatureSource gtFs) {
210     return new TMFeatureTypeInfo()
211         .keywords(info.getKeywords())
212         .description(info.getDescription())
213         .bounds(GeoToolsHelper.fromEnvelope(info.getBounds()))
214         .crs(crsToString(info.getCRS()));
215   }
216 }