/*
 * Copyright 2015-2018 OpenEstate.org.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openestate.io.examples;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Calendar;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.io.output.NullWriter;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.PropertyConfigurator;
import org.openestate.io.daft_ie.DaftIeDocument;
import org.openestate.io.daft_ie.DaftIeUtils;
import org.openestate.io.daft_ie.xml.CommercialType;
import org.openestate.io.daft_ie.xml.Daft;
import org.openestate.io.daft_ie.xml.HouseType;
import org.openestate.io.daft_ie.xml.ObjectFactory;
import org.openestate.io.daft_ie.xml.OverseasRentalAdType;
import org.openestate.io.daft_ie.xml.OverseasSaleAdType;
import org.openestate.io.daft_ie.xml.PropertyType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Example for writing XML files for daft.ie.
 *

 * This example illustrates the programmatic creation of documents for
 * daft.ie and how they are written into XML.
 *
 * @author Andreas Rudolph
 * @since 1.0
 */
@SuppressWarnings("WeakerAccess")
public class DaftIeWritingExample {
    @SuppressWarnings("unused")
    private final static Logger LOGGER = LoggerFactory.getLogger(DaftIeWritingExample.class);
    private final static String PACKAGE = "/org/openestate/io/examples";
    private final static ObjectFactory FACTORY = DaftIeUtils.getFactory();
    private final static boolean PRETTY_PRINT = true;

    /**
     * Start the example application.
     *
     * @param args command line arguments
     */
    @SuppressWarnings("Duplicates")
    public static void main(String[] args) {
        // init logging
        PropertyConfigurator.configure(
                DaftIeWritingExample.class.getResource(PACKAGE + "/log4j.properties"));

        // create a Daft object with some example data
        // this object corresponds to the <daft> root element in XML
        Daft daft = FACTORY.createDaft();

        // append some example objects for rent to the Daft object
        daft.setOverseasRental(FACTORY.createDaftOverseasRental());
        daft.getOverseasRental().getOverseasRentalAd().add(createAdForRent());
        daft.getOverseasRental().getOverseasRentalAd().add(createAdForRent());

        // append some example objects for sale to the Daft object
        daft.setOverseasSales(FACTORY.createDaftOverseasSales());
        daft.getOverseasSales().getOverseasSaleAd().add(createAdForSale());
        daft.getOverseasSales().getOverseasSaleAd().add(createAdForSale());
        daft.getOverseasSales().getOverseasSaleAd().add(createAdForSale());

        // convert the Daft object into a XML document
        DaftIeDocument doc = null;
        try {
            doc = DaftIeDocument.newDocument(daft);
        } catch (Exception ex) {
            LOGGER.error("Can't create XML document!");
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            System.exit(1);
        }

        // write XML document into a java.io.File
        try {
            write(doc, File.createTempFile("output-", ".xml"));
        } catch (IOException ex) {
            LOGGER.error("Can't create temporary file!");
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            System.exit(1);
        }

        // write XML document into a java.io.OutputStream
        write(doc, new NullOutputStream());

        // write XML document into a java.io.Writer
        write(doc, new NullWriter());

        // write XML document into a string and send it to the console
        writeToConsole(doc);
    }

    /**
     * Create an {@link OverseasRentalAdType} with some example data.
     *
     * @return created example object
     */
    @SuppressWarnings("CatchMayIgnoreException")
    protected static OverseasRentalAdType createAdForRent() {
        // create an example real estate for rent
        OverseasRentalAdType ad = FACTORY.createOverseasRentalAdType();
        ad.setAddress("Beispielstraße 123");
        ad.setAgentId("123");
        ad.setArea("Berlin");
        ad.setAvailableFrom(Calendar.getInstance());
        ad.setBathroomNumber(BigInteger.valueOf(RandomUtils.nextInt(0, 5)));
        ad.setBedroomNumber(BigInteger.valueOf(RandomUtils.nextInt(0, 5)));
        ad.setCableTelevision(RandomUtils.nextInt(0, 2) == 1);
        ad.setCcEmail("test@openestate.org");
        ad.setCommercialType(CommercialType.LAND);
        ad.setContactName("Max Mustermann");
        ad.setCountry("DE");
        ad.setDescription("A description about the property.");
        ad.setDishwasher(RandomUtils.nextInt(0, 2) == 1);
        ad.setDoubleBeds(BigInteger.valueOf(RandomUtils.nextInt(0, 5)));
        ad.setDryer(RandomUtils.nextInt(0, 2) == 1);
        ad.setExternalId(String.valueOf(RandomUtils.nextInt(1, 1000)));
        ad.setFurnished(OverseasRentalAdType.Furnished.FURNISHED);
        ad.setHouseType(HouseType.TOWNHOUSE);
        ad.setLease(BigInteger.valueOf(RandomUtils.nextInt(100, 1000)));
        ad.setMainEmail("test@openstate.org");
        ad.setMicrowave(RandomUtils.nextInt(0, 2) == 1);
        ad.setNumberPeople(BigInteger.valueOf(RandomUtils.nextInt(0, 5)));
        ad.setPhone1("030/123456");
        ad.setPhone2("030/123457");
        ad.setPhoneInfo("Some information about contacts via phone.");
        ad.setPropertyType(PropertyType.HOUSE);
        ad.setRegion("Berlin");
        ad.setRent(BigInteger.valueOf(RandomUtils.nextInt(100, 1000)));
        ad.setRentCollectionPeriod(OverseasRentalAdType.RentPeriod.MONTHLY);
        ad.setSingleBeds(BigInteger.valueOf(RandomUtils.nextInt(0, 5)));
        ad.setTwinBeds(BigInteger.valueOf(RandomUtils.nextInt(0, 5)));
        ad.setWashingMachine(RandomUtils.nextInt(0, 2) == 1);

        // add some features
        ad.setFeatures(FACTORY.createFeaturesType());
        ad.getFeatures().getFeature().add("another feature");
        ad.getFeatures().getFeature().add("some more feature");

        // add some photos
        ad.setPhotos(FACTORY.createPhotosType());
        try {
            ad.getPhotos().getPhoto().add(new URI("http://www.mywebsite.org/image1.jpg"));
            ad.getPhotos().getPhoto().add(new URI("http://www.mywebsite.org/image2.jpg"));
            ad.getPhotos().getPhoto().add(new URI("http://www.mywebsite.org/image3.jpg"));
        } catch (URISyntaxException ex) {
        }

        return ad;
    }

