View Javadoc
1   package net.sumaris.core.extraction.service;
2   
3   /*-
4    * #%L
5    * SUMARiS:: Core Extraction
6    * %%
7    * Copyright (C) 2018 - 2019 SUMARiS Consortium
8    * %%
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU General Public License as
11   * published by the Free Software Foundation, either version 3 of the
12   * License, or (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 General Public
20   * License along with this program.  If not, see
21   * <http://www.gnu.org/licenses/gpl-3.0.html>.
22   * #L%
23   */
24  
25  import com.google.common.base.Preconditions;
26  import com.google.common.collect.ImmutableList;
27  import net.sumaris.core.dao.technical.SortDirection;
28  import net.sumaris.core.dao.technical.extraction.ExtractionProductDao;
29  import net.sumaris.core.exception.SumarisTechnicalException;
30  import net.sumaris.core.extraction.dao.technical.table.ExtractionTableDao;
31  import net.sumaris.core.extraction.dao.trip.rdb.AggregationRdbTripDao;
32  import net.sumaris.core.extraction.utils.ExtractionBeans;
33  import net.sumaris.core.extraction.vo.*;
34  import net.sumaris.core.extraction.vo.filter.AggregationTypeFilterVO;
35  import net.sumaris.core.extraction.vo.trip.rdb.AggregationRdbTripContextVO;
36  import net.sumaris.core.model.referential.StatusEnum;
37  import net.sumaris.core.util.Beans;
38  import net.sumaris.core.util.StringUtils;
39  import net.sumaris.core.vo.technical.extraction.*;
40  import org.apache.commons.collections4.CollectionUtils;
41  import org.apache.commons.collections4.ListUtils;
42  import org.apache.commons.collections4.MapUtils;
43  import org.apache.commons.collections4.SetUtils;
44  import org.apache.commons.lang3.ArrayUtils;
45  import org.nuiton.i18n.I18n;
46  import org.slf4j.Logger;
47  import org.slf4j.LoggerFactory;
48  import org.springframework.beans.factory.annotation.Autowired;
49  import org.springframework.context.annotation.Lazy;
50  import org.springframework.dao.DataRetrievalFailureException;
51  import org.springframework.stereotype.Service;
52  
53  import javax.annotation.Nullable;
54  import java.util.*;
55  import java.util.stream.Collector;
56  import java.util.stream.Collectors;
57  
58  /**
59   * @author peck7 on 17/12/2018.
60   */
61  @Service("aggregationService")
62  @Lazy
63  public class AggregationServiceImpl implements AggregationService {
64  
65      private static final Logger log = LoggerFactory.getLogger(AggregationServiceImpl.class);
66  
67      @Autowired
68      private ExtractionService extractionService;
69  
70      @Autowired
71      private AggregationRdbTripDao aggregationRdbTripDao;
72  
73      @Autowired
74      private ExtractionProductDao extractionProductDao;
75  
76      @Autowired
77      private ExtractionTableDao extractionTableDao;
78  
79      @Override
80      public List<AggregationTypeVO> findByFilter(AggregationTypeFilterVO filter, ProductFetchOptions fetchOptions) {
81  
82          final AggregationTypeFilterVOrVO notNullFilter = filter != null ? filter : new AggregationTypeFilterVO();
83  
84          return ListUtils.emptyIfNull(getProductAggregationTypes(notNullFilter, fetchOptions))
85                  .stream()
86                  .filter(t -> notNullFilter.getIsSpatial() == null || Objects.equals(notNullFilter.getIsSpatial(), t.getIsSpatial()))
87                  .collect(Collectors.toList());
88      }
89  
90      @Override
91      public AggregationTypeVO get(int id, ProductFetchOptions fetchOptions) {
92          ExtractionProductVO result = extractionProductDao.get(id, fetchOptions)
93                  .orElseThrow(() -> new DataRetrievalFailureException(String.format("Unknown aggregation type {%s}", id)));
94          return toAggregationType(result);
95      }
96  
97      @Override
98      public AggregationContextVO execute(AggregationTypeVO type, ExtractionFilterVO filter) {
99          AggregationTypeVO checkedType = ExtractionBeans.checkAndFindType(getAllAggregationTypes(null), type);
100         ExtractionCategoryEnum category = ExtractionCategoryEnum.valueOf(checkedType.getCategory().toUpperCase());
101         ExtractionProductVO source;
102 
103         switch (category) {
104             case PRODUCT:
105                 // Get the product VO
106                 source = extractionProductDao.getByLabel(checkedType.getFormat(), ProductFetchOptions.MINIMAL);
107                 // Execute, from product
108                 return executeProduct(source, filter);
109 
110             case LIVE:
111                 // First execute the raw extraction
112                 ExtractionContextVO rawExtractionContext = extractionService.execute(checkedType, filter);
113 
114                 try {
115                     source = extractionService.toProductVO(rawExtractionContext);
116                     ExtractionFilterVO aggregationFilter = null;
117                     if (filter != null) {
118                         aggregationFilter = new ExtractionFilterVO();
119                         aggregationFilter.setSheetName(filter.getSheetName());
120                     }
121 
122                     // Execute, from product
123                     return executeProduct(source, aggregationFilter);
124                 } finally {
125                     // Clean intermediate tables
126                     asyncClean(rawExtractionContext);
127                 }
128             default:
129                 throw new SumarisTechnicalException(String.format("Aggregation on category %s not implemented yet !", type.getCategory()));
130         }
131     }
132 
133     @Override
134     public AggregationResultVO read(AggregationTypeVO type, @Nullable ExtractionFilterVO filter, @Nullable AggregationStrataVO strata, int offset, int size, String sort, SortDirection direction) {
135         Preconditions.checkNotNull(type);
136 
137         ExtractionProductVO product = extractionProductDao.getByLabel(type.getLabel(), ProductFetchOptions.MINIMAL_WITH_TABLES);
138         AggregationContextVO context = toContextVO(product);
139 
140         return read(context, filter, strata, offset, size, sort, direction);
141     }
142 
143     @Override
144     public AggregationResultVO read(AggregationContextVO context, ExtractionFilterVO filter, AggregationStrataVO strata,
145                                     int offset, int size, String sort, SortDirection direction) {
146         filter = filter != null ? filter : new ExtractionFilterVO();
147         strata = strata != null ? strata : new AggregationStrataVO();
148 
149         String tableName;
150         if (StringUtils.isNotBlank(filter.getSheetName())) {
151             tableName = context.getTableNameBySheetName(filter.getSheetName());
152         } else {
153             tableName = context.getTableNames().iterator().next();
154         }
155 
156         // Missing the expected sheet = no data
157         if (tableName == null) return createEmptyResult();
158 
159         // Read the data
160         ExtractionRawFormatEnum format = ExtractionBeans.getFormat(context);
161         switch (format) {
162             case RDB:
163             case COST:
164             case SURVIVAL_TEST:
165                 return aggregationRdbTripDao.read(tableName, filter, strata, offset, size, sort, direction);
166             default:
167                 throw new SumarisTechnicalException(String.format("Unable to read data on type '%s': not implemented", context.getLabel()));
168         }
169 
170     }
171 
172     @Override
173     public AggregationResultVO executeAndRead(AggregationTypeVO type, ExtractionFilterVO filter, AggregationStrataVO strata,
174                                               int offset, int size, String sort, SortDirection direction) {
175         // Execute the aggregation
176         AggregationContextVO context = execute(type, filter);
177 
178         // Prepare the read filter
179         ExtractionFilterVO readFilter = null;
180         if (filter != null) {
181             readFilter = new ExtractionFilterVO();
182             readFilter.setSheetName(filter.getSheetName());
183         }
184 
185         try {
186             // Read data
187             return read(context, readFilter, strata, offset, size, sort, direction);
188         } finally {
189             // Clean created tables
190             asyncClean(context);
191         }
192     }
193 
194     @Override
195     public AggregationTypeVO../../../net/sumaris/core/extraction/vo/AggregationTypeVO.html#AggregationTypeVO">AggregationTypeVO save(AggregationTypeVO type, @Nullable ExtractionFilterVO filter) {
196         Preconditions.checkNotNull(type);
197         Preconditions.checkNotNull(type.getLabel());
198         Preconditions.checkNotNull(type.getName());
199 
200         Preconditions.checkArgument(!Objects.equals(type.getLabel(), type.getFormat()), "Invalid label. Expected pattern: <type_name>-NNN");
201 
202         // Load the product
203         ExtractionProductVO target = null;
204         if (type.getId() != null) {
205             target = extractionProductDao.get(type.getId(), ProductFetchOptions.FOR_UPDATE)
206                     .orElse(null);
207         }
208 
209         boolean isNew = target == null;
210         if (isNew) {
211             target = new ExtractionProductVO();
212             target.setLabel(type.getLabel());
213         }
214 
215         // Applying a new execution
216         if (isNew || filter != null) {
217 
218             // Prepare a executable type (with label=format)
219             AggregationTypeVOpeVO.html#AggregationTypeVO">AggregationTypeVO executableType = new AggregationTypeVO();
220             executableType.setLabel(type.getFormat());
221             executableType.setCategory(type.getCategory());
222 
223             // Execute the aggregation
224             AggregationContextVO context = execute(executableType, filter);
225 
226             // Update product tables, using the aggregation result
227             toProductVO(context, target);
228 
229             // Copy some properties from the given type
230             target.setName(type.getName());
231             target.setUpdateDate(type.getUpdateDate());
232             target.setDescription(type.getDescription());
233             target.setStatusId(type.getStatusId());
234             target.setRecorderDepartment(type.getRecorderDepartment());
235             target.setRecorderPerson(type.getRecorderPerson());
236             target.setStratum(type.getStratum());
237         }
238 
239         // Aggregation already exists, and not new execution need: just save it
240         else {
241             Preconditions.checkArgument(StringUtils.equalsIgnoreCase(target.getLabel(), type.getLabel()), "Cannot update the label of an existing product");
242             target.setName(type.getName());
243             target.setUpdateDate(type.getUpdateDate());
244             target.setDescription(type.getDescription());
245             target.setComments(type.getComments());
246             target.setStatusId(type.getStatusId());
247             target.setUpdateDate(type.getUpdateDate());
248             target.setIsSpatial(type.getIsSpatial());
249             target.setStratum(type.getStratum());
250         }
251 
252         // Save the product
253         target = extractionProductDao.save(target);
254 
255         // Transform back to type
256         return toAggregationType(target);
257     }
258 
259     @Override
260     public void delete(int id) {
261         extractionProductDao.delete(id);
262     }
263 
264     @Override
265     public List<ExtractionProductColumnVO> getColumnsBySheetName(AggregationTypeVO type, String sheetName) {
266         Preconditions.checkNotNull(type);
267         Preconditions.checkArgument(type.getId() != null || type.getLabel() != null, "Missing type.id or type.label");
268 
269         Integer productId = type.getId();
270         if (productId == null) {
271             ExtractionProductVO product = extractionProductDao.getByLabel(type.getLabel(), ProductFetchOptions.MINIMAL);
272             productId = product.getId();
273         }
274         // Try to get columns from the DB
275         List<ExtractionProductColumnVO> dataColumns = null;
276         if (StringUtils.isNotBlank(sheetName)) {
277             dataColumns = extractionProductDao.getColumnsByIdAndTableLabel(productId, sheetName);
278         }
279 
280         // If nothing in the DB, get metadata from a fake extraction
281         if (CollectionUtils.isEmpty(dataColumns)) {
282             ExtractionTypeVOtionTypeVO.html#ExtractionTypeVO">ExtractionTypeVO readType = new ExtractionTypeVO();
283             readType.setCategory(ExtractionCategoryEnum.PRODUCT.name().toLowerCase());
284             readType.setLabel(type.getLabel());
285             ExtractionFilterVOFilterVO.html#ExtractionFilterVO">ExtractionFilterVO readFilter = new ExtractionFilterVO();
286             readFilter.setSheetName(sheetName);
287             ExtractionResultVO res = extractionService.executeAndRead(readType, readFilter, 0, 1, null, null);
288 
289             dataColumns = res.getColumns();
290         }
291 
292         return dataColumns;
293     }
294 
295     /* -- protected -- */
296 
297     protected List<AggregationTypeVO> getAllAggregationTypes(ProductFetchOptions fetchOptions) {
298         return ImmutableList.<AggregationTypeVO>builder()
299                 .addAll(getProductAggregationTypes(fetchOptions))
300                 .addAll(getLiveAggregationTypes())
301                 .build();
302     }
303     protected List<AggregationTypeVO> getProductAggregationTypes(ProductFetchOptions fetchOptions) {
304         return getProductAggregationTypes(null, fetchOptions);
305     }
306 
307     protected List<AggregationTypeVO> getProductAggregationTypes(@Nullable AggregationTypeFilterVO filter, ProductFetchOptions fetchOptions) {
308         filter = filter != null ? filter : new AggregationTypeFilterVO();
309 
310         // Exclude types with a DISABLE status, by default
311         if (ArrayUtils.isEmpty(filter.getStatusIds())) {
312             filter.setStatusIds(new Integer[]{StatusEnum.ENABLE.getId(), StatusEnum.TEMPORARY.getId()});
313         }
314 
315         return ListUtils.emptyIfNull(extractionProductDao.findByFilter(filter, fetchOptions))
316                 .stream()
317                 .map(this::toAggregationType)
318                 .collect(Collectors.toList());
319     }
320 
321     protected List<AggregationTypeVO> getLiveAggregationTypes() {
322         return Arrays.stream(ExtractionRawFormatEnum.values())
323                 .map(format -> {
324                     AggregationTypeVOregationTypeVO.html#AggregationTypeVO">AggregationTypeVO type = new AggregationTypeVO();
325                     type.setLabel(format.name().toLowerCase());
326                     type.setCategory(ExtractionCategoryEnum.LIVE.name().toLowerCase());
327                     type.setSheetNames(format.getSheetNames());
328                     return type;
329                 })
330                 .collect(Collectors.toList());
331     }
332 
333     protected AggregationResultVO createEmptyResult() {
334         AggregationResultVOtionResultVO.html#AggregationResultVO">AggregationResultVO result = new AggregationResultVO();
335         result.setColumns(ImmutableList.of());
336         result.setTotal(0);
337         result.setRows(ImmutableList.of());
338         return result;
339     }
340 
341     public AggregationContextVO executeProduct(ExtractionProductVO source, ExtractionFilterVO filter) {
342         Preconditions.checkNotNull(source);
343         Preconditions.checkNotNull(source.getLabel());
344 
345         ExtractionRawFormatEnum format = ExtractionBeans.getFormat(source);
346 
347         switch (format) {
348             case RDB:
349             case COST:
350             case FREE:
351             case SURVIVAL_TEST:
352                 return aggregationRdbTripDao.aggregate(source, filter);
353             default:
354                 throw new SumarisTechnicalException(String.format("Data aggregation on type '%s' is not implemented !", format.name()));
355         }
356     }
357 
358     protected void asyncClean(ExtractionContextVO context) {
359         if (context == null) return;
360         extractionService.asyncClean(context);
361     }
362 
363     protected AggregationTypeVO toAggregationType(ExtractionProductVO source) {
364         AggregationTypeVOgationTypeVO.html#AggregationTypeVO">AggregationTypeVO target = new AggregationTypeVO();
365 
366         Beans.copyProperties(source, target);
367 
368         // Change label and category to lowercase (better for UI client)
369         target.setCategory(ExtractionCategoryEnum.PRODUCT.name().toLowerCase());
370         target.setLabel(source.getLabel().toLowerCase());
371 
372         Collection<String> sheetNames = source.getSheetNames();
373         if (CollectionUtils.isNotEmpty(sheetNames)) {
374             target.setSheetNames(sheetNames.toArray(new String[sheetNames.size()]));
375         }
376 
377         if (CollectionUtils.isNotEmpty(source.getStratum())) {
378             target.setStratum(source.getStratum());
379         }
380 
381         return target;
382     }
383 
384 
385     protected void toProductVO(AggregationContextVO source, ExtractionProductVO target) {
386 
387         target.setLabel(source.getLabel().toUpperCase() + "-" + source.getId());
388         target.setName(String.format("Aggregation #%s", source.getId()));
389         target.setIsSpatial(source.isSpatial());
390 
391         target.setTables(toProductTableVO(source));
392     }
393 
394     protected List<ExtractionProductTableVO> toProductTableVO(AggregationContextVO source) {
395 
396         return SetUtils.emptyIfNull(source.getTableNames())
397                 .stream()
398                 .map(tableName -> {
399                     ExtractionProductTableVOxtractionProductTableVO.html#ExtractionProductTableVO">ExtractionProductTableVO table = new ExtractionProductTableVO();
400                     table.setTableName(tableName);
401 
402                     // Label (=the sheet name)
403                     String label = source.getSheetName(tableName);
404                     table.setLabel(label);
405 
406                     // Name: generated using i18n
407                     String name = getI18nSheetName(source.getFormatName(), label);
408                     table.setName(name);
409 
410                     table.setIsSpatial(source.hasSpatialColumn(tableName));
411 
412                     // Columns
413                     List<ExtractionProductColumnVO> columns = toProductColumnVOs(source, tableName);
414                     table.setColumns(columns);
415 
416                     return table;
417                 })
418                 .collect(Collectors.toList());
419     }
420 
421     protected List<ExtractionProductColumnVO> toProductColumnVOs(AggregationContextVO context, String tableName) {
422 
423         Set<String> hiddenColumns = Beans.getSet(context.getHiddenColumns(tableName));
424         Map<String, List<String>> columnValues = context.getColumnValues(tableName);
425 
426         // Get columns (from table metadata), but exclude hidden columns
427         List<ExtractionProductColumnVO> columns = Beans.getStream(extractionTableDao.getColumns(tableName))
428                 .filter(column -> !hiddenColumns.contains(column.getColumnName()))
429                 .collect(Collectors.toList());
430 
431         // Set values on each columns
432         if (CollectionUtils.isNotEmpty(columns) && MapUtils.isNotEmpty(columnValues)) {
433             columns.forEach(column -> column.setValues(columnValues.get(column.getColumnName())));
434         }
435 
436         return columns;
437     }
438 
439     protected AggregationContextVO toContextVO(ExtractionProductVO source) {
440 
441         AggregationContextVO target = new AggregationRdbTripContextVO();
442 
443         target.setId(source.getId());
444         target.setLabel(source.getLabel());
445 
446         ListUtils.emptyIfNull(source.getTables())
447                 .forEach(t -> target.addTableName(t.getTableName(), t.getLabel()));
448         return target;
449     }
450 
451     protected String getI18nSheetName(String format, String sheetName) {
452         return I18n.t(String.format("sumaris.aggregation.%s.%s", format.toUpperCase(), sheetName.toUpperCase()));
453     }
454 
455 
456 }