View Javadoc
1   package fr.ifremer.quadrige3.batch.shape.service;
2   
3   /*-
4    * #%L
5    * Quadrige3 Batch :: Shape import/export
6    * %%
7    * Copyright (C) 2017 - 2018 Ifremer
8    * %%
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU Affero General Public License as published by
11   * the Free Software Foundation, either version 3 of the License, or
12   * (at your option) any later version.
13   *
14   * This program is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   * GNU General Public License for more details.
18   *
19   * You should have received a copy of the GNU Affero General Public License
20   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21   * #L%
22   */
23  
24  import com.vividsolutions.jts.geom.Envelope;
25  import com.vividsolutions.jts.geom.Geometry;
26  import com.vividsolutions.jts.geom.MultiPoint;
27  import fr.ifremer.quadrige3.batch.BatchesServiceLocator;
28  import fr.ifremer.quadrige3.batch.shape.config.ShapeConfigurationOption;
29  import fr.ifremer.quadrige3.core.config.QuadrigeConfiguration;
30  import fr.ifremer.quadrige3.core.dao.technical.*;
31  import fr.ifremer.quadrige3.core.exception.QuadrigeTechnicalException;
32  import fr.ifremer.quadrige3.core.service.referential.monitoringLocation.MonitoringLocationService;
33  import fr.ifremer.quadrige3.core.vo.referential.monitoringLocation.ErrorCodes;
34  import fr.ifremer.quadrige3.core.vo.referential.monitoringLocation.ImportShapeContextVO;
35  import fr.ifremer.quadrige3.core.vo.referential.monitoringLocation.ImportShapeResultVO;
36  import fr.ifremer.quadrige3.core.vo.referential.monitoringLocation.MonitoringLocationVO;
37  import org.apache.commons.collections4.CollectionUtils;
38  import org.apache.commons.io.FileUtils;
39  import org.apache.commons.io.FilenameUtils;
40  import org.apache.commons.lang3.StringUtils;
41  import org.apache.commons.logging.Log;
42  import org.apache.commons.logging.LogFactory;
43  import org.geotools.data.FileDataStore;
44  import org.geotools.data.FileDataStoreFinder;
45  import org.geotools.data.shapefile.ShapefileDataStore;
46  import org.geotools.data.simple.SimpleFeatureCollection;
47  import org.geotools.data.simple.SimpleFeatureIterator;
48  import org.geotools.data.simple.SimpleFeatureSource;
49  import org.geotools.geometry.jts.ReferencedEnvelope;
50  import org.geotools.referencing.CRS;
51  import org.geotools.referencing.crs.DefaultGeographicCRS;
52  import org.nuiton.config.ApplicationConfig;
53  import org.opengis.feature.simple.SimpleFeature;
54  import org.opengis.referencing.FactoryException;
55  import org.opengis.referencing.crs.CoordinateReferenceSystem;
56  import org.springframework.beans.factory.InitializingBean;
57  import org.springframework.mail.MailSender;
58  import org.springframework.mail.SimpleMailMessage;
59  
60  import java.io.FileNotFoundException;
61  import java.io.IOException;
62  import java.nio.charset.Charset;
63  import java.nio.file.Files;
64  import java.nio.file.Path;
65  import java.text.DecimalFormat;
66  import java.text.ParseException;
67  import java.util.*;
68  import java.util.regex.Matcher;
69  import java.util.regex.Pattern;
70  import java.util.stream.Collectors;
71  
72  import static org.nuiton.i18n.I18n.t;
73  
74  public class ImportShapeServiceImpl implements ImportShapeService, InitializingBean {
75  
76      private static final Log log = LogFactory.getLog(ImportShapeServiceImpl.class);
77      private static final CoordinateReferenceSystem CRS_WGS84 = DefaultGeographicCRS.WGS84;
78      private static final ReferencedEnvelope ENVELOPE_WGS84 = new ReferencedEnvelope(-180, 180, -90, 90, DefaultGeographicCRS.WGS84);
79      private static final String ZIP_EXTENSION = "zip";
80      private static final String SHP_EXTENSION = "shp";
81      private static final String PRJ_EXTENSION = "prj";
82      private static final String FILE_DATE_FORMAT = "yyyyMMdd";
83      private static final String OUTPUT_DATE_FORMAT = "dd/MM/yyyy";
84  
85      private String idAttributeName;
86      private String labelAttributeName;
87      private String nameAttributeName;
88      private String bathyAttributeName;
89      private String commentAttributeName;
90      private String posSystemIdAttributeName;
91      private String utFormatAttributeName;
92      private String daylightSavingTimeAttributeName;
93  
94      private QuadrigeConfiguration config;
95  
96      private MonitoringLocationService monitoringLocationService;
97      private MailSender mailSender;
98  
99      /**
100      * {@inheritDoc}
101      */
102     @Override
103     public void afterPropertiesSet() {
104         // Load config
105         this.config = QuadrigeConfiguration.getInstance();
106 
107         // Get shape attribute names
108         ApplicationConfig applicationConfig = config.getApplicationConfig();
109         idAttributeName = applicationConfig.getOption(ShapeConfigurationOption.SHAPE_ATTRIBUTE_ID.getKey());
110         labelAttributeName = applicationConfig.getOption(ShapeConfigurationOption.SHAPE_ATTRIBUTE_LABEL.getKey());
111         nameAttributeName = applicationConfig.getOption(ShapeConfigurationOption.SHAPE_ATTRIBUTE_NAME.getKey());
112         bathyAttributeName = applicationConfig.getOption(ShapeConfigurationOption.SHAPE_ATTRIBUTE_BATHY.getKey());
113         commentAttributeName = applicationConfig.getOption(ShapeConfigurationOption.SHAPE_ATTRIBUTE_COMMENT.getKey());
114         posSystemIdAttributeName = applicationConfig.getOption(ShapeConfigurationOption.SHAPE_ATTRIBUTE_POS_SYSTEM_ID.getKey());
115         utFormatAttributeName = applicationConfig.getOption(ShapeConfigurationOption.SHAPE_ATTRIBUTE_UT_FORMAT.getKey());
116         daylightSavingTimeAttributeName = applicationConfig.getOption(ShapeConfigurationOption.SHAPE_ATTRIBUTE_DAYLIGHT_SAVING_TIME.getKey());
117 
118     }
119 
120     @Override
121     public boolean importFromFile(Path inputFile, Path processingFile) {
122         checkInit();
123 
124         log.debug(t("quadrige3.batch.shape.process.start.debug", processingFile));
125 
126         ImportShapeContextVO context = new ImportShapeContextVO();
127         ImportShapeResultVO result = new ImportShapeResultVO();
128         context.setResult(result);
129         context.setInputFile(inputFile);
130         context.setProcessingFile(processingFile);
131         List<MonitoringLocationVO> importedMonitoringLocations = null;
132         boolean imported = false;
133 
134         try {
135 
136             // validate structure
137             validateStructure(context);
138 
139             // load features
140             List<SimpleFeatureCollection> featureCollections = null;
141             if (!result.hasError()) {
142                 featureCollections = loadFeatureCollection(context);
143             }
144 
145             // validate attributes
146             if (!result.hasError()) {
147                 validateAttributes(context, featureCollections);
148             }
149 
150             // validate data
151             if (!result.hasError()) {
152                 validateData(context, featureCollections);
153             }
154 
155             // Execute importation
156             if (!result.hasError()) {
157                 importedMonitoringLocations = execute(context, featureCollections);
158             }
159 
160             // Clean temp directories
161             fr.ifremer.quadrige3.core.dao.technical.Files.deleteQuietly(context.getTempDirs());
162 //            cleanTempDirectories(context);
163 
164             // Get result
165             if (result.hasError()) {
166                 String error = getResultErrorAsString(context);
167                 log.info(t("quadrige3.batch.shape.process.error", error));
168                 sendEmail(context, t("quadrige3.batch.shape.report.email.subject.error"), error);
169                 imported = false;
170             } else {
171                 String report = getResultReportAsString(context, importedMonitoringLocations);
172                 log.info(t("quadrige3.batch.shape.process.report", report));
173                 sendEmail(context, t("quadrige3.batch.shape.report.email.subject"), report);
174                 imported = true;
175             }
176 
177             log.debug(t("quadrige3.batch.shape.process.end.debug", inputFile));
178 
179         } catch (Exception e) {
180             // if exception happened, send email (Mantis #45096)
181             context.getResult().addError(ErrorCodes.FILE_NAME.name(), e.getLocalizedMessage());
182             String error = getResultErrorAsString(context);
183             log.info(t("quadrige3.batch.shape.process.error", error));
184             sendEmail(context, t("quadrige3.batch.shape.report.email.subject.error"), error);
185         }
186         return imported;
187     }
188 
189     private void cleanTempDirectories(ImportShapeContextVO context) throws IOException {
190 
191         for (Path tempDir : context.getTempDirs()) {
192 
193             // clean the temp directory
194             fr.ifremer.quadrige3.core.dao.technical.Files.cleanDirectory(tempDir, t("quadrige3.batch.shape.error.directory.clean", tempDir));
195 
196             // delete it
197             Files.delete(tempDir);
198         }
199     }
200 
201     private void sendEmail(ImportShapeContextVO context, String subject, String content) {
202 
203         try {
204 
205             String recipients = context.getEmail();
206             SimpleMailMessage message = new SimpleMailMessage();
207             message.setFrom(config.getMailSmtpSender());
208             message.setTo(Beans.split(recipients, ";").toArray(new String[0]));
209             message.setSubject(subject);
210             message.setText(content);
211             message.setSentDate(new Date());
212 
213             // send email
214             getMailSender().send(message);
215 
216         } catch (Exception e) {
217             throw new QuadrigeTechnicalException(t("quadrige3.batch.shape.error.email"), e);
218         }
219 
220     }
221 
222     private List<SimpleFeatureCollection> loadFeatureCollection(ImportShapeContextVO context) {
223         try {
224             List<SimpleFeatureCollection> featureCollections = new ArrayList<>();
225 
226             for (Path file : context.getFiles()) {
227                 FileDataStore inputStore = FileDataStoreFinder.getDataStore(file.toFile());
228                 if (inputStore instanceof ShapefileDataStore) {
229                     // Use cp1252 encoding to read correctly special characters (Mantis #46705)
230                     ((ShapefileDataStore) inputStore).setCharset(Charset.forName("cp1252"));
231                 }
232                 SimpleFeatureSource inputSource = inputStore.getFeatureSource();
233                 SimpleFeatureCollection featureCollection = inputSource.getFeatures();
234 
235                 // Log all features
236                 logFeatureCollection(featureCollection);
237 
238                 featureCollections.add(featureCollection);
239             }
240 
241             return featureCollections;
242 
243         } catch (Exception e) {
244             throw new QuadrigeTechnicalException(t("quadrige3.batch.shape.error.feature.read"), e);
245         }
246     }
247 
248     private void validateStructure(ImportShapeContextVO context) {
249         try {
250 
251             Assert.notNull(context);
252             Assert.notNull(context.getProcessingFile());
253 
254             if (!Files.exists(context.getProcessingFile())) {
255                 throw new FileNotFoundException(context.getProcessingFile().toString());
256             }
257             if (log.isDebugEnabled())
258                 log.debug(String.format("Validating shape file structure ... {%s}", context.getProcessingFile()));
259 
260             String fileName = context.getProcessingFile().getFileName().toString();
261             String extension = FilenameUtils.getExtension(fileName);
262 
263             if (extension.equalsIgnoreCase(ZIP_EXTENSION)) {
264 
265                 // Process the zip file name
266                 Matcher matcher = Pattern.compile("(.*)#SHP_(\\d+)_(.*)\\.\\w+").matcher(fileName);
267                 if (!matcher.matches()) {
268                     context.getResult().addError(ErrorCodes.FILE_NAME.name(), t("quadrige3.batch.shape.error.format.file", fileName));
269                     return;
270                 }
271 
272                 // Grab parts of file name
273                 String email = matcher.group(1);
274                 String strDate = matcher.group(2);
275                 String depositNumber = matcher.group(3);
276 
277                 // Validate email (not fully RFC 5322 but sufficient)
278                 if (!Pattern.compile("^[A-Za-z0-9+_.-]+@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$").matcher(email).matches()) {
279                     context.getResult().addError(ErrorCodes.FILE_NAME.name(), t("quadrige3.batch.shape.error.format.email", email));
280                     return;
281                 }
282 
283                 // Validate date YYYYMMDD
284                 Date date = Dates.safeParseDate(strDate, FILE_DATE_FORMAT);
285                 if (date == null) {
286                     context.getResult().addError(ErrorCodes.FILE_NAME.name(), t("quadrige3.batch.shape.error.format.date", strDate, FILE_DATE_FORMAT));
287                     return;
288                 }
289 
290                 // Set in context
291                 context.setEmail(email);
292                 context.setDepositDate(date);
293                 context.setDepositNumber(depositNumber);
294 
295                 // Uncompress input file into a temp dir and add shape files
296                 uncompressInputFile(context);
297 
298             } else if (extension.equalsIgnoreCase(SHP_EXTENSION)) {
299 
300                 // Process shape file directly (FOR TEST PURPOSE : context will not be valid)
301                 context.addFile(context.getProcessingFile());
302 
303             } else {
304 
305                 // unknown file extension
306                 context.getResult().addError(ErrorCodes.FILE_NAME.name(), t("quadrige3.batch.shape.error.unknown.file", fileName));
307             }
308 
309             // check prj files
310             checkPrjFiles(context);
311 
312         } catch (Exception e) {
313             throw new QuadrigeTechnicalException(t("quadrige3.batch.shape.error.structure"), e);
314         }
315     }
316 
317     private void uncompressInputFile(ImportShapeContextVO context) {
318 
319         try {
320             Path tempPath = config.getTempDirectory().toPath().resolve("shape_" + System.currentTimeMillis());
321             context.addTempDir(tempPath);
322             Files.createDirectories(tempPath);
323 
324             // uncompress
325             ZipUtils.uncompressFileToPath(context.getProcessingFile(), tempPath, false);
326 
327             // find shapes file recursively
328             Files.walk(tempPath)
329                     .filter(path -> FilenameUtils.isExtension(path.getFileName().toString().toLowerCase(), SHP_EXTENSION))
330                     .forEach(context::addFile);
331 
332         } catch (Exception e) {
333             throw new QuadrigeTechnicalException(t("quadrige3.batch.shape.error.uncompress"), e);
334         }
335     }
336 
337     private void validateAttributes(ImportShapeContextVO context, List<SimpleFeatureCollection> featureCollections) {
338 
339         try {
340 
341             if (log.isDebugEnabled())
342                 log.debug(String.format("Validating shape file attributes ... {%s}", context.getProcessingFile()));
343             ImportShapeResultVO result = context.getResult();
344 
345             for (SimpleFeatureCollection featureCollection : featureCollections) {
346 
347                 try (SimpleFeatureIterator ite = featureCollection.features()) {
348                     while (ite.hasNext()) {
349 
350                         SimpleFeature feature = ite.next();
351 
352                         // Check coordinate
353                         checkCoordinate(feature, result);
354 
355                         // Check attributes
356                         checkAttributes(feature, result);
357                     }
358                 }
359             }
360         } catch (Exception e) {
361             throw new QuadrigeTechnicalException(t("quadrige3.batch.shape.error.attribute"), e);
362         }
363     }
364 
365     private void validateData(ImportShapeContextVO context, List<SimpleFeatureCollection> featureCollections) {
366 
367         try {
368 
369             // Get input
370             if (log.isDebugEnabled())
371                 log.debug(String.format("Validating shape file data ... {%s}", context.getProcessingFile()));
372 
373             ImportShapeResultVO result = context.getResult();
374             List<MonitoringLocationVO> existingMonitoringLocations = getMonitoringLocationService().getLightMonitoringLocations();
375             List<Integer> existingPosIds = getMonitoringLocationService().getPositioningSystemIds();
376 
377             for (SimpleFeatureCollection featureCollection : featureCollections) {
378                 try (SimpleFeatureIterator ite = featureCollection.features()) {
379                     while (ite.hasNext()) {
380                         SimpleFeature feature = ite.next();
381 
382                         Integer id = getIntegerAttribute(feature, idAttributeName);
383 
384                         if (id == null) {
385 
386                             // If no Id : new location
387                             validateDataForInsert(feature, existingMonitoringLocations, result);
388 
389                         } else {
390 
391                             // Id exists : update location
392                             validateDataForUpdate(feature, id, existingMonitoringLocations, result);
393 
394                         }
395 
396                         // Check positioning system id
397                         Integer posId = getIntegerAttribute(feature, posSystemIdAttributeName);
398                         if (!existingPosIds.contains(posId)) {
399                             result.addError(feature.getID(), t("quadrige3.batch.shape.error.attribute.notExists", posSystemIdAttributeName, posId));
400                         }
401                     }
402                 }
403             }
404         } catch (Exception e) {
405             throw new QuadrigeTechnicalException(t("quadrige3.batch.shape.error.attribute.data"), e);
406         }
407     }
408 
409     private void validateDataForUpdate(SimpleFeature feature, Integer id, List<MonitoringLocationVO> existingMonitoringLocations, ImportShapeResultVO result) {
410 
411         // Check id
412         if (existingMonitoringLocations.stream()
413                 .noneMatch(monitoringLocationVO -> monitoringLocationVO.getId().equals(id))) {
414             result.addError(feature.getID(), t("quadrige3.batch.shape.error.attribute.notExists", idAttributeName, id));
415         }
416 
417         // Check label on other entities
418         String label = getStringAttribute(feature, labelAttributeName);
419         if (StringUtils.isNotBlank(label)
420                 && existingMonitoringLocations.stream().filter(monitoringLocationVO -> !monitoringLocationVO.getId().equals(id))
421                 .map(MonitoringLocationVO::getLabel).filter(Objects::nonNull)
422                 .anyMatch(label::equalsIgnoreCase)) {
423             result.addError(feature.getID(), t("quadrige3.batch.shape.error.attribute.alreadyExists", labelAttributeName, label));
424         }
425 
426         // Check name on other entities
427         String name = getStringAttribute(feature, nameAttributeName);
428         assert name != null;
429         if (existingMonitoringLocations.stream().filter(monitoringLocationVO -> !monitoringLocationVO.getId().equals(id))
430                 .map(MonitoringLocationVO::getName)
431                 .anyMatch(name::equalsIgnoreCase)) {
432             result.addError(feature.getID(), t("quadrige3.batch.shape.error.attribute.alreadyExists", nameAttributeName, name));
433         }
434 
435     }
436 
437     private void validateDataForInsert(SimpleFeature feature, List<MonitoringLocationVO> existingMonitoringLocations, ImportShapeResultVO result) {
438 
439         // Check label
440         String label = getStringAttribute(feature, labelAttributeName);
441         if (StringUtils.isNotBlank(label)
442                 && existingMonitoringLocations.stream()
443                 .map(MonitoringLocationVO::getLabel).filter(Objects::nonNull)
444                 .anyMatch(label::equalsIgnoreCase)) {
445             result.addError(feature.getID(), t("quadrige3.batch.shape.error.attribute.alreadyExists", labelAttributeName, label));
446         }
447 
448         // Check name
449         String name = getStringAttribute(feature, nameAttributeName);
450         assert name != null;
451         if (existingMonitoringLocations.stream()
452                 .map(MonitoringLocationVO::getName)
453                 .anyMatch(name::equalsIgnoreCase)) {
454             result.addError(feature.getID(), t("quadrige3.batch.shape.error.attribute.alreadyExists", nameAttributeName, name));
455         }
456 
457     }
458 
459     private List<MonitoringLocationVO> execute(ImportShapeContextVO context, List<SimpleFeatureCollection> featureCollections) {
460 
461         try {
462 
463             // Build MonitoringLocationVO list
464             List<MonitoringLocationVO> list = new ArrayList<>();
465 
466             for (SimpleFeatureCollection featureCollection : featureCollections) {
467                 try (SimpleFeatureIterator ite = featureCollection.features()) {
468                     while (ite.hasNext()) {
469                         SimpleFeature feature = ite.next();
470 
471                         MonitoringLocationVO vo = new MonitoringLocationVO();
472                         vo.setFeatureId(feature.getID());
473                         vo.setId(getIntegerAttribute(feature, idAttributeName, 0)); // 0 is considered as null (Mantis #41594)
474                         vo.setLabel(getStringAttribute(feature, labelAttributeName));
475                         vo.setName(getStringAttribute(feature, nameAttributeName));
476                         vo.setBathy(getDoubleAttribute(feature, bathyAttributeName));
477                         vo.setComment(getStringAttribute(feature, commentAttributeName));
478                         vo.setPosId(getIntegerAttribute(feature, posSystemIdAttributeName));
479                         vo.setUtFormat(getIntegerAttribute(feature, utFormatAttributeName));
480                         vo.setDaylightSavingTime(getIntegerAttribute(feature, daylightSavingTimeAttributeName) == 1);
481                         vo.setGeometry(Geometries.getWKTString((Geometry) feature.getDefaultGeometry()));
482 
483                         list.add(vo);
484                     }
485                 }
486             }
487 
488             if (CollectionUtils.isNotEmpty(list)) {
489 
490                 // Keep ids for updates operation
491                 List<Integer> entityToUpdateIds = list.stream().map(MonitoringLocationVO::getId).filter(Objects::nonNull).collect(Collectors.toList());
492 
493                 // Call monitoringLocationService
494                 getMonitoringLocationService().importLocationsWithShapes(list);
495 
496                 // Set success messages (for add and update operation)
497                 list.forEach(monitoringLocation ->
498                 {
499                     String i18nMessage = entityToUpdateIds.contains(monitoringLocation.getId()) ? "quadrige3.batch.shape.report.update" : "quadrige3.batch.shape.report.add";
500                     context.getResult().addMessage(
501                             monitoringLocation.getFeatureId(),
502                             t(i18nMessage,
503                                     idAttributeName, monitoringLocation.getId(),
504                                     labelAttributeName, monitoringLocation.getLabel()));
505                 });
506 
507             } else {
508 
509                 context.getResult().addError(ErrorCodes.NO_DATA.name(), t("quadrige3.batch.shape.error.noData"));
510             }
511 
512             return list;
513 
514         } catch (Exception e) {
515             throw new QuadrigeTechnicalException(t("quadrige3.batch.shape.error.import"), e);
516         }
517     }
518 
519 
520 
521 
522     /* -- internal methods -- */
523 
524     private MonitoringLocationService getMonitoringLocationService() {
525         if (monitoringLocationService == null) {
526             monitoringLocationService = BatchesServiceLocator.getMonitoringLocationService();
527         }
528         return monitoringLocationService;
529     }
530 
531     private MailSender getMailSender() {
532         if (mailSender == null) {
533             mailSender = BatchesServiceLocator.getMailSender();
534         }
535         return mailSender;
536     }
537 
538     private void logFeatureCollection(SimpleFeatureCollection featureCollection) {
539         if (log.isDebugEnabled()) {
540             try (SimpleFeatureIterator ite = featureCollection.features()) {
541                 while (ite.hasNext()) {
542 
543                     SimpleFeature feature = ite.next();
544                     StringJoiner stringJoiner = new StringJoiner(" | ", feature.getID() + " : ", "");
545                     feature.getType().getAttributeDescriptors().forEach(
546                             attributeDescriptor -> stringJoiner.add(attributeDescriptor.getLocalName() + " = " + feature.getAttribute(attributeDescriptor.getLocalName())));
547                     log.debug(stringJoiner.toString());
548                 }
549             }
550         }
551     }
552 
553     private void checkPrjFiles(ImportShapeContextVO context) {
554 
555         ImportShapeResultVO result = context.getResult();
556 
557         for (Path file : context.getFiles()) {
558 
559             // Check file exists
560             Path prjFile = file.resolveSibling(FilenameUtils.getBaseName(file.getFileName().toString()) + "." + PRJ_EXTENSION);
561             if (!Files.exists(prjFile)) {
562                 result.addError(ErrorCodes.PROJECTION.name(), t("quadrige3.batch.shape.error.projection.missing"));
563                 return;
564             }
565 
566             // Check projection = WGS84
567             try {
568                 String projection = FileUtils.readFileToString(prjFile.toFile(), "UTF8");
569                 CoordinateReferenceSystem crs = CRS.parseWKT(projection);
570 
571                 if (!CRS.equalsIgnoreMetadata(CRS_WGS84, crs)) {
572                     result.addError(ErrorCodes.PROJECTION.name(), t("quadrige3.batch.shape.error.projection.notWGS84", crs.getName()));
573                 }
574 
575             } catch (FactoryException e) {
576                 if (log.isDebugEnabled()) log.debug(e);
577                 result.addError(ErrorCodes.PROJECTION.name(), t("quadrige3.batch.shape.error.projection.badFormat"));
578             } catch (IOException e) {
579                 if (log.isDebugEnabled()) log.debug(e);
580                 result.addError(ErrorCodes.PROJECTION.name(), t("quadrige3.batch.shape.error.projection.read"));
581             }
582         }
583     }
584 
585     private void checkCoordinate(SimpleFeature feature, ImportShapeResultVO result) {
586 
587         Geometry geometry = (Geometry) feature.getDefaultGeometry();
588         Envelope envelope = geometry.getEnvelopeInternal();
589 
590         if (!ENVELOPE_WGS84.contains(envelope)) {
591             result.addError(feature.getID(), t("quadrige3.batch.shape.error.coordinate.oversized", envelope.toString(), ENVELOPE_WGS84.toString()));
592         }
593 
594         if (geometry instanceof MultiPoint) {
595             int nbPoints = geometry.getNumPoints();
596             if (nbPoints > 1) {
597                 result.addError(feature.getID(), t("quadrige3.batch.shape.error.coordinate.moreThanOnePoint", nbPoints, 1));
598             }
599         }
600     }
601 
602     private void checkAttributes(final SimpleFeature feature, final ImportShapeResultVO result) {
603 
604         boolean isValid;
605 
606         // Id
607         checkIntegerAttribute(feature, idAttributeName, false, result);
608 
609         // Label
610         checkStringAttribute(feature, labelAttributeName, false, 50, result);
611         String label = getStringAttribute(feature, labelAttributeName);
612         if (StringUtils.isNotBlank(label) && !label.contains("P") && !label.contains("L") && !label.contains("S")) {
613             result.addError(feature.getID(), t("quadrige3.batch.shape.error.attribute.format", labelAttributeName, label));
614         }
615 
616         // Name
617         checkStringAttribute(feature, nameAttributeName, true, 100, result);
618 
619         // Bathy
620         checkDecimalAttribute(feature, bathyAttributeName, false, 2, result);
621 
622         // Comment
623         checkStringAttribute(feature, commentAttributeName, false, 2000, result);
624 
625         // Pos system id
626         checkIntegerAttribute(feature, posSystemIdAttributeName, true, result);
627 
628         // Ut format
629         isValid = checkIntegerAttribute(feature, utFormatAttributeName, true, result);
630         if (isValid) {
631             int utFormat = getIntegerAttribute(feature, utFormatAttributeName);
632             if (utFormat < -12 || utFormat > 12) {
633                 result.addError(feature.getID(), t("quadrige3.batch.shape.error.attribute.minMax", utFormatAttributeName, -12, 12));
634             }
635         }
636 
637         // Daylight saving time
638         isValid = checkIntegerAttribute(feature, daylightSavingTimeAttributeName, true, result);
639         if (isValid) {
640             int daylightSavingTime = getIntegerAttribute(feature, daylightSavingTimeAttributeName);
641             if (daylightSavingTime < 0 || daylightSavingTime > 1) {
642                 result.addError(feature.getID(), t("quadrige3.batch.shape.error.attribute.intBoolean", daylightSavingTimeAttributeName));
643             }
644         }
645 
646     }
647 
648     private void checkInit() {
649         if (config == null) {
650             try {
651                 afterPropertiesSet();
652             } catch (Exception e) {
653                 throw new QuadrigeTechnicalException(e);
654             }
655         }
656     }
657 
658     private boolean checkStringAttribute(final SimpleFeature feature,
659                                          final String attributeName,
660                                          final boolean mandatory,
661                                          final int maxLength,
662                                          final ImportShapeResultVO result) {
663 
664         Object value = feature.getAttribute(attributeName);
665         String strValue = value != null ? value.toString().trim() : null;
666 
667         // Check mandatory
668         if (StringUtils.isBlank(strValue)) {
669             if (mandatory) {
670                 result.addError(feature.getID(), t("quadrige3.batch.shape.error.attribute.missingMandatory", attributeName));
671             }
672             return !mandatory;
673         }
674 
675         // Check max length
676         if (strValue.length() > maxLength) {
677             result.addError(feature.getID(), t("quadrige3.batch.shape.error.attribute.maxLength", attributeName, maxLength));
678             return false;
679         }
680 
681         return true;
682     }
683 
684     private boolean checkDecimalAttribute(final SimpleFeature feature,
685                                           final String attributeName,
686                                           final boolean mandatory,
687                                           final int maxDecimal,
688                                           final ImportShapeResultVO result) {
689 
690         Object value = feature.getAttribute(attributeName);
691         String strValue = value != null ? value.toString().trim() : null;
692         if (StringUtils.isBlank(strValue)) {
693             if (mandatory) {
694                 result.addError(feature.getID(), t("quadrige3.batch.shape.error.attribute.missingMandatory", attributeName));
695             }
696             return !mandatory;
697         }
698 
699         // Check parsable
700         try {
701             Number number = DecimalFormat.getInstance().parse(strValue);
702 
703             // Check max decimal
704             double tmpValue = number.doubleValue() * Math.pow(10, maxDecimal);
705             if (Math.ceil(tmpValue) != tmpValue) {
706                 result.addError(feature.getID(), t("quadrige3.batch.shape.error.attribute.maxDecimal", attributeName, maxDecimal));
707                 return false;
708             }
709         } catch (ParseException e) {
710             result.addError(feature.getID(), t("quadrige3.batch.shape.error.attribute.decimal", attributeName, strValue));
711             return false;
712         }
713 
714         return true;
715     }
716 
717     private boolean checkIntegerAttribute(final SimpleFeature feature,
718                                           final String attributeName,
719                                           final boolean mandatory,
720                                           final ImportShapeResultVO result) {
721 
722         Object value = feature.getAttribute(attributeName);
723 
724         if (value instanceof CharSequence) {
725             result.addError(feature.getID(), t("quadrige3.batch.shape.error.attribute.type", attributeName,
726                     String.format("%s as %s", value.toString(), value.getClass().getName())));
727             return false;
728         }
729 
730         if (value == null) {
731             if (mandatory) {
732                 result.addError(feature.getID(), t("quadrige3.batch.shape.error.attribute.missingMandatory", attributeName));
733             }
734             return !mandatory;
735         }
736 
737         // Check integer value
738         Number number = (Number) value;
739         if (number.doubleValue() - number.intValue() > 0) {
740             result.addError(feature.getID(), t("quadrige3.batch.shape.error.attribute.integer", attributeName, number.toString()));
741         }
742 
743         return true;
744     }
745 
746     /**
747      * Get the integer value of the attribute
748      *
749      * @param feature       the feature
750      * @param attributeName the attribute name
751      * @return the Integer value of the attribute in feature
752      */
753     private Integer getIntegerAttribute(final SimpleFeature feature, final String attributeName) {
754         return getIntegerAttribute(feature, attributeName, null);
755     }
756 
757     /**
758      * Like getIntegerAttribute(feature, attributeName) but return also 'null' if the attribute value equals valueIsNull
759      *
760      * @param feature       the feature
761      * @param attributeName the attribute name
762      * @param valueIsNull   the Integer value considered as null (e.g. 0)
763      * @return the Integer value of the attribute in feature or null if equals to valueIsNull
764      */
765     private Integer getIntegerAttribute(final SimpleFeature feature, final String attributeName, final Integer valueIsNull) {
766         Object value = feature.getAttribute(attributeName);
767         Integer integer = value != null ? ((Number) value).intValue() : null;
768         // If the integer value equals the value that should be null, then set null
769         if (integer != null && integer.equals(valueIsNull)) integer = null;
770         return integer;
771     }
772 
773     private Double getDoubleAttribute(final SimpleFeature feature, final String attributeName) {
774         Object value = feature.getAttribute(attributeName);
775 
776         if (value instanceof String) {
777             if (StringUtils.isBlank((CharSequence) value)) return null;
778             try {
779                 return DecimalFormat.getInstance().parse((String) value).doubleValue();
780             } catch (ParseException e) {
781                 log.error(e.getMessage());
782                 return null;
783             }
784         } else if (value instanceof Number) {
785             return ((Number) value).doubleValue();
786         }
787 
788         return null;
789     }
790 
791     private String getStringAttribute(final SimpleFeature feature, final String attributeName) {
792         Object value = feature.getAttribute(attributeName);
793         return value != null ? (String) value : null;
794     }
795 
796     private String getResultErrorAsString(ImportShapeContextVO context) {
797 
798         StringBuilder sb = new StringBuilder();
799 
800         sb.append(t("quadrige3.batch.shape.report.file", context.getInputFile())).append('\n');
801         sb.append(t("quadrige3.batch.shape.report.deposit", Dates.formatDate(context.getDepositDate(), OUTPUT_DATE_FORMAT), context.getEmail())).append('\n');
802 
803         for (String featureId : context.getResult().getErrors().keySet()) {
804 
805             sb.append(t("quadrige3.batch.shape.report.location", featureId)).append('\n');
806             for (String error : context.getResult().getErrors().get(featureId)) {
807                 sb.append(" - ").append(error).append('\n');
808             }
809         }
810 
811         return sb.toString();
812     }
813 
814     private String getResultReportAsString(ImportShapeContextVO context, List<MonitoringLocationVO> list) {
815 
816         StringBuilder sb = new StringBuilder();
817 
818         sb.append(t("quadrige3.batch.shape.report.file", context.getInputFile())).append('\n');
819         sb.append(t("quadrige3.batch.shape.report.deposit", Dates.formatDate(context.getDepositDate(), OUTPUT_DATE_FORMAT), context.getEmail())).append('\n');
820         sb.append(t("quadrige3.batch.shape.report.nbImport", CollectionUtils.size(list))).append("\n\n");
821 
822         for (String featureId : context.getResult().getMessages().keySet()) {
823 
824             sb.append(t("quadrige3.batch.shape.report.location", featureId)).append('\n');
825             for (String message : context.getResult().getMessages().get(featureId)) {
826                 sb.append(" - ").append(message).append('\n');
827             }
828         }
829 
830         return sb.toString();
831     }
832 
833 }