    /**
     * Create an {@link OverseasRentalAdType} with some example data.
     *
     * @return created example object
     */
    @SuppressWarnings("CatchMayIgnoreException")
    protected static OverseasSaleAdType createAdForSale() {
        // create an example real estate for sale
        OverseasSaleAdType ad = FACTORY.createOverseasSaleAdType();
        ad.setAcres(BigDecimal.valueOf(RandomUtils.nextDouble(10, 1000)));
        ad.setAddress("Beispielstraße 123");
        ad.setAgentId("123");
        ad.setArea("Berlin");
        ad.setBathroomNumber(BigInteger.valueOf(RandomUtils.nextInt(0, 5)));
        ad.setBedroomNumber(BigInteger.valueOf(RandomUtils.nextInt(0, 5)));
        ad.setCcEmail("test@openestate.org");
        ad.setCo2Rating("some notes about CO2 rating");
        ad.setCommercialType(CommercialType.SHOP);
        ad.setContactName("Max Mustermann");
        ad.setCountry("DE");
        ad.setDescription("A description about the property.");
        ad.setDirections("some notes about directions");
        ad.setEnergyRating("some notes about energy rating");
        ad.setExternalId(String.valueOf(RandomUtils.nextInt(1, 1000)));
        ad.setHouseType(HouseType.DETACHED);
        ad.setIsNewDevelopment(RandomUtils.nextInt(0, 2) == 1);
        ad.setMainEmail("test@openstate.org");
        ad.setNewDevelopmentAvailability("some notes about development");
        ad.setPhone1("030/123456");
        ad.setPhone2("030/123457");
        ad.setPhoneInfo("Some information about contacts via phone.");
        ad.setPrice(BigInteger.valueOf(RandomUtils.nextInt(100, 1000000)));
        ad.setPriceType(OverseasSaleAdType.PriceType.REGION);
        ad.setPropertyStatus(OverseasSaleAdType.PropertyStatus.FOR_SALE);
        ad.setPropertyType(PropertyType.HOUSE);
        ad.setRegion("Berlin");
        ad.setSquareMetres(BigDecimal.valueOf(RandomUtils.nextDouble(10, 1000)));
        ad.setUnitsAvailable(BigInteger.valueOf(RandomUtils.nextInt(1, 50)));
        ad.setViewingDetails("some notes about viewing details");

        // add some features
        ad.setFeatures(FACTORY.createFeaturesType());
        ad.getFeatures().getFeature().add("another feature");
        ad.getFeatures().getFeature().add("some more feature");

        // add some pdf documents
        ad.setPdfs(FACTORY.createPdfsType());
        try {
            ad.getPdfs().getPdf().add(new URI("http://www.mywebsite.org/document1.pdf"));
            ad.getPdfs().getPdf().add(new URI("http://www.mywebsite.org/document2.pdf"));
        } catch (URISyntaxException ex) {
        }

        // add some photos
        ad.setPhotos(FACTORY.createPhotosType());
        try {
            ad.getPhotos().getPhoto().add(new URI("http://www.mywebsite.org/image1.jpg"));
            ad.getPhotos().getPhoto().add(new URI("http://www.mywebsite.org/image2.jpg"));
            ad.getPhotos().getPhoto().add(new URI("http://www.mywebsite.org/image3.jpg"));
        } catch (URISyntaxException ex) {
        }

        return ad;
    }

    /**
     * Write a {@link DaftIeDocument} into a {@link File}.
     *
     * @param doc  the document to write
     * @param file the file, where the document is written to
     */
    @SuppressWarnings("Duplicates")
    protected static void write(DaftIeDocument doc, File file) {
        LOGGER.info("writing document with version " + doc.getDocumentVersion());
        try {
            doc.toXml(file, PRETTY_PRINT);
            LOGGER.info("> written to: " + file.getAbsolutePath());
        } catch (Exception ex) {
            LOGGER.error("Can't write document into a file!");
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            System.exit(1);
        }
    }

    /**
     * Write a {@link DaftIeDocument} into an {@link OutputStream}.
     *
     * @param doc    the document to write
     * @param output the stream, where the document is written to
     */
    @SuppressWarnings("Duplicates")
    protected static void write(DaftIeDocument doc, OutputStream output) {
        LOGGER.info("writing document with version " + doc.getDocumentVersion());
        try {
            doc.toXml(output, PRETTY_PRINT);
            LOGGER.info("> written to a java.io.OutputStream");
        } catch (Exception ex) {
            LOGGER.error("Can't write document into an OutputStream!");
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            System.exit(1);
        }
    }

    /**
     * Write a {@link DaftIeDocument} into a {@link Writer}.
     *
     * @param doc    the document to write
     * @param output the writer, where the document is written to
     */
    @SuppressWarnings("Duplicates")
    protected static void write(DaftIeDocument doc, Writer output) {
        LOGGER.info("writing document with version " + doc.getDocumentVersion());
        try {
            doc.toXml(output, PRETTY_PRINT);
            LOGGER.info("> written to a java.io.Writer");
        } catch (Exception ex) {
            LOGGER.error("Can't write document into an OutputStream!");
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            System.exit(1);
        }
    }

    /**
     * Write a {@link DaftIeDocument} into a {@link String} and print the
     * results to the console.
     *
     * @param doc the document to write
     */
    @SuppressWarnings("Duplicates")
    protected static void writeToConsole(DaftIeDocument doc) {
        LOGGER.info("writing document with version " + doc.getDocumentVersion());
        try {
            String xml = doc.toXmlString(PRETTY_PRINT);
            LOGGER.info(StringUtils.repeat("-", 50)
                    + System.lineSeparator() + xml);
        } catch (Exception ex) {
            LOGGER.error("Can't write document into a string!");
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            System.exit(1);
        }
    }
}


/*
 * Copyright 2015-2018 OpenEstate.org.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openestate.io.trovit;

import javax.xml.bind.JAXBException;
import javax.xml.parsers.ParserConfigurationException;
import org.openestate.io.core.XmlDocument;
import org.openestate.io.core.XmlUtils;
import org.openestate.io.trovit.xml.Trovit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * Trovit-XML document with a <trovit> root element.
 *
 * @author Andreas Rudolph
 * @since 1.0
 */
@SuppressWarnings("WeakerAccess")
public class TrovitDocument extends XmlDocument<Trovit> {
    @SuppressWarnings("unused")
    private final static Logger LOGGER = LoggerFactory.getLogger(TrovitDocument.class);

    /**
     * Create from a {@link Document}.
     *
     * @param document the document to create from
     */
    public TrovitDocument(Document document) {
        super(document);
        if (!isReadable(document))
            throw new IllegalArgumentException("The provided document is invalid!");
        this.setTextWrittenAsCDATA(true);
    }

    /**
     * Checks, if a {@link Document} is readable as a {@link TrovitDocument}.
     *
     * @param doc document to check
     * @return true, if the document is usable, otherwise false
     */
    public static boolean isReadable(Document doc) {
        Element root = XmlUtils.getRootElement(doc);
        return "trovit".equals(root.getLocalName());
    }

    /**
     * Creates an empty {@link TrovitDocument}.
     *
     * @return created document
     * @throws ParserConfigurationException if the parser is not properly configured
     * @throws JAXBException                if a problem with JAXB occurred
     */
    public static TrovitDocument newDocument() throws ParserConfigurationException, JAXBException {
        return newDocument(TrovitUtils.getFactory().createTrovit());
    }

    /**
     * Creates a {@link TrovitDocument} from a {@link Trovit} object.
     *
     * @param trovit Java object, that represents the <trovit> root element
     * @return created document
     * @throws ParserConfigurationException if the parser is not properly configured
     * @throws JAXBException                if a problem with JAXB occurred
     */
    public static TrovitDocument newDocument(Trovit trovit) throws ParserConfigurationException, JAXBException {
        Document document = XmlUtils.newDocument();
        TrovitUtils.createMarshaller("UTF-8", true).marshal(trovit, document);
        return new TrovitDocument(document);
    }

    /**
     * Creates a {@link Trovit} object from the contained {@link Document}.
     *
     * @return created object, that represents the <trovit> root element
     * @throws JAXBException if a problem with JAXB occurred
     */
    @Override
    public Trovit toObject() throws JAXBException {
        return (Trovit) TrovitUtils.createUnmarshaller().unmarshal(this.getDocument());
    }
}


/*
 * Copyright 2015-2018 OpenEstate.org.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openestate.io.kyero.converters;

import java.util.List;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.jaxen.JaxenException;
import org.openestate.io.core.XmlConverter;
import org.openestate.io.core.XmlUtils;
import org.openestate.io.kyero.KyeroDocument;
import org.openestate.io.kyero.KyeroUtils;
import org.openestate.io.kyero.KyeroVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * Converter for version 3.
 *
 * @author Andreas Rudolph
 * @since 1.0
 */
