1
2
3
4
5
6 package org.tailormap.api.controller.admin;
7
8 import com.fasterxml.jackson.databind.ObjectMapper;
9 import io.micrometer.core.annotation.Timed;
10 import io.swagger.v3.oas.annotations.Operation;
11 import io.swagger.v3.oas.annotations.media.Content;
12 import io.swagger.v3.oas.annotations.media.Schema;
13 import io.swagger.v3.oas.annotations.responses.ApiResponse;
14 import java.io.IOException;
15 import java.lang.invoke.MethodHandles;
16 import java.util.NoSuchElementException;
17 import org.apache.solr.client.solrj.SolrClient;
18 import org.apache.solr.client.solrj.SolrServerException;
19 import org.apache.solr.client.solrj.response.SolrPingResponse;
20 import org.apache.solr.common.SolrException;
21 import org.slf4j.Logger;
22 import org.slf4j.LoggerFactory;
23 import org.springframework.http.HttpStatus;
24 import org.springframework.http.MediaType;
25 import org.springframework.http.ResponseEntity;
26 import org.springframework.transaction.annotation.Transactional;
27 import org.springframework.web.bind.annotation.DeleteMapping;
28 import org.springframework.web.bind.annotation.GetMapping;
29 import org.springframework.web.bind.annotation.PathVariable;
30 import org.springframework.web.bind.annotation.PutMapping;
31 import org.springframework.web.bind.annotation.RestController;
32 import org.springframework.web.server.ResponseStatusException;
33 import org.tailormap.api.geotools.featuresources.FeatureSourceFactoryHelper;
34 import org.tailormap.api.persistence.SearchIndex;
35 import org.tailormap.api.persistence.TMFeatureSource;
36 import org.tailormap.api.persistence.TMFeatureType;
37 import org.tailormap.api.repository.FeatureTypeRepository;
38 import org.tailormap.api.repository.SearchIndexRepository;
39 import org.tailormap.api.solr.SolrHelper;
40 import org.tailormap.api.solr.SolrService;
41 import org.tailormap.api.viewer.model.ErrorResponse;
42
43
44 @RestController
45 public class SolrAdminController {
46 private static final Logger logger =
47 LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
48 private final FeatureSourceFactoryHelper featureSourceFactoryHelper;
49
50 private final FeatureTypeRepository featureTypeRepository;
51 private final SearchIndexRepository searchIndexRepository;
52 private final SolrService solrService;
53
54 public SolrAdminController(
55 FeatureSourceFactoryHelper featureSourceFactoryHelper,
56 FeatureTypeRepository featureTypeRepository,
57 SearchIndexRepository searchIndexRepository,
58 SolrService solrService) {
59 this.featureSourceFactoryHelper = featureSourceFactoryHelper;
60 this.featureTypeRepository = featureTypeRepository;
61 this.searchIndexRepository = searchIndexRepository;
62 this.solrService = solrService;
63 }
64
65
66
67
68
69
70 @Operation(summary = "Ping Solr", description = "Ping Solr to check if it is available")
71 @ApiResponse(
72 responseCode = "200",
73 description = "Solr is available",
74 content =
75 @Content(
76 mediaType = MediaType.APPLICATION_JSON_VALUE,
77 schema = @Schema(example = "{\"status\":\"OK\",\"timeElapsed\":1}")))
78 @ApiResponse(
79 responseCode = "500",
80 description = "Solr is not available",
81 content =
82 @Content(
83 mediaType = MediaType.APPLICATION_JSON_VALUE,
84 schema = @Schema(example = "{\"message\":\"Some error message..\",\"code\":500}")))
85 @GetMapping(
86 path = "${tailormap-api.admin.base-path}/index/ping",
87 produces = MediaType.APPLICATION_JSON_VALUE)
88 public ResponseEntity<?> pingSolr() {
89 try (SolrClient solrClient = solrService.getSolrClientForSearching()) {
90 final SolrPingResponse ping = solrClient.ping();
91 logger.info("Solr ping status {}", ping.getResponse().get("status"));
92 return ResponseEntity.ok(
93 new ObjectMapper()
94 .createObjectNode()
95 .put("status", ping.getResponse().get("status").toString())
96 .put("timeElapsed", ping.getElapsedTime()));
97 } catch (IOException | SolrServerException e) {
98 logger.error("Error pinging solr", e);
99 return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
100 .contentType(MediaType.APPLICATION_JSON)
101 .body(
102 new ErrorResponse()
103 .message(e.getLocalizedMessage())
104 .code(HttpStatus.INTERNAL_SERVER_ERROR.value()));
105 }
106 }
107
108
109
110
111
112
113
114 @Operation(
115 summary = "Create or update a feature type index",
116 description =
117 "Create or update a feature type index for a layer, will erase existing index if present")
118 @ApiResponse(responseCode = "202", description = "Index create or update request accepted")
119 @ApiResponse(
120 responseCode = "404",
121 description = "Layer does not have feature type",
122 content =
123 @Content(
124 mediaType = MediaType.APPLICATION_JSON_VALUE,
125 schema =
126 @Schema(
127 example = "{\"message\":\"Layer does not have feature type\",\"code\":404}")))
128 @ApiResponse(
129 responseCode = "404",
130 description = "Indexing WFS feature types is not supported",
131 content =
132 @Content(
133 mediaType = MediaType.APPLICATION_JSON_VALUE,
134 schema =
135 @Schema(
136 example =
137 "{\"message\":\"Layer does not have valid feature type for indexing\",\"code\":400}")))
138 @ApiResponse(
139 responseCode = "500",
140 description = "Error while indexing",
141 content =
142 @Content(
143 mediaType = MediaType.APPLICATION_JSON_VALUE,
144 schema = @Schema(example = "{\"message\":\"Some error message..\",\"code\":500}")))
145 @Transactional
146 @Timed(value = "index_feature_type", description = "time spent to index feature type")
147 @PutMapping(
148 path = "${tailormap-api.admin.base-path}/index/{searchIndexId}",
149 produces = MediaType.APPLICATION_JSON_VALUE)
150 public ResponseEntity<?> index(@PathVariable Long searchIndexId) {
151
152 SearchIndex searchIndex =
153 searchIndexRepository
154 .findById(searchIndexId)
155 .orElseThrow(
156 () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Search index not found"));
157
158 TMFeatureType indexingFT =
159 featureTypeRepository
160 .findById(searchIndex.getFeatureTypeId())
161 .orElseThrow(
162 () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Feature type not found"));
163
164 if (TMFeatureSource.Protocol.WFS.equals(indexingFT.getFeatureSource().getProtocol())) {
165
166 searchIndex.setStatus(SearchIndex.Status.ERROR).setComment("WFS indexing not supported");
167 throw new ResponseStatusException(
168 HttpStatus.BAD_REQUEST, "Layer does not have valid feature type for indexing");
169 }
170
171 boolean createNewIndex =
172 (null == searchIndex.getLastIndexed()
173 || searchIndex.getStatus() == SearchIndex.Status.INITIAL);
174 try (SolrClient solrClient = solrService.getSolrClientForIndexing();
175 SolrHelper solrHelper = new SolrHelper(solrClient)) {
176 solrHelper.addFeatureTypeIndex(searchIndex, indexingFT, featureSourceFactoryHelper);
177 searchIndexRepository.save(searchIndex);
178 } catch (UnsupportedOperationException | IOException | SolrServerException | SolrException e) {
179 logger.error("Error indexing", e);
180 searchIndex.setStatus(SearchIndex.Status.ERROR);
181 searchIndexRepository.save(searchIndex);
182 return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
183 .contentType(MediaType.APPLICATION_JSON)
184 .body(
185 new ErrorResponse()
186 .message(e.getLocalizedMessage())
187 .code(HttpStatus.INTERNAL_SERVER_ERROR.value()));
188 }
189
190 if (createNewIndex) {
191 logger.info("Created new index for search index {}", searchIndexId);
192 return ResponseEntity.status(HttpStatus.CREATED).build();
193 } else {
194 logger.info("Updated index for search index {}", searchIndexId);
195 return ResponseEntity.accepted().build();
196 }
197 }
198
199
200
201
202
203
204
205 @Operation(
206 summary = "Clear index for a feature type",
207 description = "Clear index for the feature type")
208 @ApiResponse(responseCode = "204", description = "Index cleared")
209 @ApiResponse(responseCode = "404", description = "Index not configured for feature type")
210 @ApiResponse(
211 responseCode = "500",
212 description = "Error while clearing index",
213 content =
214 @Content(
215 mediaType = MediaType.APPLICATION_JSON_VALUE,
216 schema = @Schema(example = "{\"message\":\"Some error message..\",\"code\":500}")))
217 @Timed(value = "index_delete", description = "time spent to delete an index of a feature type")
218 @DeleteMapping(
219 path = "${tailormap-api.admin.base-path}/index/{searchIndexId}",
220 produces = MediaType.APPLICATION_JSON_VALUE)
221 @Transactional
222 public ResponseEntity<?> clearIndex(@PathVariable Long searchIndexId) {
223 try (SolrClient solrClient = solrService.getSolrClientForSearching();
224 SolrHelper solrHelper = new SolrHelper(solrClient)) {
225 solrHelper.clearIndexForLayer(searchIndexId);
226
227
228 SearchIndex searchIndex =
229 searchIndexRepository
230 .findById(searchIndexId)
231 .orElseThrow(
232 () ->
233 new ResponseStatusException(HttpStatus.NOT_FOUND, "Search index not found"));
234 searchIndex
235 .setLastIndexed(null)
236 .setStatus(SearchIndex.Status.INITIAL)
237 .setComment("Index cleared");
238 searchIndexRepository.save(searchIndex);
239 } catch (IOException | SolrServerException | NoSuchElementException e) {
240 logger.warn("Error clearing index", e);
241 return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
242 .contentType(MediaType.APPLICATION_JSON)
243 .body(
244 new ErrorResponse()
245 .message(e.getLocalizedMessage())
246 .code(HttpStatus.INTERNAL_SERVER_ERROR.value()));
247 }
248
249 logger.info("Index cleared for index {}", searchIndexId);
250 return ResponseEntity.noContent().build();
251 }
252 }