Project

General

Profile

Download (44.7 KB) Statistics
| Branch: | Revision:
1
/**
2
 * Copyright (C) 2007 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

    
10
package eu.etaxonomy.cdm.io.iapt;
11

    
12
import eu.etaxonomy.cdm.api.facade.DerivedUnitFacade;
13
import eu.etaxonomy.cdm.common.CdmUtils;
14
import eu.etaxonomy.cdm.io.mexico.SimpleExcelTaxonImport;
15
import eu.etaxonomy.cdm.io.mexico.SimpleExcelTaxonImportState;
16
import eu.etaxonomy.cdm.model.agent.Institution;
17
import eu.etaxonomy.cdm.model.agent.Person;
18
import eu.etaxonomy.cdm.model.agent.TeamOrPersonBase;
19
import eu.etaxonomy.cdm.model.common.*;
20
import eu.etaxonomy.cdm.model.name.*;
21
import eu.etaxonomy.cdm.model.occurrence.*;
22
import eu.etaxonomy.cdm.model.occurrence.Collection;
23
import eu.etaxonomy.cdm.model.reference.Reference;
24
import eu.etaxonomy.cdm.model.taxon.*;
25
import eu.etaxonomy.cdm.strategy.parser.NonViralNameParserImpl;
26
import org.apache.commons.lang.ArrayUtils;
27
import org.apache.commons.lang.StringEscapeUtils;
28
import org.apache.commons.lang.StringUtils;
29
import org.apache.log4j.Level;
30
import org.apache.log4j.Logger;
31
import org.joda.time.DateTimeFieldType;
32
import org.joda.time.Partial;
33
import org.joda.time.format.DateTimeFormat;
34
import org.joda.time.format.DateTimeFormatter;
35
import org.springframework.stereotype.Component;
36

    
37
import java.util.*;
38
import java.util.regex.Matcher;
39
import java.util.regex.Pattern;
40

    
41
/**
42
 * @author a.mueller
43
 * @created 05.01.2016
44
 */