@SuppressWarnings("WeakerAccess")
public class Kyero_3 extends XmlConverter<KyeroDocument, KyeroVersion> {
    @SuppressWarnings("unused")
    private final static Logger LOGGER = LoggerFactory.getLogger(Kyero_3.class);

    @Override
    public KyeroVersion getVersion() {
        return KyeroVersion.V3;
    }

    /**
     * Downgrade a Kyero document from version 3.
     *
     * @param doc Kyero document in version 3
     */
    @Override
    public void downgradeToPreviousVersion(KyeroDocument doc) {
        doc.setDocumentVersion(KyeroVersion.V2_1);

        try {
            this.downgradeNewBuildElements(doc.getDocument());
        } catch (Exception ex) {
            LOGGER.error("Can't downgrade <new_build> elements!");
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
        }

        try {
            this.downgradeTypeElements(doc.getDocument());
        } catch (Exception ex) {
            LOGGER.error("Can't downgrade <type> elements!");
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
        }

        try {
            this.downgradeUrlElements(doc.getDocument());
        } catch (Exception ex) {
            LOGGER.error("Can't downgrade <url> elements!");
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
        }

        try {
            this.removeLocationElements(doc.getDocument());
        } catch (Exception ex) {
            LOGGER.error("Can't remove <location> elements!");
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
        }

        try {
            this.removeEnergyRatingElements(doc.getDocument());
        } catch (Exception ex) {
            LOGGER.error("Can't remove <energy_rating> elements!");
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
        }

        try {
            this.removeNotesElements(doc.getDocument());
        } catch (Exception ex) {
            LOGGER.error("Can't remove <notes> elements!");
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
        }

        try {
            this.removeUnsupportedLanguageElements(doc.getDocument());
        } catch (Exception ex) {
            LOGGER.error("Can't remove unsupported translation elements!");
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
        }
    }

    /**
     * Upgrade a Kyero document to version 3.
     *
     * @param doc Kyero document in version 2.1
     */
    @Override
    public void upgradeFromPreviousVersion(KyeroDocument doc) {
        doc.setDocumentVersion(KyeroVersion.V3);

        try {
            this.removeCustomElements(doc.getDocument());
        } catch (Exception ex) {
            LOGGER.error("Can't remove <custom> elements!");
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
        }

        try {
            this.upgradeNewBuildElements(doc.getDocument());
        } catch (Exception ex) {
            LOGGER.error("Can't upgrade <new_build> elements!");
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
        }

        try {
            this.upgradeTypeElements(doc.getDocument());
        } catch (Exception ex) {
            LOGGER.error("Can't upgrade <type> elements!");
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
        }

        try {
            this.upgradeCurrencyElements(doc.getDocument());
        } catch (Exception ex) {
            LOGGER.error("Can't upgrade <currency> elements!");
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
        }

        try {
            this.upgradeUrlElements(doc.getDocument());
        } catch (Exception ex) {
            LOGGER.error("Can't upgrade <url> elements!");
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
        }
    }

