diff --git a/README.md b/README.md index cb0c3cc..78d4e80 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Cargo-XML XFWB to ONE Record Converter +# Cargo-XML XFWB / XFZB to ONE Record Converter Note: A live demo of this converter is available at https://onerecord.riege.com/ ## Release versioning and IATA Ontology versions @@ -6,7 +6,7 @@ Note: A live demo of this converter is available at https://onerecord.riege.com/ The IATA Ontology version is reflected by the version of the Riege [one-record-ontologymodel library](https://github.com/riege/one-record-ontologymodel). It is highly recommended to use converter library 1.0 / Ontology version 2.0 or younger for -converting XFWB to ONE Record since IATA Ontology version 2.0 added major improvements for +converting XFWB or XFZB to ONE Record since IATA Ontology version 2.0 added major improvements for eAWB data fields. Versions of the converter library: @@ -22,7 +22,7 @@ This converter intentionally does neither set IDs nor makes use of persisted dat The converter is based on two main data structures to convert from Cargo-XML to ONE Record: * For parsing Cargo-XML the converter uses Java classes which had been generated from the - Cargo-XML XFWB3 schema. These generated classes are included via library + Cargo-XML schema. These generated classes are included via library https://github.com/riege/cargoxml-jaxb. Please note that the cargoxml-jaxb package does not contain any schema information from the IATA Cargo-XML Toolkit. @@ -40,12 +40,15 @@ Codes and units are copied 1:1 from the provided Cargo-XML message where applica Line breaks are respected for some fields if provided in XML, e.g. for multi-line goods description or address name/street. Line breaks are intentionally used in GoodsDescription to preserve and indicate descriptions from more than one field if applicaple from original XML -At the moment the converted is limited to map the Cargo-XML XFWB3 message to ONE Record JSON. -Please note that the XFWB3 to 1R converter is not mapping all possible data yet and has focus on a XFWB message from a forwarder to an airline +Version 0.9 is limited to map the Cargo-XML XFWB3 message to ONE Record JSON. +Version 1.0 adds a basic mapping for the Cargo-XML XFZB3 message. +Please note that the 1R converter is not mapping all possible data yet and has focus on the use-case 'message from a forwarder to an airline'. ## Usage ### Programming + +#### XFWB The main Cargo-XML class for XFWB3 is `com.riege.cargoxml.schema.xfwb3.WaybillType`. The `com.riege.onerecord.converter.XFWB3toOneRecordConverter` converts a provided @@ -67,8 +70,13 @@ into validation results plus hints and especially into ONE Record logistics data } Waybill oneRecordWaybill = converter.getOneRecordResult(); -The `Waybill` can be serialized easily into JSON, e.g. with https://github.com/FasterXML/jackson +The `Waybill` can be serialized easily into JSON, e.g. with https://github.com/FasterXML/jackson + +#### XFZB +For XFZB3, the main Cargo-XML class is `com.riege.cargoxml.schema.xfzb3.HouseWaybillType`. +The `com.riege.onerecord.converter.XFZB3toOneRecordConverter` converts in a similar way +from `HouseWaybillType` into `org.iata.cargo.model.Waybill`. ### Library usage The converter library is not published on mavenCentral (yet). diff --git a/build.gradle b/build.gradle index 199d253..06af414 100644 --- a/build.gradle +++ b/build.gradle @@ -88,10 +88,11 @@ dependencies { // implementation('com.github.riege:one-record-ontologymodel:1.1.2') { transitive = false } // jitpack dependency (commit hash (e.g. daec14a075) based, for unreleased development versions): // implementation('com.github.riege:one-record-ontologymodel:daec14a075') { transitive = false } + // implementation('com.github.riege:one-record-ontologymodel:e9f5883a21') { transitive = false } // github/riege internal dependency: // implementation('com.riege:one-record-ontologymodel:1.1.2') { transitive = false } - implementation('com.github.riege:one-record-ontologymodel:e9f5883a21') { transitive = false } + implementation('com.github.riege:one-record-ontologymodel:2.0.0') { transitive = false } // cargoxml-jaxb from riege.com cannot be retrieved via https://jitpack.io // because building it requires access to the Cargo-XML Toolkit schema files diff --git a/src/main/java/com/riege/onerecord/converter/CargoXMLtoOneRecordConverter.java b/src/main/java/com/riege/onerecord/converter/CargoXMLtoOneRecordConverter.java new file mode 100644 index 0000000..063bd17 --- /dev/null +++ b/src/main/java/com/riege/onerecord/converter/CargoXMLtoOneRecordConverter.java @@ -0,0 +1,80 @@ +package com.riege.onerecord.converter; + +import java.util.List; + +import org.iata.cargo.model.LogisticsObject; +import org.iata.cargo.model.Waybill; + +/** + * Abstract superclass for converters into ONE Record type T + * @param the type of ONE Record LogisticsObject to convert into + */ +public abstract class CargoXMLtoOneRecordConverter { + + public static final String VG_GENERAL = "General"; + public static final String VG_UNCERTAINTY = "Mapping-Uncertainty"; + public static final String VG_UNIMPLEMENTED = "Not-Implemented-Yet"; + public static final String VG_XMLDATAWARNING = "XML-Data-Warning"; + public static final String VG_XMLDATAERROR = "XML-Data-Error"; + public static final String VG_INFORMATION = "Info"; + protected final ValidationResult validationResult; + + public CargoXMLtoOneRecordConverter() { + validationResult = new ValidationResult(); + + // General Hints + addHint(VG_GENERAL, "This converter intentionally does neither set IDs nor makes use of persisted data for linked-data purposes."); + addHint(VG_GENERAL, "This converter is based on the schema from IATA CargoXML Toolkit 8th Edition."); + addHint(VG_GENERAL, "This converter is based on ONE RECORD datamodel Ontology 2.0, see https://github.com/IATA-Cargo/ONE-Record/tree/master/June-2021-standard-forCOTBendorsement/Data-Model"); + addHint(VG_GENERAL, "Codes and units are applied 1:1 from CargoXML where applicable."); + addHint(VG_GENERAL, "Line breaks are respected for some fields if provided in XML, e.g. for multi-line goods description or address name/street"); + addHint(VG_GENERAL, "Line breaks are intentionally added in 'goodsDescription' and 'accountingInformation' to preserve and indicate descriptions from more than one field if applicaple from original XML"); + addHint(VG_GENERAL, "The 1R converter is not mapping all possible data yet and has focus on the use-case of 'message from forwarder to airline'"); + } + + /** + * @return converted ONE Record result + */ + public abstract T getOneRecordResult(); + + /** + * @return complete conversion errors, warnings and hints + */ + public final ValidationResult getValidationResult() { + return validationResult; + } + + /** + * @return conversion hints + */ + public final List getValidationHints() { + return validationResult.getHints(); + } + + /** + * @return conversion warnings + */ + public final List getValidationWarnings() { + return validationResult.getWarnings(); + } + + /** + * @return conversion warnings + */ + public final List getValidationErrors() { + return validationResult.getErrors(); + } + + protected final void addHint(String group, String text) { + validationResult.addHint(group, text); + } + + protected final void addWarning(String group, String text) { + validationResult.addWarning(group, text); + } + + protected final void addError(String group, String text) { + validationResult.addError(group, text); + } + +} diff --git a/src/main/java/com/riege/onerecord/converter/ConverterUtil.java b/src/main/java/com/riege/onerecord/converter/ConverterUtil.java index 60e3a4d..ce636a2 100644 --- a/src/main/java/com/riege/onerecord/converter/ConverterUtil.java +++ b/src/main/java/com/riege/onerecord/converter/ConverterUtil.java @@ -11,10 +11,12 @@ import javax.xml.bind.Unmarshaller; import com.riege.cargoxml.schema.xfwb3.WaybillType; +import com.riege.cargoxml.schema.xfzb3.HouseWaybillType; public final class ConverterUtil { private Unmarshaller unmarshallerXFWB3; + private Unmarshaller unmarshallerXFZB3; private Unmarshaller getXFWB3Unmarshaller() throws JAXBException { if (unmarshallerXFWB3 == null) { @@ -24,7 +26,23 @@ private Unmarshaller getXFWB3Unmarshaller() throws JAXBException { return unmarshallerXFWB3; } + private Unmarshaller getXFZB3Unmarshaller() throws JAXBException { + if (unmarshallerXFZB3 == null) { + JAXBContext jaxbContext = JAXBContext.newInstance(HouseWaybillType.class.getPackage().getName()); + unmarshallerXFZB3 = jaxbContext.createUnmarshaller(); + } + return unmarshallerXFZB3; + } + + /** + * Deprecated in version 1.0, use unmarshallXFWB3 instead + */ + @Deprecated public WaybillType unmarshalXFWB3(InputStream inputStream) throws JAXBException { + return unmarshallXFWB3(inputStream); + } + + public WaybillType unmarshallXFWB3(InputStream inputStream) throws JAXBException { Object result = getXFWB3Unmarshaller().unmarshal(inputStream); if (!(result instanceof JAXBElement)) { throw new UnmarshalException("Unexpected class " + result.getClass().getName() @@ -37,6 +55,19 @@ public WaybillType unmarshalXFWB3(InputStream inputStream) throws JAXBException return (WaybillType) cargoxml; } + public HouseWaybillType unmarshallXFZB3(InputStream inputStream) throws JAXBException { + Object result = getXFZB3Unmarshaller().unmarshal(inputStream); + if (!(result instanceof JAXBElement)) { + throw new UnmarshalException("Unexpected class " + result.getClass().getName() + + " while unmarshalling, expected JAXBElement"); + } + Object cargoxml = ((JAXBElement)result).getValue(); + if (!(cargoxml instanceof HouseWaybillType)) { + throw new UnmarshalException("No CargoXML XFWZB3 format detected"); + } + return (HouseWaybillType) cargoxml; + } + public static boolean isNullOrEmpty( final Collection< ? > c ) { return c == null || c.isEmpty(); } diff --git a/src/main/java/com/riege/onerecord/converter/OneRecordTypeConstants.java b/src/main/java/com/riege/onerecord/converter/OneRecordTypeConstants.java index 5c98059..74a0300 100644 --- a/src/main/java/com/riege/onerecord/converter/OneRecordTypeConstants.java +++ b/src/main/java/com/riege/onerecord/converter/OneRecordTypeConstants.java @@ -274,6 +274,8 @@ public static final Set buildSet() { } /** + * @param the type of elements maintained by this set + * @param singleEntry is added to the generated and returned Set * @return Set built from the provided data, null if provided data is null */ public static final Set buildSet(T singleEntry) { @@ -284,6 +286,8 @@ public static final Set buildSet(T singleEntry) { } /** + * @param the type of elements maintained by this set + * @param array entries are added to the generated and returned Set * @return Set built from the provided data even if the array is empty, null if provided data is null */ public static final Set buildSet(T[] array) { @@ -294,6 +298,8 @@ public static final Set buildSet(T[] array) { } /** + * @param the type of elements maintained by this set + * @param list entries are added to the generated and returned Set * @return Set built from the provided data even if the list is empty, null if provided data is null */ public static final Set buildSet(List list) { diff --git a/src/main/java/com/riege/onerecord/converter/ValidationMessage.java b/src/main/java/com/riege/onerecord/converter/ValidationMessage.java index e868c43..fc0a521 100644 --- a/src/main/java/com/riege/onerecord/converter/ValidationMessage.java +++ b/src/main/java/com/riege/onerecord/converter/ValidationMessage.java @@ -49,8 +49,11 @@ public String getDetail() { } /** - * @Deprecated use {@link #getGroup()} and {@link #getDetail()} instead + * Deprecated in version 1.0 use {@link #getGroup()} and {@link #getDetail()} instead + * + * @return validation message as string, prefixed with group */ + @Deprecated public String getMessage() { return group == null ? detail : "[" + group + "] " + detail; } diff --git a/src/main/java/com/riege/onerecord/converter/ValidationResult.java b/src/main/java/com/riege/onerecord/converter/ValidationResult.java index effeebb..533c747 100644 --- a/src/main/java/com/riege/onerecord/converter/ValidationResult.java +++ b/src/main/java/com/riege/onerecord/converter/ValidationResult.java @@ -9,7 +9,9 @@ public class ValidationResult { private List messageList = new ArrayList<>(); /** - * @Deprecated in version 0.10, use {@link #addHint(String, String)} instead + * Deprecated in version 1.0, use {@link #addHint(String, String)} instead + * + * @param message message to be added as validation hint */ @Deprecated public void addHint(String message) { @@ -21,7 +23,9 @@ public void addHint(String group, String detail) { } /** - * @Deprecated in version 0.10, use {@link #addWarning(String, String)} instead + * Deprecated in version 1.0, use {@link #addWarning(String, String)} instead + * + * @param message message to be added as validation warning */ @Deprecated public void addWarning(String message) { @@ -33,7 +37,9 @@ public void addWarning(String group, String detail) { } /** - * @Deprecated in version 0.10, use {@link #addError(String, String)} instead + * Deprecated in version 1.0, use {@link #addError(String, String)} instead + * + * @param message message to be added as validation werror */ @Deprecated public void addError(String message) { diff --git a/src/main/java/com/riege/onerecord/converter/XFWB3toOneRecordConverter.java b/src/main/java/com/riege/onerecord/converter/XFWB3toOneRecordConverter.java index 02a9518..1dece14 100644 --- a/src/main/java/com/riege/onerecord/converter/XFWB3toOneRecordConverter.java +++ b/src/main/java/com/riege/onerecord/converter/XFWB3toOneRecordConverter.java @@ -99,17 +99,8 @@ * to 'Useful Resources' / 'JSON-LD representation' and * the 'Take a look at the JSON-LD examples' link. */ -public final class XFWB3toOneRecordConverter { +public final class XFWB3toOneRecordConverter extends CargoXMLtoOneRecordConverter { - public final static String VG_GENERAL = "General"; - public final static String VG_UNCERTAINTY = "Mapping-Uncertainty"; - public final static String VG_UNIMPLEMENTED = "Not-Implemented-Yet"; - public final static String VG_XMLDATAWARNING = "XML-Data-Warning"; - public final static String VG_XMLDATAERROR = "XML-Data-Error"; - public final static String VG_INFORMATION = "Info"; - - private final WaybillType xfwb; - private final ValidationResult validationResult; private final Waybill waybill; /** @@ -118,49 +109,20 @@ public final class XFWB3toOneRecordConverter { * @param xfwb CargoXML XFWB */ public XFWB3toOneRecordConverter(WaybillType xfwb) { - this.xfwb = xfwb; + super(); - validationResult = new ValidationResult(); waybill = OneRecordTypeConstants.createWaybill(); - - convertData(); + convertData(xfwb); } /** * @return converted OneRecord (master or direct) Waybill */ + @Override public Waybill getOneRecordResult() { return waybill; } - /** - * @return complete conversion errors, warnings and hints et al - */ - public ValidationResult getValidationResult() { - return validationResult; - } - - /** - * @return conversion hints - */ - public List getValidationHints() { - return validationResult.getHints(); - } - - /** - * @return conversion warnings - */ - public List getValidationWarnings() { - return validationResult.getWarnings(); - } - - /** - * @return conversion warnings - */ - public List getValidationErrors() { - return validationResult.getErrors(); - } - // ************************************************************************* // OneRecord helper instances @@ -176,7 +138,7 @@ public List getValidationErrors() { private BusinessHeaderDocumentType xmlBH; private String awbCurrency; - private void convertData() { + private void convertData(WaybillType xfwb) { /* * initialize the basic main 1R data structure: * BookingOption @@ -216,15 +178,6 @@ private void convertData() { xmlMC = xfwb.getMasterConsignment(); xmlBH = xfwb.getBusinessHeaderDocument(); - // General Hints() - addHint(VG_GENERAL, "This converter intentionally does neither set IDs nor makes use of persisted data for linked-data purposes."); - addHint(VG_GENERAL, "This converter is based on XFWB3 schema from IATA CargoXML Toolkit 8th Edition."); - addHint(VG_GENERAL, "This converter is based on ONE RECORD datamodel Ontology 1.1, see https://github.com/IATA-Cargo/ONE-Record/tree/master/June-2021-standard-forCOTBendorsement/Data-Model"); - addHint(VG_GENERAL, "Codes and units are applied 1:1 from CargoXML where applicable."); - addHint(VG_GENERAL, "Line breaks are respected for some fields if provided in XML, e.g. for multi-line goods description or address name/street"); - addHint(VG_GENERAL, "Line breaks are intentionally added in 'goodsDescription' and 'accountingInformation' to preserve and indicate descriptions from more than one field if applicaple from original XML"); - addHint(VG_GENERAL, "XFWB3 to 1R converter is not mapping all possible data yet and has focus on the use-case 'XFWB message from forwarder to airline'"); - if (xmlMC.getApplicableOriginCurrencyExchange() != null && xmlMC.getApplicableOriginCurrencyExchange().getSourceCurrencyCode() != null) { awbCurrency = value(xmlMC.getApplicableOriginCurrencyExchange().getSourceCurrencyCode()); waybill.setOriginCurrency(awbCurrency); @@ -265,18 +218,6 @@ private void convertData() { convertCIMPSegment29(); } - private void addHint(String group, String text) { - validationResult.addHint(group, text); - } - - private void addWarning(String group, String text) { - validationResult.addWarning(group, text); - } - - private void addError(String group, String text) { - validationResult.addError(group, text); - } - // ************************************************************************* // CIMP FWB Segment 2: AWB Consignment Details (M) // ************************************************************************* @@ -454,7 +395,6 @@ private void convertCIMPSegment05() { // CIMP FWB Segment 6: Consignee (M) // ************************************************************************* private void convertCIMPSegment06() { - // mainBooking.setConsignee(consignee); if (mainBooking.getParties() == null) { mainBooking.setParties(buildSet()); } @@ -1104,6 +1044,8 @@ private void convertCIMPSegment27() { // CIMP FWB Segment 29: OCI: Other Customs Information // ************************************************************************* private void convertCIMPSegment29() { + // See also XFZB3toOneRecordConverter#convertCIMPSegment06 + // keep in sync! if (isNullOrEmpty(xmlMC.getIncludedCustomsNote())) { return; } @@ -1176,7 +1118,7 @@ private void convertCIMPSegment29() { } } - private boolean updateSecurityDeclaration(CustomsInfo ci, SecurityDeclaration secDec, + static boolean updateSecurityDeclaration(CustomsInfo ci, SecurityDeclaration secDec, String previousCiSubjectCode) { // ISS = "Issuing" Security Status = issuing @@ -1270,7 +1212,7 @@ private boolean updateSecurityDeclaration(CustomsInfo ci, SecurityDeclaration se return false; } - private RegulatedEntity currentRegulatedEntity(SecurityDeclaration secDec, String previousCiSubjectCode) { + static RegulatedEntity currentRegulatedEntity(SecurityDeclaration secDec, String previousCiSubjectCode) { if ("ISS".equals(previousCiSubjectCode)) { return secDec.getRegulatedEntityIssuer(); } @@ -1285,7 +1227,7 @@ private RegulatedEntity currentRegulatedEntity(SecurityDeclaration secDec, Strin return null; } - private RegulatedEntity convert(CustomsInfo ci) { + static RegulatedEntity convert(CustomsInfo ci) { // Example: IE/ISS/RA/00084-01 // Example: ///AC/12345ABCDE // CustomsInformation / CustomsInfoSubjectCode / CustomsInfoContentCode / CustomsInfoNote @@ -1414,7 +1356,7 @@ private Company createCompany(FreightForwarderAddressType xmlAddress) { return company; } - private Address prepareCompanyAddress(Company company) { + static Address prepareCompanyAddress(Company company) { if (company.getBranch() == null) { company.setBranch(OneRecordTypeConstants.createCompanyBranch()); } diff --git a/src/main/java/com/riege/onerecord/converter/XFZB3ParserHelper.java b/src/main/java/com/riege/onerecord/converter/XFZB3ParserHelper.java new file mode 100644 index 0000000..1d06e0a --- /dev/null +++ b/src/main/java/com/riege/onerecord/converter/XFZB3ParserHelper.java @@ -0,0 +1,136 @@ +package com.riege.onerecord.converter; + +import java.math.BigDecimal; + +import org.iata.cargo.model.Country; +import org.iata.cargo.model.Location; +import org.iata.cargo.model.Value; + +import com.riege.cargoxml.schema.xfzb3.AmountType; +import com.riege.cargoxml.schema.xfzb3.ArrivalLocationType; +import com.riege.cargoxml.schema.xfzb3.CodeType; +import com.riege.cargoxml.schema.xfzb3.CountryIDType; +import com.riege.cargoxml.schema.xfzb3.CurrencyCodeType; +import com.riege.cargoxml.schema.xfzb3.DepartureLocationType; +import com.riege.cargoxml.schema.xfzb3.DocumentCodeType; +import com.riege.cargoxml.schema.xfzb3.FinalDestinationLocationType; +import com.riege.cargoxml.schema.xfzb3.IDType; +import com.riege.cargoxml.schema.xfzb3.ISO3AlphaCurrencyCodeContentType; +import com.riege.cargoxml.schema.xfzb3.MeasureType; +import com.riege.cargoxml.schema.xfzb3.OriginLocationType; +import com.riege.cargoxml.schema.xfzb3.QuantityType; +import com.riege.cargoxml.schema.xfzb3.TextType; + +public class XFZB3ParserHelper { + + public static String value(DocumentCodeType id) { + return id != null && id.getValue() != null ? id.getValue() : null; + } + + public static String value(IDType id) { + return id != null && id.getValue() != null ? id.getValue() : null; + } + + public static String value(CodeType code) { + return code != null && code.getValue() != null ? code.getValue() : null; + } + + public static String value(TextType text) { + return text != null && text.getValue() != null ? text.getValue() : null; + } + + public static Value value(MeasureType measure) { + if (measure == null || measure.getValue() == null) { + return null; + } + Value result = OneRecordTypeConstants.createValue(); + result.setUnit(measure.getUnitCode()); + result.setValue(measure.getValue().doubleValue()); + return result; + } + + public static BigDecimal bigDecimal(MeasureType weightMeasure) { + return weightMeasure == null ? null : weightMeasure.getValue(); + } + + public static String unitCode(MeasureType measure) { + if (measure == null || measure.getUnitCode() == null) { + return null; + } + return measure.getUnitCode(); + } + + public static Integer integerValue(QuantityType quantity) { + return quantity != null && quantity.getValue() != null ? Integer.valueOf(quantity.getValue().intValue()) : null; + } + + public static Value value(AmountType measure, String defaultCurrency) { + if (measure == null) { + return null; + } + Value result = OneRecordTypeConstants.createValue(); + result.setUnit(measure.getCurrencyID() == null ? defaultCurrency : measure.getCurrencyID().value()); + result.setValue(measure.getValue().doubleValue()); + return result; + } + + // ************************************************************************* + + public static Location value(DepartureLocationType locationType) { + return locationType == null + ? null + : value(locationType.getID(), locationType.getName(), locationType.getTypeCode()); + } + + public static Location value(ArrivalLocationType locationType) { + return locationType == null + ? null + : value(locationType.getID(), locationType.getName(), locationType.getTypeCode()); + } + + public static Location value(OriginLocationType locationType) { + return locationType == null + ? null + : value(locationType.getID(), locationType.getName()); + } + + public static Location value(FinalDestinationLocationType locationType) { + return locationType == null + ? null + : value(locationType.getID(), locationType.getName()); + } + + private static Location value(IDType locationCode, TextType locationName) { + return value(locationCode, locationName, null); + } + + private static Location value(IDType locationCode, TextType locationName, CodeType typeCode) { + if (locationCode == null) { + return null; + } + Location location = OneRecordTypeConstants.createLocation(); + location.setCode(value(locationCode)); + location.setLocationType(value(typeCode)); + location.setLocationName(value(locationName)); + return location; + } + + public static Country value(CountryIDType countryIDType, TextType countryName) { + if (countryIDType == null) { + return null; + } + Country country = OneRecordTypeConstants.createCountry(); + country.setCountryCode(countryIDType.getValue().value()); + country.setCountryName(value(countryName)); + return country; + } + + public static String value(CurrencyCodeType currencyCodeType) { + return currencyCodeType == null ? null : value(currencyCodeType.getValue()); + } + + public static String value(ISO3AlphaCurrencyCodeContentType currencyCodeType) { + return currencyCodeType == null ? null : currencyCodeType.value(); + } + +} diff --git a/src/main/java/com/riege/onerecord/converter/XFZB3toOneRecordConverter.java b/src/main/java/com/riege/onerecord/converter/XFZB3toOneRecordConverter.java new file mode 100644 index 0000000..af49450 --- /dev/null +++ b/src/main/java/com/riege/onerecord/converter/XFZB3toOneRecordConverter.java @@ -0,0 +1,629 @@ +package com.riege.onerecord.converter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.iata.cargo.codelists.ContactTypeCode; +import org.iata.cargo.codelists.OtherIdentifierTypeCode; +import org.iata.cargo.codelists.PartyRoleCode; +import org.iata.cargo.codelists.WaybillTypeCode; +import org.iata.cargo.model.Address; +import org.iata.cargo.model.BookingOption; +import org.iata.cargo.model.Carrier; +import org.iata.cargo.model.Company; +import org.iata.cargo.model.Contact; +import org.iata.cargo.model.CustomsInfo; +import org.iata.cargo.model.Dimensions; +import org.iata.cargo.model.Insurance; +import org.iata.cargo.model.Item; +import org.iata.cargo.model.OtherIdentifier; +import org.iata.cargo.model.Party; +import org.iata.cargo.model.Person; +import org.iata.cargo.model.Piece; +import org.iata.cargo.model.SecurityDeclaration; +import org.iata.cargo.model.Shipment; +import org.iata.cargo.model.TransportSegment; +import org.iata.cargo.model.ULD; +import org.iata.cargo.model.Value; +import org.iata.cargo.model.VolumetricWeight; +import org.iata.cargo.model.Waybill; + +import com.riege.cargoxml.schema.xfzb3.ConsigneePartyType; +import com.riege.cargoxml.schema.xfzb3.ConsignorPartyType; +import com.riege.cargoxml.schema.xfzb3.StructuredAddressType; +import com.riege.cargoxml.schema.xfzb3.TextType; +import com.riege.cargoxml.schema.xfzb3.TradeContactType; +import com.riege.cargoxml.schema.xfzb3.AuthenticationLocationType; +import com.riege.cargoxml.schema.xfzb3.CarrierAuthenticationType; +import com.riege.cargoxml.schema.xfzb3.CodeType; +import com.riege.cargoxml.schema.xfzb3.ConsignorAuthenticationType; +import com.riege.cargoxml.schema.xfzb3.CustomsNoteType; +import com.riege.cargoxml.schema.xfzb3.HouseConsignmentItemType; +import com.riege.cargoxml.schema.xfzb3.HouseConsignmentType; +import com.riege.cargoxml.schema.xfzb3.LogisticsPackageType; +import com.riege.cargoxml.schema.xfzb3.SpatialDimensionType; +import com.riege.cargoxml.schema.xfzb3.BusinessHeaderDocumentType; +import com.riege.cargoxml.schema.xfzb3.MasterConsignmentType; +import com.riege.cargoxml.schema.xfzb3.MessageHeaderDocumentType; +import com.riege.cargoxml.schema.xfzb3.HouseWaybillType; +import com.riege.cargoxml.schema.xfzb3.UnitLoadTransportEquipmentType; + +import static com.riege.onerecord.converter.ConverterUtil.isNullOrEmpty; +import static com.riege.onerecord.converter.OneRecordTypeConstants.buildSet; +import static com.riege.onerecord.converter.XFZB3ParserHelper.integerValue; +import static com.riege.onerecord.converter.XFZB3ParserHelper.value; + +public class XFZB3toOneRecordConverter extends CargoXMLtoOneRecordConverter { + + private final Waybill waybill; + + public XFZB3toOneRecordConverter(HouseWaybillType xfzb) { + super(); + + waybill = OneRecordTypeConstants.createWaybill(); + convertData(xfzb); + } + + @Override + public Waybill getOneRecordResult() { + return waybill; + } + + private BookingOption mainBooking; + private Carrier mainAirline; + private TransportSegment mainTransportSegment; + private Shipment mainShipment; + private Piece mainPiece; + + // CargoXML shortcuts + private MessageHeaderDocumentType xmlMH; + private MasterConsignmentType xmlMC; + private BusinessHeaderDocumentType xmlBH; + + private HouseConsignmentType xmlHouse; + private void convertData(HouseWaybillType xfzb) { + + /* + * initialize the basic main 1R data structure: + * BookingOption + * with shipper, consignee, freightForwarder... + * with TransportSegment as "transportMovement" which + * representing the AWB-transport such as shipment-dep/des + * with transportMovement which has a Piece as "transportedPieces" which + * represents a piece/package related details on AWB level + * BookingOption also has a Shipment as "shipmentDetails" which + * represents some details only available for a shipment such + * as Insurance.. + */ + // mainBooking = OneRecordTypeConstants.createBooking(); + // waybill.setBookingRef(mainBooking); + mainBooking = OneRecordTypeConstants.createBookingOption(); + waybill.setBooking(mainBooking); + + // NOTE: v1.1 is the last version which uses TransportSegment + // will be replaced by TransportMovement in future versions + mainTransportSegment = OneRecordTypeConstants.createTransportSegment(); + mainBooking.setTransportMovement(buildSet(mainTransportSegment)); + + mainShipment = OneRecordTypeConstants.createShipment(); + mainBooking.setShipmentDetails(mainShipment); + + mainPiece = OneRecordTypeConstants.createPiece(); + mainTransportSegment.setTransportedPieces(buildSet(mainPiece)); + + // Add the main carrier, as per AWB prefix + mainAirline = OneRecordTypeConstants.createCarrier(); + mainBooking.setCarrier(mainAirline); + + /* + * initialize some shortcuts to main CargoXML elements + */ + xmlMH = xfzb.getMessageHeaderDocument(); + xmlMC = xfzb.getMasterConsignment(); + xmlBH = xfzb.getBusinessHeaderDocument(); + xmlHouse = xmlMC.getIncludedHouseConsignment(); + + waybill.setWaybillType(WaybillTypeCode.HOUSE); + /* + * Conversion is split by topic of the individual CIMP segments, + * just to allow easier navigation though source code. + * CargoXML elements which have no CIMP mapping should get mapped + * in the CIMP segment which matches best topic-wise. + */ + convertSignatory(); // Mandatory fields in XFZB not in FHL + convertCIMPSegment02(); // Master AWB Consignment Detail + convertCIMPSegment03to05(); // House Waybill Summary Details, HTS, NOG + convertCIMPSegment06(); // OCI + convertCIMPSegment07(); // Shipper + convertCIMPSegment08(); // Consignee + convertCIMPSegment09(); // Charge Declarations (O) + } + + private void convertSignatory() { + // Copied from XFWB3toOneRecordConverter#convertCIMPSegment16to17Signatory + // but using classes from xfzb3 schema package + CarrierAuthenticationType sigCarrier = xmlBH.getSignatoryCarrierAuthentication(); + if (sigCarrier != null) { + waybill.setCarrierDeclarationSignature(value(sigCarrier.getSignatory())); + waybill.setCarrierDeclarationDate(sigCarrier.getActualDateTime().toGregorianCalendar().getTime()); + AuthenticationLocationType location = sigCarrier.getIssueAuthenticationLocation(); + if (location != null) { + waybill.setCarrierDeclarationPlace(OneRecordTypeConstants.createLocation()); + waybill.getCarrierDeclarationPlace().setCode(value(location.getName())); + } + } + ConsignorAuthenticationType sigConsignor = xmlBH.getSignatoryConsignorAuthentication(); + if (sigConsignor != null) { + waybill.setConsignorDeclarationSignature(buildSet(value(sigConsignor.getSignatory()))); + } + } + + // ************************************************************************* + // CIMP FHL Segment 2: Master AWB Consignment Detail (M) + // ************************************************************************* + private void convertCIMPSegment02() { + addWarning(VG_INFORMATION, " (=Master AWB number) is ignored, this House Waybill should be added to Master Waybill#containedWaybills instead"); + } + + // ************************************************************************* + // CIMP FHL Segment 3: House Waybill Summary Details (M) + // CIMP FHL Segment 4: Free text Description Of Goods (O) + // CIMP FHL Segment 5: HTS Codes (O) + // ************************************************************************* + private void convertCIMPSegment03to05() { + waybill.setWaybillNumber(value(xmlBH.getID())); + + // Copied from XFWB3toOneRecordConverter#convertCIMPSegment12: + mainShipment.setTotalSLAC(integerValue(xmlHouse.getPackageQuantity())); + // HTS: + List hts = new ArrayList<>(); + List nog = new ArrayList<>(); + List allDims = new ArrayList<>(); + List allULD = new ArrayList<>(); + for (HouseConsignmentItemType hci : xmlHouse.getIncludedHouseConsignmentItem()) { + for (CodeType codeType : hci.getTypeCode()) { + if (codeType != null) { + hts.add(value(codeType)); + } + } + if (hci.getNatureIdentificationTransportCargo() != null) { + String nature = value( + hci.getNatureIdentificationTransportCargo().getIdentification()); + if (nature != null) { + nog.addAll(Arrays.asList(nature.split("\n"))); + } + } + + if (mainPiece.getProduct() == null) { + mainPiece.setProduct(buildSet()); + } + + if (hci.getOriginCountry() != null) { + mainPiece.setProductionCountry( + value(hci.getOriginCountry().getID(), null)); + } + + for (LogisticsPackageType lp : hci.getTransportLogisticsPackage()) { + int xmlPackageCount = lp.getItemQuantity() == null + ? 1 + : integerValue(lp.getItemQuantity()); + SpatialDimensionType xmlDim = lp.getLinearSpatialDimension(); + if (xmlDim == null) { + continue; + } + Dimensions dim1R = OneRecordTypeConstants.createDimensions(); + dim1R.setHeight(value(xmlDim.getHeightMeasure())); + dim1R.setLength(value(xmlDim.getLengthMeasure())); + dim1R.setWidth(value(xmlDim.getWidthMeasure())); + Item item = OneRecordTypeConstants.createItem(); + // "Product" is mandatory for item as per Ontology + item.setProduct(OneRecordTypeConstants.createProduct()); + item.setDimensions(dim1R); + item.setWeight(value(lp.getGrossWeightMeasure())); + Value count = OneRecordTypeConstants.createValue(); + count.setValue(Double.valueOf(xmlPackageCount)); + item.setQuantity(count); + allDims.add(item); + } + + UnitLoadTransportEquipmentType xmlULD = hci.getAssociatedUnitLoadTransportEquipment(); + if (xmlULD != null) { + ULD uld1R = OneRecordTypeConstants.createULD(); + uld1R.setSerialNumber(value(xmlULD.getID())); + uld1R.setTareWeight(value(xmlULD.getTareWeightMeasure())); + uld1R.setUldTypeCode(value(xmlULD.getCharacteristicCode())); + if (xmlULD.getOperatingParty() != null) { + uld1R.setOwnerCode(value(xmlULD.getOperatingParty().getPrimaryID())); + } + allULD.add(uld1R); + } + } + + if (xmlHouse.getSummaryDescription() != null) { + if (nog.isEmpty()) { + nog.addAll(Arrays.asList(value(xmlHouse.getSummaryDescription()).split("\n"))); + } else { + addHint(VG_INFORMATION, "IncludedHouseConsignmentItem.NatureIdentificationTransportCargo got preferred over IncludedHouseConsignment.SummaryDescription"); + } + } + + // adding now: + if (!hts.isEmpty()) { + if (mainPiece.getContainedItems() == null) { + mainPiece.setContainedItems(buildSet()); + } + for (String hsCode : hts) { + Item item = OneRecordTypeConstants.createItem(); + item.setProduct(OneRecordTypeConstants.createProduct()); + item.getProduct().setHsCode(hsCode); + mainPiece.getContainedItems().add(item); + } + } + // Dimensions + if (!allDims.isEmpty()) { + if (mainPiece.getContainedItems() == null) { + mainPiece.setContainedItems(OneRecordTypeConstants.buildSet()); + } + mainPiece.getContainedItems().addAll(allDims); + } + // ULDs + if (!allULD.isEmpty()) { + if (mainTransportSegment.getTransportedUlds() == null) { + mainTransportSegment.setTransportedUlds(OneRecordTypeConstants.buildSet()); + } + mainTransportSegment.getTransportedUlds().addAll(allULD); + } + // Nature of Goods + for (String s : nog) { + if (mainPiece.getGoodsDescription() == null) { + mainPiece.setGoodsDescription(s); + } else { + mainPiece.setGoodsDescription(mainPiece.getGoodsDescription() + "\n" + s); + } + } + + + // Copied from XFWB3toOneRecordConverter#convertCIMPSegment02: + // departure and destination + mainTransportSegment.setDepartureLocation(value(xmlHouse.getOriginLocation())); + mainTransportSegment.setArrivalLocation(value(xmlHouse.getFinalDestinationLocation())); + + // totalPieceCount + mainShipment.setTotalPieceCount(integerValue(xmlHouse.getTotalPieceQuantity())); + + // totalGrossWeight + mainShipment.setTotalGrossWeight(value(xmlHouse.getIncludedTareGrossWeightMeasure())); + mainPiece.setGrossWeight(mainShipment.getTotalGrossWeight()); + addHint(VG_INFORMATION, "(Total)GrossWeight is mandatory on Shipment and on Piece, value from MasterConsignment/IncludedTareGrossWeightMeasure is used for both"); + + VolumetricWeight volumetricWeight = OneRecordTypeConstants.createVolumetricWeight(); + volumetricWeight.setChargeableWeight(mainShipment.getTotalGrossWeight()); + mainShipment.setVolumetricWeight(buildSet(volumetricWeight)); + mainPiece.setVolumetricWeight(volumetricWeight); + addHint(VG_INFORMATION, "VolumetricWeight is mandatory on Shipment and on Piece, value from MasterConsignment/IncludedTareGrossWeightMeasure is used for both"); + + // totalVolume + if (xmlHouse.getGrossVolumeMeasure() != null) { + Dimensions volume = OneRecordTypeConstants.createDimensions(); + volume.setVolume(value(xmlHouse.getGrossVolumeMeasure())); + mainPiece.setDimensions(volume); + } + } + + // ************************************************************************* + // CIMP FHL Segment 6: OCI + // ************************************************************************* + private void convertCIMPSegment06() { + // Copied from XFWB3toOneRecordConverter#convertCIMPSegment29: + // keep in sync! + if (isNullOrEmpty(xmlHouse.getIncludedCustomsNote())) { + return; + } + SecurityDeclaration secDec = OneRecordTypeConstants.createSecurityDeclaration(); + String previousCiSubjectCode = "X"; + boolean haveSecDec = false; + boolean haveCTCP = false; + for (CustomsNoteType xmlCustNote : xmlHouse.getIncludedCustomsNote()) { + /* + * Example: + * CP + * HILDA HILARIOUS + * SHP + * IE + */ + String contentCode = value(xmlCustNote.getContentCode()); + String contentText = value(xmlCustNote.getContent()); + String subjectCode = value(xmlCustNote.getSubjectCode()); + String countryCode = xmlCustNote.getCountryID() == null + ? null + : value(xmlCustNote.getCountryID(), null).getCountryCode(); + CustomsInfo custInfo = OneRecordTypeConstants.createCustomsInfo(); + + custInfo.setCustomsInfoContentCode(contentCode); + custInfo.setCustomsInfoCountryCode(countryCode); + // data field "customsInfoNote": + // Free text for customs remarks, not used in OCI Composition Rules Table + custInfo.setCustomsInfoSubjectCode(subjectCode); + custInfo.setCustomsInformation(contentText); + + if ("CT".equals(contentCode) || + "CP".equals(contentCode)) + { + // CT = Contact Telephone + // CP = Contact Person + // we already added them during address processing! + if (!haveCTCP) { + addHint(VG_INFORMATION, "IncludedCustomsNote 'CT' and 'CP' (Contact Telephone / Contact Person) are added as contact to the applicable company rather converted to a CustomsInfo"); + } + haveCTCP = true; + continue; + } + if (XFWB3toOneRecordConverter.updateSecurityDeclaration(custInfo, secDec, previousCiSubjectCode)) { + haveSecDec = true; + // /IE/ISS/RA/00084-01 + addHint(VG_INFORMATION, + "IncludedCustomsNote '" + + (countryCode == null ? "" : countryCode) + + "/" + + (subjectCode == null ? "" : subjectCode) + + "/" + + (contentCode == null ? "" : contentCode) + + "/" + + (contentText == null ? "" : contentText) + + "' transformed into SecurityStatus." + ); + } else { + if (mainPiece.getCustomsInfo() == null) { + mainPiece.setCustomsInfo(OneRecordTypeConstants.buildSet()); + } + mainPiece.getCustomsInfo().add(custInfo); + } + if (subjectCode != null) { + previousCiSubjectCode = subjectCode; + } + } + if (haveSecDec) { + mainPiece.setSecurityStatus(secDec); + } + } + + // ************************************************************************* + // CIMP FHL Segment 7: Shipper Name and Address (O) + // ************************************************************************* + private void convertCIMPSegment07() { + // Copied from XFWB3toOneRecordConverter#convertCIMPSegment05: + if (mainBooking.getParties() == null) { + mainBooking.setParties(buildSet()); + } + mainBooking.getParties().add(createParty( + PartyRoleCode.SHP, + xmlHouse.getConsignorParty(), + getCustomsNotesBySubjectCode("SHP") + )); + } + + // ************************************************************************* + // CIMP FHL Segment 8: Consignee Name and Address (C) + // ************************************************************************* + private void convertCIMPSegment08() { + // Copied from XFWB3toOneRecordConverter#convertCIMPSegment06: + if (mainBooking.getParties() == null) { + mainBooking.setParties(buildSet()); + } + mainBooking.getParties().add(createParty( + PartyRoleCode.CNE, + xmlHouse.getConsigneeParty(), + getCustomsNotesBySubjectCode("CNE") + )); + } + + private Party createParty(PartyRoleCode partyRole, + ConsignorPartyType xmlParty, + List xmlCustomsNotes) + { + Company company = enhanceCompany( + partyRole, + createCompany(xmlParty.getPostalStructuredAddress()), + xmlParty.getName(), + xmlParty.getDefinedTradeContact(), + xmlCustomsNotes); + String accountID = xmlParty.getAccountID() != null + ? value(xmlParty.getAccountID()) + : null; + return createParty(partyRole, company, accountID); + } + + private Party createParty(PartyRoleCode partyRole, + ConsigneePartyType xmlParty, + List xmlCustomsNotes) + { + Company company = enhanceCompany( + partyRole, + createCompany(xmlParty.getPostalStructuredAddress()), + xmlParty.getName(), + xmlParty.getDefinedTradeContact(), + xmlCustomsNotes); + String accountID = xmlParty.getAccountID() != null + ? value(xmlParty.getAccountID()) + : null; + return createParty(partyRole, company, accountID); + } + + private Party createParty(PartyRoleCode partyRole, Company company, String accountID) { + Party party = OneRecordTypeConstants.createParty(); + party.setPartyRole(partyRole); + party.setPartyDetails(company); + if (accountID != null) { + // See https://github.com/IATA-Cargo/ONE-Record/issues/130 + OtherIdentifier oi = OneRecordTypeConstants.createOtherIdentifier(); + oi.setOtherIdentifierType(OtherIdentifierTypeCode.ACCOUNT_ID); + oi.setOtherIdentifierType("AccountID"); + oi.setIdentifier(accountID); + party.setOtherIdentifiers(buildSet(oi)); + } + return party; + } + + private Company createCompany(StructuredAddressType xmlAddress) { + if (xmlAddress == null) { + return null; + } + Company company = OneRecordTypeConstants.createCompany(); + Address address = prepareCompanyAddress(company); + String street = value(xmlAddress.getStreetName()); + if (street != null) { + address.setStreet(buildSet(street.split("\n"))); + } + address.setCityName(value(xmlAddress.getCityName())); + address.setPostalCode(value(xmlAddress.getPostcodeCode())); + address.setCountry(value(xmlAddress.getCountryID(), xmlAddress.getCountryName())); + return company; + } + static Address prepareCompanyAddress(Company company) { + if (company.getBranch() == null) { + company.setBranch(OneRecordTypeConstants.createCompanyBranch()); + } + if (company.getBranch().getLocation() == null) { + company.getBranch().setLocation(OneRecordTypeConstants.createLocation()); + } + if (company.getBranch().getLocation().getAddress() == null) { + company.getBranch().getLocation().setAddress(OneRecordTypeConstants.createAddress()); + } + return company.getBranch().getLocation().getAddress(); + } + + private Contact createContact(ContactTypeCode type, String value) { + Contact contact = OneRecordTypeConstants.createContact(); + contact.setContactType(type); + contact.setContactValue(value); + return contact; + } + private Company enhanceCompany(PartyRoleCode partyRole, + Company company, TextType xmlName, + List xmlContacts, List xmlPartyCustomsNotes) + { + if (company == null) { + return null; + } + String name = value(xmlName); + if (name != null && name.contains("\n")) { + String[] array = name.split("\n"); + company.setCompanyName(array[0]); + // company.getBranch().setBranchName(array[1]); + company.getBranch().setBranchName(name); + } else { + company.setCompanyName(name); + company.getBranch().setBranchName(name); + } + + final Person person = OneRecordTypeConstants.createPerson(); + String phone = null; + String fax = null; + String mail = null; + boolean haveContact = false; + if (xmlContacts != null) { + for (TradeContactType tc : xmlContacts) { + person.setLastName(value(tc.getPersonName())); + if (tc.getDirectTelephoneCommunication() != null) { + haveContact = true; + phone = value( + tc.getDirectTelephoneCommunication().getCompleteNumber()); + } + if (tc.getURIEmailCommunication() != null) { + haveContact = true; + mail = value(tc.getURIEmailCommunication().getURIID()); + } + if (tc.getFaxCommunication() != null) { + haveContact = true; + fax = value(tc.getFaxCommunication().getCompleteNumber()); + } + } + } + if (xmlPartyCustomsNotes != null) { + for (CustomsNoteType customsNote : xmlPartyCustomsNotes) { + if ("CT".equals(value(customsNote.getContentCode()))) { + // CT = Contact Telephone + if (phone != null) { + addHint(VG_INFORMATION, + "IncludedCustomsNote 'CT' (Contact Telephone) got preferred over for " + + partyRole.code()); + } + haveContact = true; + phone = value(customsNote.getContent()); + } else + if ("CP".equals(value(customsNote.getContentCode()))) { + // CP = Contact Person + haveContact = true; + person.setLastName(value(customsNote.getContent())); + } + } + } + if (haveContact) { + person.setContact(buildSet()); + if (phone != null) { + person.getContact().add(createContact(ContactTypeCode.PHONE, phone)); + } + if (fax != null) { + person.getContact().add(createContact(ContactTypeCode.FAX, fax)); + } + if (mail != null) { + person.getContact().add(createContact(ContactTypeCode.EMAIL, mail)); + } + company.getBranch().setContactPersons(buildSet(person)); + } + return company; + } + + private List getCustomsNotesBySubjectCode(String subjectCode) + { + if (subjectCode == null) { + return null; + } + return xmlHouse.getIncludedCustomsNote().stream() + .filter(icn -> subjectCode.equals(value(icn.getSubjectCode()))) + .collect(Collectors.toList()); + } + + // ************************************************************************* + // CIMP FHL Segment 9: Charge Declarations (0) + // ************************************************************************* + private void convertCIMPSegment09() { + if (xmlHouse.getTotalPrepaidChargeAmount() != null) { + addWarning(VG_UNCERTAINTY, + "Unclear where to put "); + } + if (xmlHouse.getTotalCollectChargeAmount() != null) { + addWarning(VG_UNCERTAINTY, + "Unclear where to put "); + } + + // Copied from XFWB3toOneRecordConverter#convertCIMPSegment11: + Insurance insurance = OneRecordTypeConstants.createInsurance(); + insurance.setNvdIndicator(xmlHouse.isNilInsuranceValueIndicator()); + boolean isNilInsurance = xmlHouse.isNilInsuranceValueIndicator() != null && xmlHouse.isNilInsuranceValueIndicator(); + if (!isNilInsurance) { + insurance.setInsuranceAmount( + value(xmlHouse.getInsuranceValueAmount(), null)); + } + mainShipment.setInsurance(insurance); + + mainPiece.setNvdForCarriage(xmlHouse.isNilCarriageValueIndicator()); + boolean isNilCarriage = xmlHouse.isNilCarriageValueIndicator() != null && xmlHouse.isNilCarriageValueIndicator(); + if (!isNilCarriage) { + mainPiece.setDeclaredValueForCarriage(buildSet( + xmlHouse.getDeclaredValueForCarriageAmount().getValue().toString() + )); + } + + mainPiece.setNvdForCustoms(xmlHouse.isNilCustomsValueIndicator()); + boolean isNilCustoms = xmlHouse.isNilCustomsValueIndicator() != null && xmlHouse.isNilCustomsValueIndicator(); + if (!isNilCustoms) { + mainPiece.setDeclaredValueForCustoms(buildSet( + xmlHouse.getDeclaredValueForCustomsAmount().getValue().toString() + )); + } + } + +} diff --git a/src/test/java/com/riege/onerecord/converter/XFWB3toOneRecordConverterTest.java b/src/test/java/com/riege/onerecord/converter/XFWB3toOneRecordConverterTest.java index eaa1bfb..d86efdf 100644 --- a/src/test/java/com/riege/onerecord/converter/XFWB3toOneRecordConverterTest.java +++ b/src/test/java/com/riege/onerecord/converter/XFWB3toOneRecordConverterTest.java @@ -196,7 +196,7 @@ private Result fileProcessingTest(String filename) throws JAXBException, JsonPro mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")); InputStream is = ClassLoader.getSystemResourceAsStream(filename); - WaybillType xfwb = new ConverterUtil().unmarshalXFWB3(is); + WaybillType xfwb = new ConverterUtil().unmarshallXFWB3(is); result.awb = xfwb.getBusinessHeaderDocument().getID().getValue(); result.converter = new XFWB3toOneRecordConverter(xfwb); result.json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result.converter.getOneRecordResult()); diff --git a/src/test/java/com/riege/onerecord/converter/XFZB3toOneRecordConverterTest.java b/src/test/java/com/riege/onerecord/converter/XFZB3toOneRecordConverterTest.java new file mode 100644 index 0000000..acecb43 --- /dev/null +++ b/src/test/java/com/riege/onerecord/converter/XFZB3toOneRecordConverterTest.java @@ -0,0 +1,118 @@ +package com.riege.onerecord.converter; + +import java.io.InputStream; +import java.text.SimpleDateFormat; + +import javax.xml.bind.JAXBException; + +import org.iata.cargo.codelists.WaybillTypeCode; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +import com.riege.cargoxml.schema.xfwb3.WaybillType; +import com.riege.cargoxml.schema.xfzb3.HouseWaybillType; + +public class XFZB3toOneRecordConverterTest { + + @Test + public void testSEL22222222() throws JAXBException, JsonProcessingException { + XFZB3toOneRecordConverterTest.Result result = + fileProcessingTest("SEL22222222_XFZB.xml"); + Assertions.assertNotNull(result.converter.getValidationWarnings()); + Assertions.assertFalse(result.converter.getValidationWarnings().isEmpty()); + for (ValidationMessage msg : result.converter.getValidationWarnings()) { + System.out.println(result.awb + " WARNING: " + msg.getMessage()); + } + + Assertions.assertNotNull(result.converter.getValidationErrors()); + Assertions.assertTrue(result.converter.getValidationErrors().isEmpty()); + for (ValidationMessage msg : result.converter.getValidationErrors()) { + System.out.println(result.awb + " ERROR: " + msg.getMessage()); + } + + for (ValidationMessage msg : result.converter.getValidationHints()) { + System.out.println(result.awb + " HINT: " + msg.getMessage()); + } + + System.out.println(result.awb + " JSON=\n" + result.json); + Assertions.assertEquals( + WaybillTypeCode.HOUSE.code(), result.converter.getOneRecordResult().getWaybillType()); + + // check that all payload is also in the JSON: + Assertions.assertTrue(result.json.contains("waybillType\" : \"House\"")); + Assertions.assertTrue(result.json.contains("waybillNumber\" : \"SEL22222222\"")); + Assertions.assertTrue(result.json.contains("2022-04-29T00:00:00")); + Assertions.assertTrue(result.json.contains("ARMIN AIRLINE")); + Assertions.assertTrue(result.json.contains("SIEGFRIED SIGNATURE")); + Assertions.assertTrue(result.json.contains("Location#code\" : \"ICN\"")); + Assertions.assertTrue(result.json.contains("Location#code\" : \"FDH\"")); + Assertions.assertFalse(result.json.contains("Location#code\" : \"FRA\"")); + // FEHLT Assertions.assertTrue(result.json.contains("8112345678")); + Assertions.assertTrue(result.json.contains("nvdForCarriage\" : true")); + Assertions.assertTrue(result.json.contains("nvdForCustoms\" : true")); + Assertions.assertTrue(result.json.contains("nvdIndicator\" : true")); + // Unclear where to put + // Assertions.assertTrue(result.json.contains("420")); + Assertions.assertTrue(result.json.contains("134.0")); + + Assertions.assertTrue(result.json.contains("customsInformation\" : \"5432109876\"")); + Assertions.assertTrue(result.json.contains("Piece#goodsDescription\" : \"SPARE PARTS\"")); + + Assertions.assertTrue(result.json.contains("SPARE PART LTD")); + Assertions.assertTrue(result.json.contains("42. GONGDAN SEONGSAN")); + Assertions.assertTrue(result.json.contains("CHANGWON-SI. KYUN")); + Assertions.assertTrue(result.json.contains("12345")); + Assertions.assertTrue(result.json.contains("\"KR\"")); + Assertions.assertTrue(result.json.contains("0553456789012")); + Assertions.assertFalse(result.json.contains("055281234567890")); + Assertions.assertTrue(result.json.contains("KIM QUAN KWUN")); + + Assertions.assertTrue(result.json.contains("REPAIR DIENST GMBH")); + Assertions.assertTrue(result.json.contains("SPEZIAL-STR. 13")); + Assertions.assertTrue(result.json.contains("87654")); + Assertions.assertTrue(result.json.contains("FRIEDRICHSHAFEN")); + Assertions.assertTrue(result.json.contains("DE")); + + // OCIs + Assertions.assertTrue(result.json.contains("customsInfoContentCode\" : \"T\"")); + Assertions.assertTrue(result.json.contains("customsInfoCountryCode\" : \"DE\"")); + Assertions.assertTrue(result.json.contains("customsInfoSubjectCode\" : \"CNE\"")); + Assertions.assertTrue(result.json.contains("customsInformation\" : \"DE3056777000\"")); + + Assertions.assertTrue(result.json.contains("SLAC\" : 200")); + Assertions.assertTrue(result.json.contains("PieceCount\" : 10")); + + // Some more + Assertions.assertTrue(result.json.contains("#hsCode\" : \"1133557799\"")); + } + + private XFZB3toOneRecordConverterTest.Result fileProcessingTest(String filename) throws JAXBException, JsonProcessingException { + XFZB3toOneRecordConverterTest.Result result = new Result(); + + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + // mapper.disable(SerializationFeature.WRAP_ROOT_VALUE); + mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")); + + InputStream is = ClassLoader.getSystemResourceAsStream(filename); + HouseWaybillType xfzb = new ConverterUtil().unmarshallXFZB3(is); + result.awb = xfzb.getBusinessHeaderDocument().getID().getValue(); + result.converter = new XFZB3toOneRecordConverter(xfzb); + result.json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result.converter.getOneRecordResult()); + Assertions.assertNotNull(result.json); + return result; + } + + class Result { + XFZB3toOneRecordConverter converter; + String awb; + String json; + } + +} diff --git a/src/test/resources/SEL22222222_XFZB.xml b/src/test/resources/SEL22222222_XFZB.xml new file mode 100644 index 0000000..777c997 --- /dev/null +++ b/src/test/resources/SEL22222222_XFZB.xml @@ -0,0 +1,135 @@ + + + + SEL22222222_888-11111111_285472ed-0059-4004-9590-8a1fbab07421 + House Waybill + 703 + 2022-04-29T10:10:00+02:00 + Creation + 3.00 + + RIEGESOFTWARE + + + REUAGT88FORWARDER/FRA01 + + + CCSPROVIDERNAME + + + REUAIR08$CODE + + + + SEL22222222 + + + SIEGFRIED SIGNATURE + + + 2022-04-29T00:00:00 + ARMIN AIRLINE + + ICN + + + + + 2217.0 + 59 + + 888-11111111 + + + ICN + + + FRA + + + SEL22222222 + true + true + true + true + true + 420 + 0 + 134.0 + 200 + 10 + SPARE PARTS F345732-D CIP TERM VOL.WGHT. 133.7 KG .088 CBM + + SPARE PART LTD. + + 12345 + 42. GONGDAN SEONGSAN + CHANGWON-SI. KYUN + KR + + + + 055281234567890 + + + + + REPAIR DIENST GMBH + + 87654 + SPEZIAL-STR. 13 + FRIEDRICHSHAFEN + DE + + + + + + + + ICN + + + FDH + + + T + 5432109876 + SHP + KR + + + CP + KIM QUAN KWUN + SHP + KR + + + CT + 0553456789012 + SHP + KR + + + T + DE3056777000 + CNE + DE + + + 1 + 1133557799 + 133.7 + 100 + 5 + NDA + + SPARE PARTS + + + 50.0 + + + + +