d570d090accd78f79930eff86cc9c70b048c9939
[cdmlib.git] / cdmlib-io / src / main / java / eu / etaxonomy / cdm / io / distribution / excelupdate / ExcelDistributionUpdate.java
1 /**
2 * Copyright (C) 2017 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
5 *
6 * The contents of this file are subject to the Mozilla Public License Version 1.1
7 * See LICENSE.TXT at the top of this package for the full license terms.
8 */
9 package eu.etaxonomy.cdm.io.distribution.excelupdate;
10
11 import java.util.HashMap;
12 import java.util.HashSet;
13 import java.util.Iterator;
14 import java.util.Map;
15 import java.util.Set;
16 import java.util.UUID;
17
18 import org.apache.log4j.Logger;
19 import org.springframework.stereotype.Component;
20
21 import eu.etaxonomy.cdm.common.CdmUtils;
22 import eu.etaxonomy.cdm.io.common.ImportResult;
23 import eu.etaxonomy.cdm.io.excel.common.ExcelImportBase;
24 import eu.etaxonomy.cdm.io.excel.common.ExcelRowBase;
25 import eu.etaxonomy.cdm.model.common.CdmBase;
26 import eu.etaxonomy.cdm.model.description.DescriptionBase;
27 import eu.etaxonomy.cdm.model.description.DescriptionElementBase;
28 import eu.etaxonomy.cdm.model.description.Distribution;
29 import eu.etaxonomy.cdm.model.description.PresenceAbsenceTerm;
30 import eu.etaxonomy.cdm.model.description.TaxonDescription;
31 import eu.etaxonomy.cdm.model.location.NamedArea;
32 import eu.etaxonomy.cdm.model.taxon.Taxon;
33 import eu.etaxonomy.cdm.model.term.DefinedTermBase;
34 import eu.etaxonomy.cdm.model.term.TermVocabulary;
35
36 /**
37 * This Import class updates existing distributions with the new state
38 * described in the Excel file. It requires that the data was exported
39 * before in the defined format.
40 *
41 * TODO where is the export to be found?
42 *
43 * This class is initiated by #6524
44 *
45 * @author a.mueller
46 * @since 04.04.2017
47 *
48 */
49 @Component
50 public class ExcelDistributionUpdate
51 extends ExcelImportBase<ExcelDistributionUpdateState, ExcelDistributionUpdateConfigurator, ExcelRowBase>{
52
53 private static final long serialVersionUID = 621338661492857764L;
54 private static final Logger logger = Logger.getLogger(ExcelDistributionUpdate.class);
55
56 private static final String AREA_MAP = "AreaMap";
57
58 /**
59 * {@inheritDoc}
60 */
61 @Override
62 protected void analyzeRecord(Map<String, String> record, ExcelDistributionUpdateState state) {
63 // nothing to do
64 }
65
66 /**
67 * {@inheritDoc}
68 */
69 @Override
70 protected void firstPass(ExcelDistributionUpdateState state) {
71 Map<String, String> record = state.getOriginalRecord();
72 String line = state.getCurrentLine() + ": ";
73 String taxonUuid = getValue(record, "taxon_uuid");
74 String taxonName = getValue(record, "Taxonname");
75
76 if ("taxon_uuid".equals(taxonUuid)){
77 return;
78 }
79 UUID uuidTaxon = UUID.fromString(taxonUuid);
80 Taxon taxon = (Taxon)getTaxonService().find(uuidTaxon);
81 if (taxon == null){
82 String message = line + "Taxon for uuid not found: " + uuidTaxon;
83 state.getResult().addError(message);
84 }else{
85 try {
86 handleAreasForTaxon(state, taxon, record, line);
87 } catch (Exception e) {
88 String message = line + "An unexpected error occurred when handling %s (uuid: %s)";
89 message = String.format(message, taxonName, taxonUuid);
90 state.getResult().addError(message);
91 state.getResult().addException(e);
92 }
93 }
94 }
95
96 /**
97 * @param state
98 * @param taxon
99 * @param record
100 * @param line
101 */
102 private void handleAreasForTaxon(ExcelDistributionUpdateState state, Taxon taxon, Map<String, String> record,
103 String line) {
104 ImportResult result = state.getResult();
105 Map<NamedArea, Set<Distribution>> existingDistributions = getExistingDistributions(state, taxon, line);
106 Map<NamedArea, Distribution> newDistributions = getNewDistributions(state, record, line);
107 TaxonDescription newDescription = TaxonDescription.NewInstance();
108 newDescription.addImportSource(null, null, state.getConfig().getSourceReference(), "row " + state.getCurrentLine());
109 newDescription.setTitleCache("Updated distributions for " + getTaxonLabel(taxon), true);
110 Set<TaxonDescription> oldReducedDescriptions = new HashSet<>();
111 for (NamedArea area : newDistributions.keySet()){
112 Set<Distribution> existingDistrForArea = existingDistributions.get(area);
113 boolean hasChange = false;
114 Distribution newDistribution = newDistributions.get(area);
115 if (existingDistrForArea == null || existingDistrForArea.isEmpty()){
116 if (newDistribution != null){
117 //new distribution exists, old distribution did not exist
118 hasChange = true;
119 }
120 }else{
121 for (Distribution existingDistr : existingDistrForArea){
122 if (!isEqualDistribution(existingDistr, newDistribution)){
123 //distribution changed or deleted
124 if (state.getConfig().isCreateNewDistribution() || newDistribution == null ){
125 DescriptionBase<?> inDescription = existingDistr.getInDescription();
126 inDescription.removeElement(existingDistr);
127 result.addDeletedRecord(existingDistr);
128 hasChange = true;
129 oldReducedDescriptions.add(CdmBase.deproxy(inDescription, TaxonDescription.class));
130 }else{
131 existingDistr.setStatus(newDistribution.getStatus());
132 result.addUpdatedRecord(existingDistr);
133 existingDistr.addImportSource(null, null, state.getConfig().getSourceReference(), "row "+state.getCurrentLine());
134 }
135 }else{
136 // addSource? => not if nothing changed
137 }
138 }
139 }
140 if (hasChange && newDistribution != null){
141 newDescription.addElement(newDistribution);
142 result.addNewRecord(newDistribution);
143 }
144 }
145 //add new description to taxon if any new element exists
146 if (!newDescription.getElements().isEmpty()){
147 taxon.addDescription(newDescription);
148 result.addNewRecord(newDescription);
149 }
150 //remove old empty descriptions (oldReducedDescriptions) if really empty
151 for (TaxonDescription desc : oldReducedDescriptions){
152 if (desc.getElements().isEmpty()){
153 desc.getTaxon().removeDescription(desc);
154 result.addDeletedRecord(desc);
155 }
156 }
157 }
158
159 /**
160 * @param taxon
161 * @return
162 */
163 private String getTaxonLabel(Taxon taxon) {
164 return taxon.getName() == null ? taxon.getTitleCache() : taxon.getName().getTitleCache();
165 }
166
167 private Map<NamedArea, Distribution> getNewDistributions(ExcelDistributionUpdateState state,
168 Map<String, String> record, String line) {
169
170 Map<NamedArea, Distribution> result = new HashMap<>();
171
172 Set<String> keys = record.keySet();
173 keys = removeNonAreaKeys(keys);
174 for (String key : keys){
175 NamedArea area = getAreaByIdInVoc(state, key, line);
176 if (area != null){
177 String statusStr = record.get(key);
178 PresenceAbsenceTerm status = getStatusByStatusStr(state, statusStr, line);
179 if (status != null){
180 Distribution distribution = Distribution.NewInstance(area, status);
181 distribution.addImportSource(null, null, state.getConfig().getSourceReference(), "row " + state.getCurrentLine());
182 Distribution previousDistribution = result.put(area, distribution);
183 if (previousDistribution != null){
184 String message = line + "Multiple distributions exist for same area (" + area.getTitleCache() + ") in input source";
185 logger.warn(message);
186 state.getResult().addWarning(message);
187 }
188 }else{
189 result.put(area, null);
190 }
191 }else{
192 //??
193 }
194 }
195 return result;
196 }
197
198 /**
199 * @param statusStr
200 * @return
201 */
202 private PresenceAbsenceTerm getStatusByStatusStr(ExcelDistributionUpdateState state, String statusStr, String line) {
203 // FIXME replace hardcoded;
204 if ("A".equals(statusStr)) {
205 return PresenceAbsenceTerm.ABSENT();
206 }else if ("P".equals(statusStr)) {
207 return PresenceAbsenceTerm.PRESENT();
208 }else if ("P?".equals(statusStr)) {
209 return PresenceAbsenceTerm.PRESENT_DOUBTFULLY();
210 }else if (isBlank(statusStr)){
211 return null;
212 }else{
213 String message = line + "Status string not recognized: " + statusStr +". Status not imported.";
214 logger.warn(message);
215 state.getResult().addWarning(message);
216 }
217
218 return null;
219 }
220
221 /**
222 * @param state
223 * @param key
224 * @param line
225 * @return
226 */
227 private NamedArea getAreaByIdInVoc(ExcelDistributionUpdateState state, String id, String line) {
228 //TODO remember in state
229 Map<String, NamedArea> areaMap = (Map<String, NamedArea>)state.getStatusItem(AREA_MAP);
230 if (areaMap == null){
231 areaMap = createAreaMap(state);
232 state.putStatusItem(AREA_MAP, areaMap);
233 }
234 NamedArea area = areaMap.get(id);
235 return area;
236 }
237
238 /**
239 * @param state
240 * @return
241 */
242 private Map<String, NamedArea> createAreaMap(ExcelDistributionUpdateState state) {
243 Map<String, NamedArea> result = new HashMap<>();
244 TermVocabulary<?> voc = getVocabularyService().find(state.getConfig().getAreaVocabularyUuid());
245 //TODO handle null
246 for (DefinedTermBase<?> obj : voc.getTerms()){
247 //TODO handle exception
248 NamedArea area = CdmBase.deproxy(obj, NamedArea.class);
249 String key = area.getIdInVocabulary();
250 result.put(key, area);
251 }
252 return result;
253 }
254
255 /**
256 * @param keys
257 * @return
258 */
259 private Set<String> removeNonAreaKeys(Set<String> keys) {
260 Iterator<String> it = keys.iterator();
261 while (it.hasNext()){
262 if (it.next().matches("(Family|Taxonname|taxon_uuid)")){
263 it.remove();
264 }
265 }
266 return keys;
267 }
268
269 private boolean isEqualDistribution(Distribution existingDistribution, Distribution newDistribution) {
270 if (existingDistribution == null || newDistribution == null){
271 return existingDistribution == newDistribution;
272 }
273 if (existingDistribution.getArea().equals(newDistribution.getArea())){
274 if (CdmUtils.nullSafeEqual(existingDistribution.getStatus(), newDistribution.getStatus())){
275 return true;
276 }
277 }
278 return false;
279 }
280
281 /**
282 * @param state
283 * @param taxon
284 * @param line
285 * @return
286 */
287 private Map<NamedArea, Set<Distribution>> getExistingDistributions(
288 ExcelDistributionUpdateState state, Taxon taxon,
289 String line) {
290 Map<NamedArea, Set<Distribution>> result = new HashMap<>();
291 //TODO better use service layer call to return only distributions, this might be necessary if the list of description elements is large
292 for (TaxonDescription desc : taxon.getDescriptions()){
293 for (DescriptionElementBase descElem : desc.getElements()){
294 if (descElem.isInstanceOf(Distribution.class)){
295 Distribution distribution = CdmBase.deproxy(descElem, Distribution.class);
296 NamedArea area = distribution.getArea();
297 Set<Distribution> set = result.get(area);
298 if (set == null){
299 set = new HashSet<>();
300 result.put(area, set);
301 }
302 set.add(distribution);
303 }
304 }
305 }
306 return result;
307 }
308
309 /**
310 * {@inheritDoc}
311 */
312 @Override
313 protected void secondPass(ExcelDistributionUpdateState state) {
314 // nothing to do
315 }
316
317 /**
318 * {@inheritDoc}
319 */
320 @Override
321 protected boolean isIgnore(ExcelDistributionUpdateState state) {
322 return false;
323 }
324 @Override
325 protected boolean needsNomenclaturalCode() {
326 return false;
327 }
328
329 }