    /**
     * Downgrade <new_build> elements to Kyero 2.1.
     *

     * The <new_build> elements are not available in version 2.1. Instead
     * the value "new_build" is used in the <price_freq> element.
     *

     * Any <new_build> elements are removed. If its value is set to "1",
     * then <price_freq>sale</price_freq> is converted to
     * <price_freq>new_build</price_freq>,
     *
     * @param doc Kyero document in version 3
     * @throws JaxenException if xpath evaluation failed
     */
    protected void downgradeNewBuildElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:root/io:property/io:new_build",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            Element parentNode = (Element) node.getParentNode();
            String value = StringUtils.trimToNull(node.getTextContent());
            if ("1".equals(value)) {
                Element priceFreqNode = (Element) XmlUtils.newXPath("io:price_freq", doc)
                        .selectSingleNode(parentNode);
                if (priceFreqNode == null) {
                    priceFreqNode = doc.createElementNS(KyeroUtils.NAMESPACE, "price_freq");
                    priceFreqNode.setTextContent("new_build");
                    parentNode.appendChild(priceFreqNode);
                } else if ("sale".equalsIgnoreCase(priceFreqNode.getTextContent())) {
                    priceFreqNode.setTextContent("new_build");
                }
            }
            parentNode.removeChild(node);
        }
    }

    /**
     * Downgrade <type> elements to Kyero 2.1.
     *

     * The <type> elements require a <en> child element in version
     * 2.1.
     *

     * An <en> child element is created for any <type> element.
     *
     * @param doc Kyero document in version 3
     * @throws JaxenException if xpath evaluation failed
     */
    protected void downgradeTypeElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:root/io:property/io:type",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            String value = StringUtils.trimToNull(node.getTextContent());
            node.setTextContent(null);

            Element childNode = doc.createElementNS(KyeroUtils.NAMESPACE, "en");
            childNode.setTextContent(value);
            node.appendChild(childNode);
        }
    }

    /**
     * Downgrade <url> elements to Kyero 2.1.
     *

     * The <url> elements only support a simple text value in version
     * 2.1. Version 3 allows different URL's for different languages.
     *

     * Any children of <url> elements are removed. The english URL or the
     * first found URL is copied as simple value into the <url> element.
     *
     * @param doc Kyero document in version 3
     * @throws JaxenException if xpath evaluation failed
     */
    @SuppressWarnings("Duplicates")
    protected void downgradeUrlElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:root/io:property/io:url",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;

            String enUrlValue = null;
            String fallbackUrlValue = null;
            List childNodes = XmlUtils.newXPath(
                    "*", doc).selectNodes(node);
            for (Object childItem : childNodes) {
                Element langNode = (Element) childItem;
                if ("en".equalsIgnoreCase(langNode.getLocalName()))
                    enUrlValue = StringUtils.trimToNull(langNode.getTextContent());
                else if (fallbackUrlValue == null)
                    fallbackUrlValue = StringUtils.trimToNull(langNode.getTextContent());
                node.removeChild(langNode);
            }

            node.setTextContent((enUrlValue != null) ? enUrlValue : fallbackUrlValue);
        }
    }

    /**
     * Remove <custom> elements.
     *

     * Kyero 3 does not support <custom> elements in <property> and
     * <agent>.
     *

     * Any occurrence of these elements is removed.
     *
     * @param doc OpenImmo document in version 2.1
     * @throws JaxenException if xpath evaluation failed
     */
    protected void removeCustomElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:root/io:property/io:custom  | " +
                        "/io:root/io:agent/io:custom",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            Element parentNode = (Element) node.getParentNode();
            parentNode.removeChild(node);
        }
    }

    /**
     * Remove <energy_rating> elements.
     *

     * Kyero 2.1 does not support <energy_rating> elements.
     *
     * @param doc OpenImmo document in version 3
     * @throws JaxenException if xpath evaluation failed
     */
    protected void removeEnergyRatingElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:root/io:property/io:energy_rating",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            Element parentNode = (Element) node.getParentNode();
            parentNode.removeChild(node);
        }
    }

    /**
     * Remove <location> elements.
     *

     * Kyero 2.1 does not support <location> elements.
     *
     * @param doc OpenImmo document in version 3
     * @throws JaxenException if xpath evaluation failed
     */
    protected void removeLocationElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:root/io:property/io:location",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            Element parentNode = (Element) node.getParentNode();
            parentNode.removeChild(node);
        }
    }

    /**
     * Remove <notes> elements.
     *

     * Kyero 2.1 does not support <notes> elements.
     *
     * @param doc OpenImmo document in version 3
     * @throws JaxenException if xpath evaluation failed
     */
    protected void removeNotesElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:root/io:property/io:notes",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            Element parentNode = (Element) node.getParentNode();
            parentNode.removeChild(node);
        }
    }

    /**
     * Remove elements with translations in unsupported languages.
     *

     * Kyero 2.1 does only support translation in <title> (for images),
     * <desc> (for properties) elements for "en", "es", "de", "nl", "fr".
     *
     * @param doc OpenImmo document in version 3
     * @throws JaxenException if xpath evaluation failed
     */
    protected void removeUnsupportedLanguageElements(Document doc) throws JaxenException {
        String[] unsupportedLanguages = new String[]{
                "ar", "bg", "ca", "cs", "da", "el", "et", "fa", "fi", "he", "hi", "hu",
                "id", "it", "ja", "ko", "lt", "lv", "no", "pl", "pt", "ro", "ru", "sk",
                "sl", "sv", "th", "tr", "uk", "vi", "zh",
        };
        List nodes = XmlUtils.newXPath(
                "/io:root/io:property/io:desc | " +
                        "/io:root/io:property/io:images/io:image/io:title",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            List childNodes = XmlUtils.newXPath(
                    "*", doc).selectNodes(node);
            for (Object childItem : childNodes) {
                Element langNode = (Element) childItem;
                String lang = langNode.getLocalName().toLowerCase();
                if (ArrayUtils.contains(unsupportedLanguages, lang)) {
                    node.removeChild(langNode);
                }
            }
        }
    }

    /**
     * Upgrade <currency> elements to Kyero 3.
     *

     * The <currency> only supports the values "EUR", "GBP", "USD" in
     * version 3.
     *

     * Any <currency> with an unsupported value is removed from the
     * document.
     *
     * @param doc Kyero document in version 2.1
     * @throws JaxenException if xpath evaluation failed
     */
    protected void upgradeCurrencyElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:root/io:property/io:currency",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            Element parentNode = (Element) node.getParentNode();
            String value = StringUtils.trimToNull(node.getTextContent());
            if ("EUR".equalsIgnoreCase(value))
                node.setTextContent("EUR");
            else if ("GBP".equalsIgnoreCase(value))
                node.setTextContent("GBP");
            else if ("USD".equalsIgnoreCase(value))
                node.setTextContent("USD");
            else
                parentNode.removeChild(node);
        }
    }

    /**
     * Upgrade <new_build> elements for Kyero 3.
     *

     * The <new_build> elements are not available in version 2.1. Instead
     * the value "new_build" is used in the <price_freq> element.
     *

     * Any occurrences of <price_freq>new_build</price_freq> is
     * replaced by <price_freq>sale</price_freq> and
     * <new_build>1</new_build> is added to the property.
     *
     * @param doc Kyero document in version 2.1
     * @throws JaxenException if xpath evaluation failed
     */
    protected void upgradeNewBuildElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:root/io:property/io:price_freq",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            Element parentNode = (Element) node.getParentNode();
            if (!"new_build".equalsIgnoreCase(node.getTextContent())) continue;
            node.setTextContent("sale");

            Element newBuildNode = doc.createElementNS(KyeroUtils.NAMESPACE, "new_build");
            newBuildNode.setTextContent("1");
            parentNode.appendChild(newBuildNode);
        }
    }

    /**
     * Upgrade <type> elements to Kyero 3.
     *

     * The <type> elements do not support any child element in version 3.
     *

     * Any child elements of <type> are removed. The text from the
     * <en> child element is copied into the <type> element.
     *
     * @param doc Kyero document in version 2.1
     * @throws JaxenException if xpath evaluation failed
     */
    @SuppressWarnings("Duplicates")
    protected void upgradeTypeElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:root/io:property/io:type",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;

            String enTypeValue = null;
            String fallbackTypeValue = null;
            List childNodes = XmlUtils.newXPath(
                    "*", doc).selectNodes(node);
            for (Object childItem : childNodes) {
                Element langNode = (Element) childItem;
                if ("en".equalsIgnoreCase(langNode.getLocalName()))
                    enTypeValue = StringUtils.trimToNull(langNode.getTextContent());
                else if (fallbackTypeValue == null)
                    fallbackTypeValue = StringUtils.trimToNull(langNode.getTextContent());
                node.removeChild(langNode);
            }
            node.setTextContent((enTypeValue != null) ? enTypeValue : fallbackTypeValue);
        }
    }

    /**
     * Upgrade <url> elements to Kyero 3.
     *

     * The <url> elements only support a simple text value in version
     * 2.1. Version 3 allows different URL's for different languages.
     *

     * The simple value of <url> elements is removed and copied into the
     * <en> child element.
     *
     * @param doc Kyero document in version 2.1
     * @throws JaxenException if xpath evaluation failed
     */
    protected void upgradeUrlElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:root/io:property/io:url",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            String value = StringUtils.trimToNull(node.getTextContent());
            node.setTextContent(null);
            if (value == null) {
                Element parentNode = (Element) node.getParentNode();
                parentNode.removeChild(node);
            } else {
                Element childNode = doc.createElementNS(KyeroUtils.NAMESPACE, "en");
                childNode.setTextContent(value);
                node.appendChild(childNode);
            }
        }
    }
}


/*
 * Copyright 2015-2018 OpenEstate.org.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openestate.io.wis_it;

import java.math.BigInteger;
import javax.xml.bind.JAXBException;
import javax.xml.parsers.ParserConfigurationException;
import org.openestate.io.core.XmlDocument;
import org.openestate.io.core.XmlUtils;
import org.openestate.io.wis_it.xml.WIS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * XML document from
 * wohnen-in-suedtirol.it with a
 * <WIS> root element.
 *
 * @author Andreas Rudolph
 * @since 1.0
 */
@SuppressWarnings("WeakerAccess")
public class WisItDocument extends XmlDocument<WIS> {
    @SuppressWarnings("unused")
    private final static Logger LOGGER = LoggerFactory.getLogger(WisItDocument.class);

    /**
     * Create from a {@link Document}.
     *
     * @param document the document to create from
     */
    public WisItDocument(Document document) {
        super(document);
        if (!isReadable(document))
            throw new IllegalArgumentException("The provided document is invalid!");
        setTextWrittenAsCDATA(true);
    }

    /**
     * Checks, if a {@link Document} is readable as a {@link WisItDocument}.
     *
     * @param doc document to check
     * @return true, if the document is usable, otherwise false
     */
    public static boolean isReadable(Document doc) {
        Element root = XmlUtils.getRootElement(doc);
        return "WIS".equals(root.getLocalName());
    }

    /**
     * Creates an empty {@link WisItDocument}.
     *
     * @return created document
     * @throws ParserConfigurationException if the parser is not properly configured
     * @throws JAXBException                if a problem with JAXB occurred
     */
    public static WisItDocument newDocument() throws ParserConfigurationException, JAXBException {
        return newDocument(WisItUtils.getFactory().createWIS());
    }

    /**
     * Creates a {@link WisItDocument} from a {@link WIS} object.
     *
     * @param wis Java object, that represents the <WIS> root element
     * @return created document
     * @throws ParserConfigurationException if the parser is not properly configured
     * @throws JAXBException                if a problem with JAXB occurred
     */
    public static WisItDocument newDocument(WIS wis) throws ParserConfigurationException, JAXBException {
        if (wis.getOBJEKTE() == null)
            wis.setOBJEKTE(WisItUtils.getFactory().createWISOBJEKTE());
        wis.getOBJEKTE().setANZAHL(BigInteger.valueOf(wis.getOBJEKTE().getOBJEKT().size()));

        Document document = XmlUtils.newDocument();
        WisItUtils.createMarshaller("UTF-8", true).marshal(wis, document);
        return new WisItDocument(document);
    }