45

    
46
@Component("iAPTExcelImport")
47
public class IAPTExcelImport<CONFIG extends IAPTImportConfigurator> extends SimpleExcelTaxonImport<CONFIG> {
48
    private static final long serialVersionUID = -747486709409732371L;
49
    private static final Logger logger = Logger.getLogger(IAPTExcelImport.class);
50
    public static final String ANNOTATION_MARKER_STRING = "[*]";
51

    
52

    
53
    private static UUID ROOT_UUID = UUID.fromString("4137fd2a-20f6-4e70-80b9-f296daf51d82");
54

    
55
    private static NonViralNameParserImpl nameParser = NonViralNameParserImpl.NewInstance();
56

    
57
    private final static String REGISTRATIONNO_PK= "RegistrationNo_Pk";
58
    private final static String HIGHERTAXON= "HigherTaxon";
59
    private final static String FULLNAME= "FullName";
60
    private final static String AUTHORSSPELLING= "AuthorsSpelling";
61
    private final static String LITSTRING= "LitString";
62
    private final static String REGISTRATION= "Registration";
63
    private final static String TYPE= "Type";
64
    private final static String CAVEATS= "Caveats";
65
    private final static String FULLBASIONYM= "FullBasionym";
66
    private final static String FULLSYNSUBST= "FullSynSubst";
67
    private final static String NOTESTXT= "NotesTxt";
68
    private final static String REGDATE= "RegDate";
69
    private final static String NAMESTRING= "NameString";
70
    private final static String BASIONYMSTRING= "BasionymString";
71
    private final static String SYNSUBSTSTR= "SynSubstStr";
72
    private final static String AUTHORSTRING= "AuthorString";
73

    
74
    private  static List<String> expectedKeys= Arrays.asList(new String[]{
75
            REGISTRATIONNO_PK, HIGHERTAXON, FULLNAME, AUTHORSSPELLING, LITSTRING, REGISTRATION, TYPE, CAVEATS, FULLBASIONYM, FULLSYNSUBST, NOTESTXT, REGDATE, NAMESTRING, BASIONYMSTRING, SYNSUBSTSTR, AUTHORSTRING});
76

    
77
    private static final Pattern nomRefTokenizeP = Pattern.compile("^(.*):\\s([^\\.:]+)\\.(.*?)\\.?$");
78
    private static final Pattern[] datePatterns = new Pattern[]{
79
            // NOTE:
80
            // The order of the patterns is extremely important!!!
81
            //
82
            // all patterns cover the years 1700 - 1999
83
            Pattern.compile("^(?<year>1[7,8,9][0-9]{2})$"), // only year, like '1969'
84
            Pattern.compile("^(?<monthName>\\p{L}+\\.?)\\s(?<day>[0-9]{1,2})(?:st|rd|th)?\\.?,?\\s(?<year>(?:1[7,8,9])?[0-9]{2})$"), // full date like April 12, 1969 or april 12th 1999
85
            Pattern.compile("^(?<monthName>\\p{L}+\\.?),?\\s?(?<year>(?:1[7,8,9])?[0-9]{2})$"), // April 99 or April, 1999 or Apr. 12
86
            Pattern.compile("^(?<day>[0-9]{1,2})([\\.\\-/])(\\s?)(?<month>[0-1]?[0-9])\\2\\3(?<year>(?:1[7,8,9])?[0-9]{2})$"), // full date like 12.04.1969 or 12. 04. 1969 or 12/04/1969 or 12-04-1969
87
            Pattern.compile("^(?<day>[0-9]{1,2})([\\.\\-/])(?<month>[IVX]{1,2})\\2(?<year>(?:1[7,8,9])?[0-9]{2})$"), // full date like 12-VI-1969
88
            Pattern.compile("^(?:(?<day>[0-9]{1,2})(?:\\sde)\\s)(?<monthName>\\p{L}+)\\sde\\s(?<year>(?:1[7,8,9])?[0-9]{2})$"), // full and partial date like 12 de Enero de 1999 or Enero de 1999
89
            Pattern.compile("^(?<month>[0-1]?[0-9])([\\.\\-/])(?<year>(?:1[7,8,9])?[0-9]{2})$"), // partial date like 04.1969 or 04/1969 or 04-1969
90
            Pattern.compile("^(?<year>(?:1[7,8,9])?[0-9]{2})([\\.\\-/])(?<month>[0-1]?[0-9])$"),//  partial date like 1999-04
91
            Pattern.compile("^(?<month>[IVX]{1,2})([\\.\\-/])(?<year>(?:1[7,8,9])?[0-9]{2})$"), // partial date like VI-1969
92
            Pattern.compile("^(?<day>[0-9]{1,2})(?:[\\./]|th|rd|st)?\\s(?<monthName>\\p{L}+\\.?),?\\s?(?<year>(?:1[7,8,9])?[0-9]{2})$"), // full date like 12. April 1969 or april 1999 or 22 Dec.1999
93
        };
94
    private static final Pattern typeSplitPattern =  Pattern.compile("^(?:\"*[Tt]ype: (?<fieldUnit>.*?))(?:[Hh]olotype:(?<holotype>.*?)\\.?)?(?:[Ii]sotype[^:]*:(?<isotype>.*)\\.?)?\\.?$");
95

    
96
    private static final Pattern collectorPattern =  Pattern.compile(".*?\\(leg\\.\\s+([^\\)]*)\\)|.*?\\sleg\\.\\s+(.*?)\\.?$");
97
    private static final Pattern collectionDataPattern =  Pattern.compile("^(?<collector>[^,]*),\\s?(?<detail>.*?)\\.?$");
98
    private static final Pattern collectorsNumber =  Pattern.compile("^([nN]o\\.\\s.*)$");
99

    
100
    // AccessionNumbers: , #.*, n°:?, 96/3293, No..*, -?\w{1,3}-[0-9\-/]*
101
    private static final Pattern accessionNumberOnlyPattern = Pattern.compile("^(?<accNumber>(?:n°\\:?\\s?|#|No\\.?\\s?)?[\\d\\w\\-/]*)$");
102

    
103
    private static final Pattern[] specimenTypePatterns = new Pattern[]{
104
            Pattern.compile("^(?<colCode>[A-Z]+|CPC Micropaleontology Lab\\.?)\\s+(?:\\((?<institute>.*[^\\)])\\))(?<accNumber>.*)?$"), // like: GAUF (Gansu Agricultural University) No. 1207-1222
105
            Pattern.compile("^(?<colCode>[A-Z]+|CPC Micropaleontology Lab\\.?)\\s+(?:Coll\\.\\s(?<subCollection>[^\\.,;]*)(.))(?<accNumber>.*)?$"), // like KASSEL Coll. Krasske, Praep. DII 78
106
            Pattern.compile("^(?:Coll\\.\\s(?<subCollection>[^\\.,;]*)(.))(?<institute>.*?)(?<accNumber>Praep\\..*)?$"), // like Coll. Lange-Bertalot, Bot. Inst., Univ. Frankfurt/Main, Germany Praep. Neukaledonien OTL 62
107
            Pattern.compile("^(?<colCode>[A-Z]+)(?:\\s+(?<accNumber>.*))?$"), // identifies the Collection code and takes the rest as accessionNumber if any
108
    };
109

    
110
    private static Map<String, Integer> monthFromNameMap = new HashMap<>();
111

    
112
    static {
113
        String[] ck = new String[]{"leden", "únor", "březen", "duben", "květen", "červen", "červenec ", "srpen", "září", "říjen", "listopad", "prosinec"};
114
        String[] fr = new String[]{"janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"};
115
        String[] de = new String[]{"januar", "februar", "märz", "april", "mai", "juni", "juli", "august", "september", "oktober", "november", "dezember"};
116
        String[] en = new String[]{"january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"};
117
        String[] it = new String[]{"gennaio", "febbraio", "marzo", "aprile", "maggio", "giugno", "luglio", "agosto", "settembre", "ottobre", "novembre", "dicembre"};
118
        String[] sp = new String[]{"enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"};
119
        String[] de_abbrev = new String[]{"jan.", "feb.", "märz", "apr.", "mai", "jun.", "jul.", "aug.", "sept.", "okt.", "nov.", "dez."};
120
        String[] en_abbrev = new String[]{"jan.", "feb.", "mar.", "apr.", "may", "jun.", "jul.", "aug.", "sep.", "oct.", "nov.", "dec."};
121
        String[] port = new String[]{"Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro"};
122
        String[] rom_num = new String[]{"i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix", "x", "xi", "xii"};
123

    
124
        String[][] perLang =  new String[][]{ck, de, fr, en, it, sp, port, de_abbrev, en_abbrev, rom_num};
125

    
126
        for (String[] months: perLang) {
127
            for(int m = 1; m < 13; m++){
128
                monthFromNameMap.put(months[m - 1].toLowerCase(), m);
129
            }
130
        }
131

    
132
        // special cases
133
        monthFromNameMap.put("mar", 3);
134
        monthFromNameMap.put("dec", 12);
135
        monthFromNameMap.put("Februari", 2);
136
    }
137

    
138

    
139
    DateTimeFormatter formatterYear = DateTimeFormat.forPattern("yyyy");
140

    
141
    private Map<String, Collection> collectionMap = new HashMap<>();
142

    
143

    
144
    enum TypesName {
145
        fieldUnit, holotype, isotype;
146

    
147
        public SpecimenTypeDesignationStatus status(){
148
            switch (this) {
149
                case holotype:
150
                    return SpecimenTypeDesignationStatus.HOLOTYPE();
151
                case isotype:
152
                    return SpecimenTypeDesignationStatus.ISOTYPE();
153
                default:
154
                    return null;
155
            }
156
        }
157
    }
158

    
159
    private MarkerType markerTypeFossil = null;
160
    private Rank rankUnrankedSupraGeneric = null;
161
    private Rank familyIncertisSedis = null;
162
    private AnnotationType annotationTypeCaveats = null;
163

    
164
    private Taxon makeTaxon(HashMap<String, String> record, SimpleExcelTaxonImportState<CONFIG> state,
165
                            TaxonNode higherTaxonNode, boolean isFossil) {
166

    
167
        String line = state.getCurrentLine() + ": ";
168

    
169
        String regNumber = getValue(record, REGISTRATIONNO_PK, false);
170
        String regStr = getValue(record, REGISTRATION, true);
171
        String titleCacheStr = getValue(record, FULLNAME, true);
172
        String nameStr = getValue(record, NAMESTRING, true);
173
        String authorStr = getValue(record, AUTHORSTRING, true);
174
        String nomRefStr = getValue(record, LITSTRING, true);
175
        String authorsSpelling = getValue(record, AUTHORSSPELLING, true);
176
        String notesTxt = getValue(record, NOTESTXT, true);
177
        String caveats = getValue(record, CAVEATS, true);
178
        String fullSynSubstStr = getValue(record, FULLSYNSUBST, true);
179
        String synSubstStr = getValue(record, SYNSUBSTSTR, true);
180
        String typeStr = getValue(record, TYPE, true);
181

    
182

    
183
        String nomRefTitle = null;
184
        String nomRefDetail;
185
        String nomRefPupDate = null;
186
        Partial pupDate = null;
187

    
188
        // preprocess nomRef: separate citation, reference detail, publishing date
189
        if(!StringUtils.isEmpty(nomRefStr)){
190
            nomRefStr = nomRefStr.trim();
191
            Matcher m = nomRefTokenizeP.matcher(nomRefStr);
192
            if(m.matches()){
193
                nomRefTitle = m.group(1);
194
                nomRefDetail = m.group(2);
195
                nomRefPupDate = m.group(3).trim();
196

    
197
                pupDate = parseDate(regNumber, nomRefPupDate);
198
                if (pupDate != null) {
199
                    nomRefTitle = nomRefTitle + ": " + nomRefDetail + ". " + pupDate.toString(formatterYear) + ".";
200
                } else {
201
                    logger.warn(csvReportLine(regNumber, "Pub date", nomRefPupDate, "in", nomRefStr, "not parsable"));
202
                }
203
            } else {
204
                nomRefTitle = nomRefStr;
205
            }
206
        }
207

    
208
        BotanicalName taxonName = makeBotanicalName(state, regNumber, titleCacheStr, nameStr, authorStr, nomRefTitle);
209

    
210
        // always add the original strings of parsed data as annotation
211
        taxonName.addAnnotation(Annotation.NewInstance("imported and parsed data strings:" +
212
                        "\n -  '" + LITSTRING + "': "+ nomRefStr +
213
                        "\n -  '" + TYPE + "': " + typeStr +
214
                        "\n -  '" + REGISTRATION  + "': " + regStr
215
                , AnnotationType.TECHNICAL(), Language.DEFAULT()));
216

    
217
        if(pupDate != null) {
218
            taxonName.getNomenclaturalReference().setDatePublished(TimePeriod.NewInstance(pupDate));
219
        }
220

    
221
        if(!StringUtils.isEmpty(notesTxt)){
222
            notesTxt = notesTxt.replace("Notes: ", "").trim();
223
            taxonName.addAnnotation(Annotation.NewInstance(notesTxt, AnnotationType.EDITORIAL(), Language.DEFAULT()));
224
        }
225
        if(!StringUtils.isEmpty(caveats)){
226
            caveats = caveats.replace("Caveats: ", "").trim();
227
            taxonName.addAnnotation(Annotation.NewInstance(caveats, annotationTypeCaveats(), Language.DEFAULT()));
228
        }
229
        //
230

    
231
        // Namerelations
232
        if(!StringUtils.isEmpty(authorsSpelling)){
233
            authorsSpelling = authorsSpelling.replaceFirst("Author's spelling:", "").replaceAll("\"", "").trim();
234

    
235
            String[] authorSpellingTokens = StringUtils.split(authorsSpelling, " ");
236
            String[] nameStrTokens = StringUtils.split(nameStr, " ");
237

    
238
            ArrayUtils.reverse(authorSpellingTokens);
239
            ArrayUtils.reverse(nameStrTokens);
240

    
241
            for (int i = 0; i < nameStrTokens.length; i++){
242
                if(i < authorSpellingTokens.length){
243
                    nameStrTokens[i] = authorSpellingTokens[i];
244
                }
245
            }
246
            ArrayUtils.reverse(nameStrTokens);
247

    
248
            String misspelledNameStr = StringUtils.join (nameStrTokens, ' ');
249
            // build the fullnameString of the misspelled name
250
            misspelledNameStr = taxonName.getTitleCache().replace(nameStr, misspelledNameStr);
251

    
252
            TaxonNameBase misspelledName = (BotanicalName) nameParser.parseReferencedName(misspelledNameStr, NomenclaturalCode.ICNAFP, null);
253
            misspelledName.addRelationshipToName(taxonName, NameRelationshipType.MISSPELLING(), null);
254
            getNameService().save(misspelledName);
255
        }
256

    
257
        // Replaced Synonyms
258
        if(!StringUtils.isEmpty(fullSynSubstStr)){
259
            fullSynSubstStr = fullSynSubstStr.replace("Syn. subst.: ", "");
260
            BotanicalName replacedSynonymName = makeBotanicalName(state, regNumber, fullSynSubstStr, synSubstStr, null, null);
261
            replacedSynonymName.addReplacedSynonym(taxonName, null, null, null);
262
            getNameService().save(replacedSynonymName);
263
        }
264

    
265
        Reference sec = state.getConfig().getSecReference();
266
        Taxon taxon = Taxon.NewInstance(taxonName, sec);
267

    
268
        // Markers
269
        if(isFossil){
270
            taxon.addMarker(Marker.NewInstance(markerTypeFossil(), true));
271
        }
272

    
273
        // Types
274
        if(!StringUtils.isEmpty(typeStr)){
275
            makeTypeData(typeStr, taxonName, regNumber, state);
276
        }
277

    
278
        getTaxonService().save(taxon);
279
        if(higherTaxonNode != null){
280
            higherTaxonNode.addChildTaxon(taxon, null, null);
281
            getTaxonNodeService().save(higherTaxonNode);
282
        }
283

    
284
        return taxon;
285

    
286
    }
287

    
288
    private void makeTypeData(String typeStr, BotanicalName taxonName, String regNumber, SimpleExcelTaxonImportState<CONFIG> state) {
289

    
290
        Matcher m = typeSplitPattern.matcher(typeStr);
291

    
292
        if(m.matches()){
293
            String fieldUnitStr = m.group(TypesName.fieldUnit.name());
294
            // boolean isFieldUnit = typeStr.matches(".*([°']|\\d+\\s?m\\s|\\d+\\s?km\\s).*"); // check for location or unit m, km // makes no sense!!!!
295
            FieldUnit fieldUnit = parseFieldUnit(fieldUnitStr, regNumber, state);
296
            if(fieldUnit == null) {
297
                // create a field unit with only a titleCache using the fieldUnitStr substring
298
                logger.warn(csvReportLine(regNumber, "Type: fielUnitStr can not be parsed", fieldUnitStr));
299
                fieldUnit = FieldUnit.NewInstance();
300
                fieldUnit.setTitleCache(fieldUnitStr, true);
301
                getOccurrenceService().save(fieldUnit);
302
            }
303
            getOccurrenceService().save(fieldUnit);
304

    
305
            // all others ..
306
            addSpecimenTypes(taxonName, fieldUnit, m.group(TypesName.holotype.name()), TypesName.holotype, false, regNumber);
307
            addSpecimenTypes(taxonName, fieldUnit, m.group(TypesName.isotype.name()), TypesName.isotype, true, regNumber);
308

    
309
        } else {
310
            // create a field unit with only a titleCache using the full typeStr
311
            FieldUnit fieldUnit = FieldUnit.NewInstance();
312
            fieldUnit.setTitleCache(typeStr, true);
313
            getOccurrenceService().save(fieldUnit);
314
            logger.warn(csvReportLine(regNumber, "Type: field 'Type' can not be parsed", typeStr));
315
        }
316
        getNameService().save(taxonName);
317
    }
318

    
319
    /**
320
     * Currently only parses the collector, fieldNumber and the collection date.
321
     *
322
     * @param fieldUnitStr
323
     * @param regNumber
324
     * @param state
325
     * @return null if the fieldUnitStr could not be parsed
326
     */
327
    private FieldUnit parseFieldUnit(String fieldUnitStr, String regNumber, SimpleExcelTaxonImportState<CONFIG> state) {
328

    
329
        FieldUnit fieldUnit = null;
330

    
331
        Matcher m1 = collectorPattern.matcher(fieldUnitStr);
332
        if(m1.matches()){
333
            String collectionData = m1.group(1); // like (leg. Metzeltin, 30. 9. 1996)
334
            if(collectionData == null){
335
                collectionData = m1.group(2); // like leg. Metzeltin, 30. 9. 1996
336
            }
337
            if(collectionData == null){
338
                return null;
339
            }
340

    
341
            String collectorStr = null;
342
            String detailStr = null;
343
            Partial date = null;
344
            String fieldNumber = null;
345

    
346
            Matcher m2 = collectionDataPattern.matcher(collectionData);
347
            if(m2.matches()){
348
                collectorStr = m2.group("collector");
349
                detailStr = m2.group("detail");
350

    
351
                // Try to make sense of the detailStr
352
                if(detailStr != null){
353
                    detailStr = detailStr.trim();
354
                    // 1. try to parse as date
355
                    date = parseDate(regNumber, detailStr);
356
                    if(date == null){
357
                        // 2. try to parse as number
358
                        if(collectorsNumber.matcher(detailStr).matches()){
359
                            fieldNumber = detailStr;
360
                        }
361
                    }
362
                }
363
                if(date == null && fieldNumber == null){
364
                    // detailed parsing not possible, so need fo fallback
365
                    collectorStr = collectionData;
366
                }
367
            }
368

    
369
            if(collectorStr != null) {
370
                fieldUnit = FieldUnit.NewInstance();
371
                GatheringEvent ge = GatheringEvent.NewInstance();
372

    
373
                TeamOrPersonBase agent =  state.getAgentBase(collectorStr);
374
                if(agent == null) {
375
                    agent = Person.NewTitledInstance(collectorStr);
376
                    getAgentService().save(agent);
377
                    state.putAgentBase(collectorStr, agent);
378
                }
379
                ge.setCollector(agent);
380

    
381
                if(date != null){
382
                    ge.setGatheringDate(date);
383
                }
384

    
385
                getEventBaseService().save(ge);
386
                fieldUnit.setGatheringEvent(ge);
387

    
388
                if(fieldNumber != null) {
389
                    fieldUnit.setFieldNumber(fieldNumber);
390
                }
391
                getOccurrenceService().save(fieldUnit);
392
            }
393
        }
394

    
395
        return fieldUnit;
396
    }
397

    
398
    private Partial parseDate(String regNumber, String dateStr) {
399

    
400
        Partial pupDate = null;
401
        boolean parseError = false;
402

    
403
        String day = null;
404
        String month = null;
405
        String monthName = null;
406
        String year = null;
407

    
408
        for(Pattern p : datePatterns){
409
            Matcher m2 = p.matcher(dateStr);
410
            if(m2.matches()){
411
                try {
412
                    year = m2.group("year");
413
                } catch (IllegalArgumentException e){
414
                    // named capture group not found
415
                }
416
                try {
417
                    month = m2.group("month");
418
                } catch (IllegalArgumentException e){
419
                    // named capture group not found
420
                }
421

    
422
                try {
423
                    monthName = m2.group("monthName");
424
                    month = monthFromName(monthName, regNumber);
425
                    if(month == null){
426
                        parseError = true;
427
                    }
428
                } catch (IllegalArgumentException e){
429
                    // named capture group not found
430
                }
431
                try {
432
                    day = m2.group("day");
433
                } catch (IllegalArgumentException e){
434
                    // named capture group not found
435
                }
436

    
437
                if(year != null){
438
                    if (year.length() == 2) {
439
                        // it is an abbreviated year from the 19** years
440
                        year = "19" + year;
441
                    }
442
                    break;
443
                } else {
444
                    parseError = true;
445
                }
446
            }
447
        }
448
        if(year == null){
449
            parseError = true;
450
        }
451
        List<DateTimeFieldType> types = new ArrayList<>();
452
        List<Integer> values = new ArrayList<>();
453
        if(!parseError) {
454
            types.add(DateTimeFieldType.year());
455
            values.add(Integer.parseInt(year));
456
            if (month != null) {
457
                types.add(DateTimeFieldType.monthOfYear());
458
                values.add(Integer.parseInt(month));
459
            }
460
            if (day != null) {
461
                types.add(DateTimeFieldType.dayOfMonth());
462
                values.add(Integer.parseInt(day));
463
            }
464
            pupDate = new Partial(types.toArray(new DateTimeFieldType[types.size()]), ArrayUtils.toPrimitive(values.toArray(new Integer[values.size()])));
465
        }
466
        return pupDate;
467
    }
468

    
469
    private String monthFromName(String monthName, String regNumber) {
470

    
471
        Integer month = monthFromNameMap.get(monthName.toLowerCase());
472
        if(month == null){
473
            logger.warn(csvReportLine(regNumber, "Unknown month name", monthName));
474
            return null;
475
        } else {
476
            return month.toString();
477
        }
478
    }
479

    
480

    
481
    private void addSpecimenTypes(BotanicalName taxonName, FieldUnit fieldUnit, String typeStr, TypesName typeName, boolean multiple, String regNumber){
482

    
483
        if(StringUtils.isEmpty(typeStr)){
484
            return;
485
        }
486
        typeStr = typeStr.trim().replaceAll("\\.$", "");
487

    
488
        Collection collection = null;
489
        DerivedUnit specimen = null;
490

    
491
        List<DerivedUnit> specimens = new ArrayList<>();
492
        if(multiple){
493
            String[] tokens = typeStr.split("\\s?,\\s?");
494
            for (String t : tokens) {
495
                // command to  list all complex parsabel types:
496
                // csvcut -t -c RegistrationNo_Pk,Type iapt.csv | csvgrep -c Type -m "Holotype" | egrep -o 'Holotype:\s([A-Z]*\s)[^.]*?'
497
                // csvcut -t -c RegistrationNo_Pk,Type iapt.csv | csvgrep -c Type -m "Holotype" | egrep -o 'Isotype[^:]*:\s([A-Z]*\s)[^.]*?'
498

    
499
                if(!t.isEmpty()){
500
                    // trying to parse the string
501
                    specimen = parseSpecimenType(fieldUnit, typeName, collection, t, regNumber);
502
                    if(specimen != null){
503
                        specimens.add(specimen);
504
                    } else {
505
                        // parsing was not successful make simple specimen
506
                        specimens.add(makeSpecimenType(fieldUnit, t));
507
                    }
508
                }
509
            }
510
        } else {
511
            specimen = parseSpecimenType(fieldUnit, typeName, collection, typeStr, regNumber);
512
            if(specimen != null) {
513
                specimens.add(specimen);
514
                // remember current collection
515
                collection = specimen.getCollection();
516
            } else {
517
                // parsing was not successful make simple specimen
518
                specimens.add(makeSpecimenType(fieldUnit, typeStr));
519
            }
520
        }
521

    
522
        for(DerivedUnit s : specimens){
523
            taxonName.addSpecimenTypeDesignation(s, typeName.status(), null, null, null, false, true);
524
       }
525
    }
526

    
527
    private DerivedUnit makeSpecimenType(FieldUnit fieldUnit, String titleCache) {
528
        DerivedUnit specimen;DerivedUnitFacade facade = DerivedUnitFacade.NewInstance(SpecimenOrObservationType.PreservedSpecimen, fieldUnit);
529
        facade.setTitleCache(titleCache.trim(), true);
530
        specimen = facade.innerDerivedUnit();
531
        return specimen;
532
    }
533

    
534
    /**
535
     *
536
     * @param fieldUnit
537
     * @param typeName
538
     * @param collection
539
     * @param text
540
     * @param regNumber
541
     * @return
542
     */
543
    private DerivedUnit parseSpecimenType(FieldUnit fieldUnit, TypesName typeName, Collection collection, String text, String regNumber) {
544

    
545
        DerivedUnit specimen = null;
546

    
547
        String collectionCode = null;
548
        String subCollectionStr = null;
549
        String instituteStr = null;
550
        String accessionNumber = null;
551

    
552
        boolean unusualAccessionNumber = false;
553

    
554
        text = text.trim();
555

    
556
        // 1.  For Isotypes often the accession number is noted alone if the
557
        //     preceeding entry has a collection code.
558
        if(typeName .equals(TypesName.isotype) && collection != null){
559
            Matcher m = accessionNumberOnlyPattern.matcher(text);
560
            if(m.matches()){
561
                try {
562
                    accessionNumber = m.group("accNumber");
563
                    specimen = makeSpecimenType(fieldUnit, collection, accessionNumber);
564
                } catch (IllegalArgumentException e){
565
                    // match group acc_number not found
566
                }
567
            }
568
        }
569

    
570
        //2. try it the 'normal' way
571
        if(specimen == null) {
572
            for (Pattern p : specimenTypePatterns) {
573
                Matcher m = p.matcher(text);
574
                if (m.matches()) {
575
                    // collection code is mandatory
576
                    try {
577
                        collectionCode = m.group("colCode");
578
                    } catch (IllegalArgumentException e){
579
                        // match group colCode not found
580
                    }
581
                    try {
582
                        subCollectionStr = m.group("subCollection");
583
                    } catch (IllegalArgumentException e){
584
                        // match group subCollection not found
585
                    }
586
                    try {
587
                        instituteStr = m.group("institute");
588
                    } catch (IllegalArgumentException e){
589
                        // match group col_name not found
590
                    }
591
                    try {
592
                        accessionNumber = m.group("accNumber");
593

    
594
                        // try to improve the accessionNumber
595
                        if(accessionNumber!= null) {
596
                            accessionNumber = accessionNumber.trim();
597
                            Matcher m2 = accessionNumberOnlyPattern.matcher(accessionNumber);
598
                            String betterAccessionNumber = null;
599
                            if (m2.matches()) {
600
                                try {
601
                                    betterAccessionNumber = m.group("accNumber");
602
                                } catch (IllegalArgumentException e) {
603
                                    // match group acc_number not found
604
                                }
605
                            }
606
                            if (betterAccessionNumber != null) {
607
                                accessionNumber = betterAccessionNumber;
608
                            } else {
609
                                unusualAccessionNumber = true;
610
                            }
611
                        }
612

    
613
                    } catch (IllegalArgumentException e){
614
                        // match group acc_number not found
615
                    }
616

    
617
                    if(collectionCode == null && instituteStr == null){
618
                        logger.warn(csvReportLine(regNumber, "Type: neither 'collectionCode' nor 'institute' found in ", text));
619
                        continue;
620
                    }
621
                    collection = getCollection(collectionCode, instituteStr, subCollectionStr);
622
                    specimen = makeSpecimenType(fieldUnit, collection, accessionNumber);
623
                    break;
624
                }
625
            }
626
        }
627
        if(specimen == null) {
628
            logger.warn(csvReportLine(regNumber, "Type: Could not parse specimen", typeName.name().toString(), text));
629
        }
630
        if(unusualAccessionNumber){
631
            logger.warn(csvReportLine(regNumber, "Type: Unusual accession number", typeName.name().toString(), text, accessionNumber));
632
        }
633
        return specimen;
634
    }
635

    
636
    private DerivedUnit makeSpecimenType(FieldUnit fieldUnit, Collection collection, String accessionNumber) {
637

    
638
        DerivedUnitFacade facade = DerivedUnitFacade.NewInstance(SpecimenOrObservationType.PreservedSpecimen, fieldUnit);
639
        facade.setCollection(collection);
640
        if(accessionNumber != null){
641
            facade.setAccessionNumber(accessionNumber);
642
        }
643
        return facade.innerDerivedUnit();
644
    }
645

    
646
    private BotanicalName makeBotanicalName(SimpleExcelTaxonImportState<CONFIG> state, String regNumber, String titleCacheStr, String nameStr,
647
                                            String authorStr, String nomRefTitle) {
648

    
649
        BotanicalName taxonName;// cache field for the taxonName.titleCache
650
        String taxonNameTitleCache = null;
651
        Map<String, AnnotationType> nameAnnotations = new HashMap<>();
652

    
653
        // TitleCache preprocessing
654
        if(titleCacheStr.endsWith(ANNOTATION_MARKER_STRING) || (authorStr != null && authorStr.endsWith(ANNOTATION_MARKER_STRING))){
655
            nameAnnotations.put("Author abbreviation not checked.", AnnotationType.EDITORIAL());
656
            titleCacheStr = titleCacheStr.replace(ANNOTATION_MARKER_STRING, "").trim();
657
            authorStr = authorStr.replace(ANNOTATION_MARKER_STRING, "").trim();
658
        }
659

    
660
        // parse the full taxon name
661
        if(!StringUtils.isEmpty(nomRefTitle)){
662
            String referenceSeparator = nomRefTitle.startsWith("in ") ? " " : ", ";
663
            String taxonFullNameStr = titleCacheStr + referenceSeparator + nomRefTitle;
664
            logger.debug(":::::" + taxonFullNameStr);
665
            taxonName = (BotanicalName) nameParser.parseReferencedName(taxonFullNameStr, NomenclaturalCode.ICNAFP, null);
666
        } else {
667
            taxonName = (BotanicalName) nameParser.parseFullName(titleCacheStr, NomenclaturalCode.ICNAFP, null);
668
        }
669

    
670
        taxonNameTitleCache = taxonName.getTitleCache().trim();
671
        if (taxonName.isProtectedTitleCache()) {
672
            logger.warn(csvReportLine(regNumber, "Name could not be parsed", titleCacheStr));
673
        } else {
674

    
675
            boolean doRestoreTitleCacheStr = false;
676

    
677
            // Check if titleCache and nameCache are plausible
678
            String titleCacheCompareStr = titleCacheStr;
679
            String nameCache = taxonName.getNameCache();
680
            String nameCompareStr = nameStr;
681
            if(taxonName.isBinomHybrid()){
682
                titleCacheCompareStr = titleCacheCompareStr.replace(" x ", " ×");
683
                nameCompareStr = nameCompareStr.replace(" x ", " ×");
684
            }
685
            if(taxonName.isMonomHybrid()){
686
                titleCacheCompareStr = titleCacheCompareStr.replaceAll("^X ", "× ");
687
                nameCompareStr = nameCompareStr.replace("^X ", "× ");
688
            }
689
            if(authorStr != null && authorStr.contains(" et ")){
690
                titleCacheCompareStr = titleCacheCompareStr.replaceAll(" et ", " & ");
691
            }
692
            if (!taxonNameTitleCache.equals(titleCacheCompareStr)) {
693
                logger.warn(csvReportLine(regNumber, "The generated titleCache differs from the imported string", taxonNameTitleCache, " != ", titleCacheStr, " ==> original titleCacheStr has been restored"));
694
                doRestoreTitleCacheStr = true;
695
            }
696
            if (!nameCache.trim().equals(nameCompareStr)) {
697
                logger.warn(csvReportLine(regNumber, "The parsed nameCache differs from field '" + NAMESTRING + "'", nameCache, " != ", nameCompareStr));
698
            }
699

    
700
            //  Author
701
            //nameParser.handleAuthors(taxonName, titleCacheStr, authorStr);
702
            //if (!titleCacheStr.equals(taxonName.getTitleCache())) {
703
            //    logger.warn(regNumber + ": titleCache has changed after setting authors, will restore original titleCacheStr");
704
            //    doRestoreTitleCacheStr = true;
705
            //}
706

    
707
            if(doRestoreTitleCacheStr){
708
                taxonName.setTitleCache(titleCacheStr, true);
709
            }
710

    
711
            // deduplicate
712
            replaceAuthorNamesAndNomRef(state, taxonName);
713
        }
714

    
715
        // Annotations
716
        if(!nameAnnotations.isEmpty()){
717
            for(String text : nameAnnotations.keySet()){
718
                taxonName.addAnnotation(Annotation.NewInstance(text, nameAnnotations.get(text), Language.DEFAULT()));
719
            }
720
            getNameService().save(taxonName);
721
        }
722
        return taxonName;
723
    }
724

    
725
    /**
726
     * @param state
727
     * @return
728
     */
729
    private TaxonNode getClassificationRootNode(IAPTImportState state) {
730

    
731
     //   Classification classification = state.getClassification();
732
     //   if (classification == null){
733
     //       IAPTImportConfigurator config = state.getConfig();
734
     //       classification = Classification.NewInstance(state.getConfig().getClassificationName());
735
     //       classification.setUuid(config.getClassificationUuid());
736
     //       classification.setReference(config.getSecReference());
737
     //       classification = getClassificationService().find(state.getConfig().getClassificationUuid());
738
     //   }
739
        TaxonNode rootNode = state.getRootNode();
740
        if (rootNode == null){
741
            rootNode = getTaxonNodeService().find(ROOT_UUID);
742
        }
743
        if (rootNode == null){
744
            Classification classification = state.getClassification();
745
            if (classification == null){
746
                Reference sec = state.getSecReference();
747
                String classificationName = state.getConfig().getClassificationName();
748
                Language language = Language.DEFAULT();
749
                classification = Classification.NewInstance(classificationName, sec, language);
750
                state.setClassification(classification);
751
                classification.setUuid(state.getConfig().getClassificationUuid());
752
                classification.getRootNode().setUuid(ROOT_UUID);
753
                getClassificationService().save(classification);
754
            }
755
            rootNode = classification.getRootNode();
756
            state.setRootNode(rootNode);
757
        }
758
        return rootNode;
759
    }
760

    
761
    private Collection getCollection(String collectionCode, String instituteStr, String subCollectionStr){
762

    
763
        Collection superCollection = null;
764
        if(subCollectionStr != null){
765
            superCollection = getCollection(collectionCode, instituteStr, null);
766
            collectionCode = subCollectionStr;
767
            instituteStr = null;
768
        }
769

    
770
        final String key = collectionCode + "-#i:" + StringUtils.defaultString(instituteStr);
771

    
772
        Collection collection = collectionMap.get(key);
773

    
774
        if(collection == null) {
775
            collection = Collection.NewInstance();
776
            collection.setCode(collectionCode);
777
            if(instituteStr != null){
778
                collection.setInstitute(Institution.NewNamedInstance(instituteStr));
779
            }
780
            if(superCollection != null){
781
                collection.setSuperCollection(superCollection);
782
            }
783
            collectionMap.put(key, collection);
784
            getCollectionService().save(collection);
785
        }
786

    
787
        return collection;
788
    }
789

    
790

    
791
    /**
792
     * @param record
793
     * @param originalKey
794
     * @param doUnescapeHtmlEntities
795
     * @return
796
     */
797
    private String getValue(HashMap<String, String> record, String originalKey, boolean doUnescapeHtmlEntities) {
798
        String value = record.get(originalKey);
799

    
800
        value = fixCharacters(value);
801

    
802
        if (! StringUtils.isBlank(value)) {
803
        	if (logger.isDebugEnabled()) {
804
        	    logger.debug(originalKey + ": " + value);
805
        	}
806
        	value = CdmUtils.removeDuplicateWhitespace(value.trim()).toString();
807
            if(doUnescapeHtmlEntities){
808
                value = StringEscapeUtils.unescapeHtml(value);
809
            }
810
        	return value.trim();
811
        }else{
812
        	return null;
813
        }
814
    }
815

    
816
    /**
817
     * Fixes broken characters.
818
     * For details see
819
     * http://dev.e-taxonomy.eu/redmine/issues/6035
820
     *
821
     * @param value
822
     * @return
823
     */
824
    private String fixCharacters(String value) {
825

    
826
        value = StringUtils.replace(value, "s$K", "š");
827
        value = StringUtils.replace(value, "n$K", "ň");
828
        value = StringUtils.replace(value, "e$K", "ě");
829
        value = StringUtils.replace(value, "r$K", "ř");
830
        value = StringUtils.replace(value, "c$K", "č");
831
        value = StringUtils.replace(value, "z$K", "ž");
832
        value = StringUtils.replace(value, "S>U$K", "Š");
833
        value = StringUtils.replace(value, "C>U$K", "Č");
834
        value = StringUtils.replace(value, "R>U$K", "Ř");
835
        value = StringUtils.replace(value, "Z>U$K", "Ž");
836
        value = StringUtils.replace(value, "g$K", "ǧ");
837
        value = StringUtils.replace(value, "s$A", "ś");
838
        value = StringUtils.replace(value, "n$A", "ń");
839
        value = StringUtils.replace(value, "c$A", "ć");
840
        value = StringUtils.replace(value, "e$E", "ę");
841
        value = StringUtils.replace(value, "o$H", "õ");
842
        value = StringUtils.replace(value, "s$C", "ş");
843
        value = StringUtils.replace(value, "t$C", "ț");
844
        value = StringUtils.replace(value, "S>U$C", "Ş");
845
        value = StringUtils.replace(value, "a$O", "å");
846
        value = StringUtils.replace(value, "A>U$O", "Å");
847
        value = StringUtils.replace(value, "u$O", "ů");
848
        value = StringUtils.replace(value, "g$B", "ğ");
849
        value = StringUtils.replace(value, "g$B", "ĕ");
850
        value = StringUtils.replace(value, "a$B", "ă");
851
        value = StringUtils.replace(value, "l$/", "ł");
852
        value = StringUtils.replace(value, ">i", "ı");
853
        value = StringUtils.replace(value, "i$U", "ï");
854
        // Special-cases
855
        value = StringUtils.replace(value, "&yacute", "ý");
856
        value = StringUtils.replace(value, ">L", "Ł"); // corrected rule
857
        value = StringUtils.replace(value, "E>U$D", "З");
858
        value = StringUtils.replace(value, "S>U$E", "Ş");
859
        value = StringUtils.replace(value, "s$E", "ş");
860

    
861
        value = StringUtils.replace(value, "c$k", "č");
862
        value = StringUtils.replace(value, " U$K", " Š");
863

    
864
        return value;
865
    }
866

    
867

    
868
    /**
869
	 *  Stores taxa records in DB
870
	 */
871
	@Override
872
    protected void firstPass(SimpleExcelTaxonImportState<CONFIG> state) {
873

    
874
        String lineNumber = "L#" + state.getCurrentLine() + ": ";
875
        logger.setLevel(Level.DEBUG);
876
        HashMap<String, String> record = state.getOriginalRecord();
877
        logger.debug(lineNumber + record.toString());
878

    
879
        Set<String> keys = record.keySet();
880
        for (String key: keys) {
881
            if (! expectedKeys.contains(key)){
882
                logger.warn(lineNumber + "Unexpected Key: " + key);
883
            }
884
        }
885

    
886
        String reg_id = record.get(REGISTRATIONNO_PK);
887

    
888
        //higherTaxon
889
        String higherTaxaString = record.get(HIGHERTAXON);
890
        boolean isFossil = false;
891
        if(higherTaxaString.startsWith("FOSSIL ")){
892
            higherTaxaString = higherTaxaString.replace("FOSSIL ", "");
893
            isFossil = true;
894
        }
895
        TaxonNode higherTaxon = getHigherTaxon(higherTaxaString, (IAPTImportState)state);
896

    
897
       //Taxon
898
        Taxon taxon = makeTaxon(record, state, higherTaxon, isFossil);
899
        if (taxon == null){
900
            logger.warn(lineNumber + "taxon could not be created and is null");
901
            return;
902
        }
903
        ((IAPTImportState)state).setCurrentTaxon(taxon);
904

    
905

    
906
		return;
907
    }
908

    
909
    private TaxonNode getHigherTaxon(String higherTaxaString, IAPTImportState state) {
910
        String[] higherTaxaNames = higherTaxaString.toLowerCase().replaceAll("[\\[\\]]", "").split(":");
911
        TaxonNode higherTaxonNode = null;
912

    
913
        ITaxonTreeNode rootNode = getClassificationRootNode(state);
914
        for (String htn :  higherTaxaNames) {
915
            htn = StringUtils.capitalize(htn.trim());
916
            Taxon higherTaxon = state.getHigherTaxon(htn);
917
            if (higherTaxon != null){
918
                higherTaxonNode = higherTaxon.getTaxonNodes().iterator().next();
919
            }else{
920
                BotanicalName name = makeHigherTaxonName(state, htn);
921
                Reference sec = state.getSecReference();
922
                higherTaxon = Taxon.NewInstance(name, sec);
923
                getTaxonService().save(higherTaxon);
924
                higherTaxonNode = rootNode.addChildTaxon(higherTaxon, sec, null);
925
                state.putHigherTaxon(htn, higherTaxon);
926
                getClassificationService().saveTreeNode(higherTaxonNode);
927
            }
928
            rootNode = higherTaxonNode;
929
        }
930
        return higherTaxonNode;
931
    }
932

    
933
    private BotanicalName makeHigherTaxonName(IAPTImportState state, String name) {
934

    
935
        Rank rank = guessRank(name);
936

    
937
        BotanicalName taxonName = BotanicalName.NewInstance(rank);
938
        taxonName.addSource(makeOriginalSource(state));
939
        taxonName.setGenusOrUninomial(StringUtils.capitalize(name));
940
        return taxonName;
941
    }
942

    
943
    private Rank guessRank(String name) {
944

    
945
        // normalize
946
        name = name.replaceAll("\\(.*\\)", "").trim();
947

    
948
        if(name.matches("^Plantae$|^Fungi$")){
949
           return Rank.KINGDOM();
950
        } else if(name.matches("^Incertae sedis$|^No group assigned$")){
951
           return rankFamilyIncertisSedis();
952
        } else if(name.matches(".*phyta$|.*mycota$")){
953
           return Rank.SECTION_BOTANY();
954
        } else if(name.matches(".*phytina$|.*mycotina$")){
955
           return Rank.SUBSECTION_BOTANY();
956
        } else if(name.matches("Gymnospermae$|.*ones$")){ // Monocotyledones, Dicotyledones
957
            return rankUnrankedSupraGeneric();
958
        } else if(name.matches(".*opsida$|.*phyceae$|.*mycetes$|.*ones$|^Musci$|^Hepaticae$")){
959
           return Rank.CLASS();
960
        } else if(name.matches(".*idae$|.*phycidae$|.*mycetidae$")){
961
           return Rank.SUBCLASS();
962
        } else if(name.matches(".*ales$")){
963
           return Rank.ORDER();
964
        } else if(name.matches(".*ineae$")){
965
           return Rank.SUBORDER();
966
        } else if(name.matches(".*aceae$")){
967
            return Rank.FAMILY();
968
        } else if(name.matches(".*oideae$")){
969
           return Rank.SUBFAMILY();
970
        } else
971
        //    if(name.matches(".*eae$")){
972
        //    return Rank.TRIBE();
973
        // } else
974
            if(name.matches(".*inae$")){
975
           return Rank.SUBTRIBE();
976
        } else if(name.matches(".*ae$")){
977
           return Rank.FAMILY();
978
        }
979
        return Rank.UNKNOWN_RANK();
980
    }
981

    
982
    private Rank rankUnrankedSupraGeneric() {
983

    
984
        if(rankUnrankedSupraGeneric == null){
985
            rankUnrankedSupraGeneric = Rank.NewInstance(RankClass.Suprageneric, "Unranked supra generic", " ", " ");
986
            getTermService().save(rankUnrankedSupraGeneric);
987
        }
988
        return rankUnrankedSupraGeneric;
989
    }
990

    
991
    private Rank rankFamilyIncertisSedis() {
992

    
993
        if(familyIncertisSedis == null){
994
            familyIncertisSedis = Rank.NewInstance(RankClass.Suprageneric, "Family incertis sedis", " ", " ");
995
            getTermService().save(familyIncertisSedis);
996
        }
997
        return familyIncertisSedis;
998
    }
999

    
1000
    private AnnotationType annotationTypeCaveats(){
1001
        if(annotationTypeCaveats == null){
1002
            annotationTypeCaveats = AnnotationType.NewInstance("Caveats", "Caveats", "");
1003
            getTermService().save(annotationTypeCaveats);
1004
        }
1005
        return annotationTypeCaveats;
1006
    }
1007

    
1008

    
1009
    /**
1010
     * @param state
1011
     * @return
1012
     */
1013
    private IdentifiableSource makeOriginalSource(IAPTImportState state) {
1014
        return IdentifiableSource.NewDataImportInstance("line: " + state.getCurrentLine(), null, state.getConfig().getSourceReference());
1015
    }
1016

    
1017

    
1018
    private Reference makeReference(IAPTImportState state, UUID uuidRef) {
1019
        Reference ref = state.getReference(uuidRef);
1020
        if (ref == null){
1021
            ref = getReferenceService().find(uuidRef);
1022
            state.putReference(uuidRef, ref);
1023
        }
1024
        return ref;
1025
    }
1026

    
1027
    private MarkerType markerTypeFossil(){
1028
        if(this.markerTypeFossil == null){
1029
            markerTypeFossil = MarkerType.NewInstance("isFossilTaxon", "isFossil", null);
1030
            getTermService().save(this.markerTypeFossil);
1031
        }
1032
        return markerTypeFossil;
1033
    }
1034

    
1035
    private String csvReportLine(String regId, String message, String ... fields){
1036
        StringBuilder out = new StringBuilder("regID#");
1037
        out.append(regId).append(",\"").append(message).append('"');
1038

    
1039
        for(String f : fields){
1040
            out.append(",\"").append(f).append('"');
1041
        }
1042
        return out.toString();
1043
    }
1044

    
1045

    
1046
}
(1-1/4)