    /**
     * Creates a {@link WIS} object from the contained {@link Document}.
     *
     * @return created object, that represents the <WIS> root element
     * @throws JAXBException if a problem with JAXB occurred
     */
    @Override
    public WIS toObject() throws JAXBException {
        return (WIS) WisItUtils.createUnmarshaller().unmarshal(this.getDocument());
    }
}


/*
 * Copyright 2015-2018 OpenEstate.org.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openestate.io.examples;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import org.apache.log4j.PropertyConfigurator;
import org.openestate.io.is24_csv.Is24CsvParser;
import org.openestate.io.is24_csv.Is24CsvRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Example for reading IS24-CSV files.
 * <p>
 * This example illustrates the programmatic creation of IS24-CSV records and how
 * they are written into CSV.
 *
 * @author Andreas Rudolph
 * @since 1.0
 */
public class Is24CsvReadingExample {
    @SuppressWarnings("unused")
    private final static Logger LOGGER = LoggerFactory.getLogger(Is24CsvReadingExample.class);
    private final static String PACKAGE = "/org/openestate/io/examples";

    /**
     * Start the example application.
     *
     * @param args command line arguments
     */
    @SuppressWarnings("Duplicates")
    public static void main(String[] args) {
        // init logging
        PropertyConfigurator.configure(
                Is24CsvReadingExample.class.getResource(PACKAGE + "/log4j.properties"));

        // read example file, if no files were specified as command line arguments
        if (args.length < 1) {
            try {
                read(Is24CsvReadingExample.class.getResourceAsStream(PACKAGE + "/is24.csv"));
            } catch (Exception ex) {
                LOGGER.error("Can't read example file!");
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                System.exit(2);
            }
        }

        // read files, that were specified as command line arguments
        else {
            for (String arg : args) {
                try {
                    read(new File(arg));
                } catch (Exception ex) {
                    LOGGER.error("Can't read file '" + arg + "'!");
                    LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                    System.exit(2);
                }
            }
        }
    }

    /**
     * Read a {@link File} into an {@link Is24CsvParser} and print some of its
     * content to console.
     *
     * @param csvFile the CSV file to read
     * @throws IOException if the file is not readable
     */
    protected static void read(File csvFile) throws IOException {
        LOGGER.info("process file: " + csvFile.getAbsolutePath());
        if (!csvFile.isFile()) {
            LOGGER.warn("> The provided file is invalid!");
            return;
        }
        try (Is24CsvParser parser = Is24CsvParser.create(csvFile)) {
            if (parser == null)
                LOGGER.warn("> Can't create parser!");
            else
                printToConsole(parser);
        }
    }

    /**
     * Read an {@link InputStream} into an {@link Is24CsvParser} and print some of
     * its content to console.
     *
     * @param csvInputStream the input stream to read
     * @throws IOException if the file is not readable
     */
    protected static void read(InputStream csvInputStream) throws IOException {
        LOGGER.info("process example file");
        try (Is24CsvParser parser = Is24CsvParser.create(csvInputStream)) {
            if (parser == null)
                LOGGER.warn("> Can't create parser!");
            else
                printToConsole(parser);
        }
    }

    /**
     * Print some content of an {@link Is24CsvParser} to console.
     *
     * @param parser parser instance
     */
    protected static void printToConsole(Is24CsvParser parser) {
        // process records
        while (parser.hasNext()) {
            Is24CsvRecord record = parser.next();

            // get object nr
            String objectNr = record.getAnbieterObjektId();
            if (objectNr == null) objectNr = "???";

            // get object title
            String objectTitle = record.getUeberschrift();
            if (objectTitle == null) objectTitle = "???";

            // print object information to console
            LOGGER.info("> found object '" + objectNr + "' "
                    + "with title '" + objectTitle + "'");
        }
    }
}


/*
 * Copyright 2015-2018 OpenEstate.org.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openestate.io.immobiliare_it;

import javax.xml.bind.JAXBException;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.lang3.StringUtils;
import org.jaxen.JaxenException;
import org.openestate.io.core.XmlConvertableDocument;
import org.openestate.io.core.XmlUtils;
import org.openestate.io.immobiliare_it.xml.Feed;
import org.openestate.io.immobiliare_it.xml.Version;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * XML document from immobiliare.it with a
 * <feed> root element.
 *
 * @author Andreas Rudolph
 * @since 1.0
 */
@SuppressWarnings("WeakerAccess")
public class ImmobiliareItDocument extends XmlConvertableDocument<Feed, ImmobiliareItVersion> {
    @SuppressWarnings("unused")
    private final static Logger LOGGER = LoggerFactory.getLogger(ImmobiliareItDocument.class);

    /**
     * Create from a {@link Document}.
     *
     * @param document the document to create from
     */
    public ImmobiliareItDocument(Document document) {
        super(document);
        if (!isReadable(document))
            throw new IllegalArgumentException("The provided document is invalid!");
    }

    @Override
    public ImmobiliareItVersion getDocumentVersion() {
        String version;
        try {
            Document doc = this.getDocument();
            version = StringUtils.trimToNull(XmlUtils
                    .newXPath("/io:feed/io:version/text()", doc)
                    .stringValueOf(doc));
            if (version == null) {
                LOGGER.warn("Can't find version information in the XML document!");
                //System.out.println( "----------------------------" );
                //try
                //{
                //  DocumentUtils.write( doc, System.out );
                //}
                //catch (Exception ex)
                //{
                //  LOGGER.error( "Can't write XML document!" );
                //  LOGGER.error( "> " + ex.getLocalizedMessage(), ex );
                //}
                //System.out.println( "----------------------------" );
                return null;
            }
        } catch (JaxenException ex) {
            LOGGER.error("Can't evaluate XPath expression!");
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            return null;
        }

        ImmobiliareItVersion v = ImmobiliareItVersion.detectFromString(version);
        if (v != null) return v;

        LOGGER.warn("The provided version (" + version + ") is not supported!");
        return null;
    }

    @Override
    public ImmobiliareItVersion getLatestVersion() {
        return ImmobiliareItUtils.VERSION;
    }

    /**
     * Checks, if a {@link Document} is readable as a
     * {@link ImmobiliareItDocument}.
     *
     * @param doc document to check
     * @return true, if the document is usable, otherwise false
     */
    public static boolean isReadable(Document doc) {
        Element root = XmlUtils.getRootElement(doc);
        return "feed".equals(root.getLocalName());
    }

    /**
     * Creates an empty {@link ImmobiliareItDocument}.
     *
     * @return created document
     * @throws ParserConfigurationException if the parser is not properly configured
     * @throws JAXBException                if a problem with JAXB occurred
     */
    public static ImmobiliareItDocument newDocument() throws ParserConfigurationException, JAXBException {
        return newDocument(ImmobiliareItUtils.getFactory().createFeed());
    }

    /**
     * Creates a {@link ImmobiliareItDocument} from a {@link Feed} object.
     *
     * @param feed Java object, that represents the <feed> root element
     * @return created document
     * @throws ParserConfigurationException if the parser is not properly configured
     * @throws JAXBException                if a problem with JAXB occurred
     */
    public static ImmobiliareItDocument newDocument(Feed feed) throws ParserConfigurationException, JAXBException {
        if (feed.getVersion() == null)
            feed.setVersion(Version.V2_5);

        Document document = XmlUtils.newDocument();
        ImmobiliareItUtils.createMarshaller("UTF-8", true).marshal(feed, document);
        return new ImmobiliareItDocument(document);
    }

    @Override
    public void setDocumentVersion(ImmobiliareItVersion version) {
        try {
            Document doc = this.getDocument();

            Element node = (Element) XmlUtils
                    .newXPath("/io:feed/io:version", doc)
                    .selectSingleNode(doc);
            if (node == null) {
                Element parentNode = (Element) XmlUtils
                        .newXPath("/io:feed", doc)
                        .selectSingleNode(doc);
                if (parentNode == null) {
                    LOGGER.warn("Can't find a <feed> element in the document!");
                    return;
                }
                node = doc.createElement("version");
                parentNode.insertBefore(node, parentNode.getFirstChild());
            }

            node.setTextContent(version.toReadableVersion());
        } catch (JaxenException ex) {
            LOGGER.error("Can't evaluate XPath expression!");
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
        }
    }

    /**
     * Creates a {@link Feed} object from the contained {@link Document}.
     *
     * @return created object, that represents the <feed> root element
     * @throws JAXBException if a problem with JAXB occurred
     */
    @Override
    public Feed toObject() throws JAXBException {
        this.upgradeToLatestVersion();
        return (Feed) ImmobiliareItUtils.createUnmarshaller().unmarshal(this.getDocument());
    }
}


/*
 * Copyright 2015-2018 OpenEstate.org.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openestate.io.openimmo.converters;

import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.jaxen.JaxenException;
import org.openestate.io.core.XmlConverter;
import org.openestate.io.core.XmlUtils;
import org.openestate.io.openimmo.OpenImmoDocument;
import org.openestate.io.openimmo.OpenImmoTransferDocument;
import org.openestate.io.openimmo.OpenImmoVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * Converter for version 1.2.2.
 *
 * @author Andreas Rudolph
 * @since 1.0
 */
@SuppressWarnings({"SpellCheckingInspection", "WeakerAccess"})
public class OpenImmo_1_2_2 extends XmlConverter<OpenImmoDocument, OpenImmoVersion> {
    @SuppressWarnings("unused")
    private final static Logger LOGGER = LoggerFactory.getLogger(OpenImmo_1_2_2.class);

    @Override
    public OpenImmoVersion getVersion() {
        return OpenImmoVersion.V1_2_2;
    }

    /**
     * Downgrade an OpenImmo document from version 1.2.2 to 1.2.1.
     *
     * @param doc OpenImmo document in version 1.2.2
     */
    @Override
    public void downgradeToPreviousVersion(OpenImmoDocument doc) {
        doc.setDocumentVersion(OpenImmoVersion.V1_2_1);

        if (doc instanceof OpenImmoTransferDocument) {
            try {
                this.downgradeUebertragungElements(doc.getDocument());
            } catch (Exception ex) {
                LOGGER.error("Can't downgrade <uebertragung> elements!");
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            }

            try {
                this.removeVersteigerungElements(doc.getDocument());
            } catch (Exception ex) {
                LOGGER.error("Can't remove <versteigerung> elements!");
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            }

            try {
                this.removeProvisionspflichtigElements(doc.getDocument());
            } catch (Exception ex) {
                LOGGER.error("Can't remove <provisionspflichtig> elements!");
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            }

            try {
                this.removeAusstattungChildElements(doc.getDocument());
            } catch (Exception ex) {
                LOGGER.error("Can't remove unsupported children of <ausstattung> elements!");
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            }

            try {
                this.downgradeObjektartElements(doc.getDocument());
            } catch (Exception ex) {
                LOGGER.error("Can't downgrade <objektart> elements!");
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            }

            try {
                this.downgradeEnergiepassElements(doc.getDocument());
            } catch (Exception ex) {
                LOGGER.error("Can't downgrade <energiepass> elements!");
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            }

            try {
                this.downgradeBodenElements(doc.getDocument());
            } catch (Exception ex) {
                LOGGER.error("Can't downgrade <boden> elements!");
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            }

            try {
                this.downgradeBefeuerungElements(doc.getDocument());
            } catch (Exception ex) {
                LOGGER.error("Can't downgrade <befeuerung> elements!");
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            }

            try {
                this.downgradeWohnungElements(doc.getDocument());
            } catch (Exception ex) {
                LOGGER.error("Can't downgrade <wohnung> elements!");
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            }

            try {
                this.downgradeHausElements(doc.getDocument());
            } catch (Exception ex) {
                LOGGER.error("Can't downgrade <haus> elements!");
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            }

            try {
                this.downgradeGrundstueckElements(doc.getDocument());
            } catch (Exception ex) {
                LOGGER.error("Can't downgrade <grundstueck> elements!");
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            }

            try {
                this.downgradeBueroPraxenElements(doc.getDocument());
            } catch (Exception ex) {
                LOGGER.error("Can't downgrade <buero_praxen> elements!");
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            }

            try {
                this.downgradeEinzelhandelElements(doc.getDocument());
            } catch (Exception ex) {
                LOGGER.error("Can't downgrade <einzelhandel> elements!");
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            }

            try {
                this.downgradeGastgewerbeElements(doc.getDocument());
            } catch (Exception ex) {
                LOGGER.error("Can't downgrade <gastgewerbe> elements!");
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            }

            try {
                this.downgradeHallenLagerProdElements(doc.getDocument());
            } catch (Exception ex) {
                LOGGER.error("Can't downgrade <hallen_lager_prod> elements!");
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            }

            try {
                this.downgradeLandUndForstwirtschaftElements(doc.getDocument());
            } catch (Exception ex) {
                LOGGER.error("Can't downgrade <land_und_forstwirtschaft> elements!");
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            }

            try {
                this.downgradeFreizeitimmobilieGewerblichElements(doc.getDocument());
            } catch (Exception ex) {
                LOGGER.error("Can't downgrade <freizeitimmobilie_gewerblich> elements!");
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            }
        }
    }

    /**
     * Upgrade an OpenImmo document from version 1.2.1 to 1.2.2.
     *
     * @param doc OpenImmo document in version 1.2.1
     */
    @Override
    @SuppressWarnings("Duplicates")
    public void upgradeFromPreviousVersion(OpenImmoDocument doc) {
        doc.setDocumentVersion(OpenImmoVersion.V1_2_2);

        if (doc instanceof OpenImmoTransferDocument) {
            try {
                this.upgradeEnergiepassElements(doc.getDocument());
            } catch (Exception ex) {
                LOGGER.error("Can't upgrade <energiepass> elements!");
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            }
        }
    }

    /**
     * Downgrade <befeuerung> elements to OpenImmo 1.2.1.
     *

     * The attribute "LUFTWP" for <befeuerung> elements is not available in
     * version 1.2.1.
     *

     * Any occurences of these values are removed.
     *
     * @param doc OpenImmo document in version 1.2.2
     * @throws JaxenException if xpath evaluation failed
     */
    protected void downgradeBefeuerungElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:openimmo/io:anbieter/io:immobilie/io:ausstattung/io:befeuerung[@LUFTWP]",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            node.removeAttribute("LUFTWP");
        }
    }

    /**
     * Downgrade <boden> elements to OpenImmo 1.2.1.
     *

     * The attributes "FERTIGPARKETT", "LAMINAT", "LINOLEUM" for <boden>
     * elements are not available in version 1.2.1.
     *

     * Any occurences of these values are removed.
     *
     * @param doc OpenImmo document in version 1.2.2
     * @throws JaxenException if xpath evaluation failed
     */
    protected void downgradeBodenElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:openimmo/io:anbieter/io:immobilie/io:ausstattung/io:boden[@FERTIGPARKETT or @LAMINAT or @LINOLEUM]",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            node.removeAttribute("FERTIGPARKETT");
            node.removeAttribute("LAMINAT");
            node.removeAttribute("LINOLEUM");
        }
    }

    /**
     * Downgrade <buero_praxen> elements to OpenImmo 1.2.1.
     *

     * The options "BUEROZENTRUM", "LOFT_ATELIER", "PRAXISFLAECHE", "PRAXISHAUS"
     * for the "buero_typ" attribute of <buero_praxen> elements are not
     * available in version 1.2.1.
     *

     * Any occurence of these values is replaced by the general "BUEROFLAECHE"
     * value.
     *
     * @param doc OpenImmo document in version 1.2.2
     * @throws JaxenException if xpath evaluation failed
     */
    protected void downgradeBueroPraxenElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:openimmo/io:anbieter/io:immobilie/io:objektkategorie/io:objektart/io:buero_praxen[@buero_typ]",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            String value = StringUtils.trimToNull(node.getAttribute("buero_typ"));
            if ("BUEROZENTRUM".equalsIgnoreCase(value))
                node.setAttribute("buero_typ", "BUEROFLAECHE");
            else if ("LOFT_ATELIER".equalsIgnoreCase(value))
                node.setAttribute("buero_typ", "BUEROFLAECHE");
            else if ("PRAXISFLAECHE".equalsIgnoreCase(value))
                node.setAttribute("buero_typ", "BUEROFLAECHE");
            else if ("PRAXISHAUS".equalsIgnoreCase(value))
                node.setAttribute("buero_typ", "BUEROFLAECHE");
        }
    }

    /**
     * Downgrade <einzelhandel> elements to OpenImmo 1.2.1.
     *

     * The options "KAUFHAUS", "FACTORY_OUTLET", "KIOSK", "VERKAUFSFLAECHE",
     * "AUSSTELLUNGSFLAECHE" for the "handel_typ" attribute of
     * <einzelhandel> elements are not available in version 1.2.1.
     *

     * Any occurence of these values is removed.
     *
     * @param doc OpenImmo document in version 1.2.2
     * @throws JaxenException if xpath evaluation failed
     */
    protected void downgradeEinzelhandelElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:openimmo/io:anbieter/io:immobilie/io:objektkategorie/io:objektart/io:einzelhandel[@handel_typ]",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            String value = StringUtils.trimToNull(node.getAttribute("handel_typ"));
            if ("KAUFHAUS".equalsIgnoreCase(value))
                node.removeAttribute("handel_typ");
            else if ("FACTORY_OUTLET".equalsIgnoreCase(value))
                node.removeAttribute("handel_typ");
            else if ("KIOSK".equalsIgnoreCase(value))
                node.removeAttribute("handel_typ");
            else if ("VERKAUFSFLAECHE".equalsIgnoreCase(value))
                node.removeAttribute("handel_typ");
            else if ("AUSSTELLUNGSFLAECHE".equalsIgnoreCase(value))
                node.removeAttribute("handel_typ");
        }
    }

    /**
     * Downgrade <energiepass> elements to OpenImmo 1.2.1.
     *

     * The <epart> child element of the <energiepass> element is
     * renamed to <art> in version 1.2.1.
     *
     * @param doc OpenImmo document in version 1.2.2
     * @throws JaxenException if xpath evaluation failed
     */
    protected void downgradeEnergiepassElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:openimmo/io:anbieter/io:immobilie/io:zustand_angaben/io:energiepass/io:epart",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            Element parentNode = (Element) node.getParentNode();

            Element newNode = doc.createElementNS(StringUtils.EMPTY, "art");
            newNode.setTextContent(node.getTextContent());

            parentNode.replaceChild(newNode, node);
        }
    }

    /**
     * Downgrade <freizeitimmobilie_gewerblich> elements to OpenImmo 1.2.1.
     *

     * The option "FREIZEITANLAGE" for the "freizeit_typ" attribute of
     * <freizeitimmobilie_gewerblich> elements is not available in version
     * 1.2.1.
     *

     * Any occurence of these values is removed.
     *
     * @param doc OpenImmo document in version 1.2.2
     * @throws JaxenException if xpath evaluation failed
     */
    protected void downgradeFreizeitimmobilieGewerblichElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:openimmo/io:anbieter/io:immobilie/io:objektkategorie/io:objektart/io:freizeitimmobilie_gewerblich[@freizeit_typ]",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            String value = StringUtils.trimToNull(node.getAttribute("freizeit_typ"));
            if ("FREIZEITANLAGE".equalsIgnoreCase(value))
                node.removeAttribute("freizeit_typ");
        }
    }

    /**
     * Downgrade <einzelhandel> elements to OpenImmo 1.2.1.
     *

     * The options "BAR", "CAFE", "DISCOTHEK", "RESTAURANT", "RAUCHERLOKAL",
     * "EINRAUMLOKAL" for the "gastgew_typ" attribute of
     * <gastgewerbe> elements are not available in version 1.2.1.
     *

     * Any occurence of these values is removed.
     *
     * @param doc OpenImmo document in version 1.2.2
     * @throws JaxenException if xpath evaluation failed
     */
    protected void downgradeGastgewerbeElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:openimmo/io:anbieter/io:immobilie/io:objektkategorie/io:objektart/io:gastgewerbe[@gastgew_typ]",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            String value = StringUtils.trimToNull(node.getAttribute("gastgew_typ"));
            if ("BAR".equalsIgnoreCase(value))
                node.removeAttribute("gastgew_typ");
            else if ("CAFE".equalsIgnoreCase(value))
                node.removeAttribute("gastgew_typ");
            else if ("DISCOTHEK".equalsIgnoreCase(value))
                node.removeAttribute("gastgew_typ");
            else if ("RESTAURANT".equalsIgnoreCase(value))
                node.removeAttribute("gastgew_typ");
            else if ("RAUCHERLOKAL".equalsIgnoreCase(value))
                node.removeAttribute("gastgew_typ");
            else if ("EINRAUMLOKAL".equalsIgnoreCase(value))
                node.removeAttribute("gastgew_typ");
        }
    }

    /**
     * Downgrade <grundstueck> elements to OpenImmo 1.2.1.
     *

     * The option "GEWERBEPARK" for the "grundst_typ" attribute of
     * <grundstueck> elements is not available in version 1.2.1.
     *

     * Any occurence of these values is removed.
     *
     * @param doc OpenImmo document in version 1.2.2
     * @throws JaxenException if xpath evaluation failed
     */
    protected void downgradeGrundstueckElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:openimmo/io:anbieter/io:immobilie/io:objektkategorie/io:objektart/io:grundstueck[@grundst_typ]",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            String value = StringUtils.trimToNull(node.getAttribute("grundst_typ"));
            if ("GEWERBEPARK".equalsIgnoreCase(value))
                node.removeAttribute("grundst_typ");
        }
    }

    /**
     * Downgrade <hallen_lager_prod> elements to OpenImmo 1.2.1.
     *

     * The options "INDUSTRIEHALLE", "LAGERFLAECHEN", "LAGER_MIT_FREIFLAECHE",
     * "SPEDITIONSLAGER", "KUEHLHAUS" for the "hallen_typ" attribute of
     * <hallen_lager_prod> elements are not available in version 1.2.1.
     *

     * Any occurence of "KUEHLHAUS" is removed. Other unsupported options are
     * replaced by "HALLE" or "LAGER".
     *
     * @param doc OpenImmo document in version 1.2.2
     * @throws JaxenException if xpath evaluation failed
     */
    protected void downgradeHallenLagerProdElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:openimmo/io:anbieter/io:immobilie/io:objektkategorie/io:objektart/io:hallen_lager_prod[@hallen_typ]",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            String value = StringUtils.trimToNull(node.getAttribute("hallen_typ"));
            if ("INDUSTRIEHALLE".equalsIgnoreCase(value))
                node.setAttribute("hallen_typ", "HALLE");
            else if ("LAGERFLAECHEN".equalsIgnoreCase(value))
                node.setAttribute("hallen_typ", "LAGER");
            else if ("LAGER_MIT_FREIFLAECHE".equalsIgnoreCase(value))
                node.setAttribute("hallen_typ", "LAGER");
            else if ("SPEDITIONSLAGER".equalsIgnoreCase(value))
                node.setAttribute("hallen_typ", "LAGER");
            else if ("KUEHLHAUS".equalsIgnoreCase(value))
                node.removeAttribute("hallen_typ");
        }
    }

    /**
     * Downgrade <haus> elements to OpenImmo 1.2.1.
     *

     * The option "KEINE_ANGABE" for the "haustyp" attribute of <haus>
     * elements are not available in version 1.2.1.
     *

     * Any occurence of these values is removed.
     *
     * @param doc OpenImmo document in version 1.2.2
     * @throws JaxenException if xpath evaluation failed
     */
    protected void downgradeHausElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:openimmo/io:anbieter/io:immobilie/io:objektkategorie/io:objektart/io:haus[@haustyp]",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            String value = StringUtils.trimToNull(node.getAttribute("haustyp"));
            if ("KEINE_ANGABE".equalsIgnoreCase(value))
                node.removeAttribute("haustyp");
        }
    }

    /**
     * Downgrade <land_und_forstwirtschaft> elements to OpenImmo 1.2.1.
     *

     * The option "ANWESEN" for the "land_typ" attribute of
     * <land_und_forstwirtschaft> elements is not available in version
     * 1.2.1.
     *

     * Any occurence of these values is replaced by the
     * "SONSTIGE_LANDWIRTSCHAFTSIMMOBILIEN" value.
     *
     * @param doc OpenImmo document in version 1.2.2
     * @throws JaxenException if xpath evaluation failed
     */
    protected void downgradeLandUndForstwirtschaftElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:openimmo/io:anbieter/io:immobilie/io:objektkategorie/io:objektart/io:land_und_forstwirtschaft[@land_typ]",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            String value = StringUtils.trimToNull(node.getAttribute("land_typ"));
            if ("ANWESEN".equalsIgnoreCase(value))
                node.setAttribute("land_typ", "SONSTIGE_LANDWIRTSCHAFTSIMMOBILIEN");
        }
    }

    /**
     * Downgrade <objektart> elements to OpenImmo 1.2.1.
     *

     * The <objektart> element does only allow the same type of child
     * element in version 1.2.1.
     *

     * Any child type, that differs from the first child type, is removed.
     *
     * @param doc OpenImmo document in version 1.2.2
     * @throws JaxenException if xpath evaluation failed
     */
    protected void downgradeObjektartElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:openimmo/io:anbieter/io:immobilie/io:objektkategorie/io:objektart",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element parentNode = (Element) item;
            String type = null;

            List childNodes = XmlUtils.newXPath(
                    "*", doc).selectNodes(parentNode);
            for (Object childItem : childNodes) {
                Element node = (Element) childItem;
                if (type == null) {
                    //LOGGER.debug( "PRIMARY TYPE: " + node.getLocalName() );
                    type = node.getLocalName();
                } else if (!type.equalsIgnoreCase(node.getLocalName())) {
                    //LOGGER.debug( "REMOVE SECONDARY TYPE: " + node.getLocalName() );
                    parentNode.removeChild(node);
                }
            }
        }
    }

    /**
     * Downgrade <uebertragung> elements to OpenImmo 1.2.1.
     *

     * The attribute "timestamp" for <uebertragung> elements is not
     * available in version 1.2.1.
     *

     * Any occurences of these values are removed.
     *
     * @param doc OpenImmo document in version 1.2.2
     * @throws JaxenException if xpath evaluation failed
     */
    protected void downgradeUebertragungElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:openimmo/io:uebertragung[@timestamp]",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            node.removeAttribute("timestamp");
        }
    }

    /**
     * Downgrade <wohnung> elements to OpenImmo 1.2.1.
     *

     * The option "KEINE_ANGABE" for the "wohnungtyp" attribute of <wohnung>
     * elements is not available in version 1.2.1.
     *

     * Any occurence of these values is removed.
     *
     * @param doc OpenImmo document in version 1.2.2
     * @throws JaxenException if xpath evaluation failed
     */
    protected void downgradeWohnungElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:openimmo/io:anbieter/io:immobilie/io:objektkategorie/io:objektart/io:wohnung[@wohnungtyp]",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            String value = StringUtils.trimToNull(node.getAttribute("wohnungtyp"));
            if ("KEINE_ANGABE".equalsIgnoreCase(value))
                node.removeAttribute("wohnungtyp");
        }
    }

    /**
     * Remove unsupported children from all <ausstattung> elements.
     *

     * OpenImmo 1.2.1 does not support the following children for
     * <ausstattung> elements: <ausstatt_kategorie>, <dachform>,
     * <bauweise>, <ausbaustufe>
     *

     * These elements are removed by this function.
     *
     * @param doc OpenImmo document in version 1.2.2
     * @throws JaxenException if xpath evaluation failed
     */
    protected void removeAusstattungChildElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:openimmo/io:anbieter/io:immobilie/io:ausstattung/io:ausstatt_kategorie | " +
                        "/io:openimmo/io:anbieter/io:immobilie/io:ausstattung/io:dachform | " +
                        "/io:openimmo/io:anbieter/io:immobilie/io:ausstattung/io:bauweise | " +
                        "/io:openimmo/io:anbieter/io:immobilie/io:ausstattung/io:ausbaustufe",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            Element parentNode = (Element) node.getParentNode();
            parentNode.removeChild(node);
        }
    }

    /**
     * Remove <provisionspflichtig> elements.
     *

     * OpenImmo 1.2.1 does not support <provisionspflichtig> elements.
     *
     * @param doc OpenImmo document in version 1.2.2
     * @throws JaxenException if xpath evaluation failed
     */
    protected void removeProvisionspflichtigElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:openimmo/io:anbieter/io:immobilie/io:preise/io:provisionspflichtig",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            Element parentNode = (Element) node.getParentNode();
            parentNode.removeChild(node);
        }
    }

    /**
     * Remove <versteigerung> elements.
     *

     * OpenImmo 1.2.1 does not support <versteigerung> elements.
     *
     * @param doc OpenImmo document in version 1.2.2
     * @throws JaxenException if xpath evaluation failed
     */
    protected void removeVersteigerungElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:openimmo/io:anbieter/io:immobilie/io:versteigerung",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            Element parentNode = (Element) node.getParentNode();
            parentNode.removeChild(node);
        }
    }

    /**
     * Upgrade <energiepass> elements to OpenImmo 1.2.2.
     *

     * The <art> child element of the <energiepass> element is
     * renamed to <epart> in version 1.2.2.
     *
     * @param doc OpenImmo document in version 1.2.1
     * @throws JaxenException if xpath evaluation failed
     */
    protected void upgradeEnergiepassElements(Document doc) throws JaxenException {
        List nodes = XmlUtils.newXPath(
                "/io:openimmo/io:anbieter/io:immobilie/io:zustand_angaben/io:energiepass/io:art",
                doc).selectNodes(doc);
        for (Object item : nodes) {
            Element node = (Element) item;
            Element parentNode = (Element) node.getParentNode();

            Element newNode = doc.createElementNS(StringUtils.EMPTY, "epart");
            newNode.setTextContent(node.getTextContent());

            parentNode.replaceChild(newNode, node);
        }
    }
}