/*
 * 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 java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Currency;
import java.util.Date;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.bind.DatatypeConverter;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.openestate.io.core.XmlUtils;
import org.openestate.io.core.XmlValidationHandler;
import org.openestate.io.trovit.xml.ObjectFactory;
import org.openestate.io.trovit.xml.types.AreaUnitValue;
import org.openestate.io.trovit.xml.types.ForeclosureTypeValue;
import org.openestate.io.trovit.xml.types.OrientationValue;
import org.openestate.io.trovit.xml.types.PricePeriodValue;
import org.openestate.io.trovit.xml.types.TypeValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

/**
 * Some helper functions for the Trovit-XML format.
 *
 * @author Andreas Rudolph
 * @since 1.0
 */
@SuppressWarnings("WeakerAccess")
public class TrovitUtils {
    @SuppressWarnings("unused")
    private final static Logger LOGGER = LoggerFactory.getLogger(TrovitUtils.class);
    private final static Pattern ROOMS_INTERVAL = Pattern.compile("^(\\d+)\\-(\\d+)$");
    private static JAXBContext JAXB = null;

    /*
     * the latest implemented version of this format
     *
     * public final static String VERSION = "1.0";
     */

    /**
     * the XML target namespace of this format
     */
    @SuppressWarnings("unused")
    public final static String NAMESPACE = StringUtils.EMPTY;

    /**
     * the package, where generated JAXB classes are located
     */
    @SuppressWarnings("unused")
    public final static String PACKAGE = "org.openestate.io.trovit.xml";

    /**
     * the factory for creation of JAXB objects
     */
    @SuppressWarnings("unused")
    public final static ObjectFactory FACTORY = new ObjectFactory();

    private TrovitUtils() {
    }

    /**
     * Creates a {@link TrovitDocument} from an {@link InputStream}.
     *
     * @param input XML input
     * @return created document or null, of the document is not supported by this format
     * @throws SAXException                 if XML is invalid
     * @throws IOException                  if reading failed
     * @throws ParserConfigurationException if the parser is not properly configured
     */
    public static TrovitDocument createDocument(InputStream input) throws SAXException, IOException, ParserConfigurationException {
        return createDocument(XmlUtils.newDocument(input, true));
    }

    /**
     * Creates a {@link TrovitDocument} from a {@link File}.
     *
     * @param xmlFile XML file
     * @return created document or null, of the document is not supported by this format
     * @throws SAXException                 if XML is invalid
     * @throws IOException                  if reading failed
     * @throws ParserConfigurationException if the parser is not properly configured
     */
    public static TrovitDocument createDocument(File xmlFile) throws SAXException, IOException, ParserConfigurationException {
        return createDocument(XmlUtils.newDocument(xmlFile, true));
    }

    /**
     * Creates a {@link TrovitDocument} from a {@link String}.
     *
     * @param xmlString XML string
     * @return created document or null, of the document is not supported by this format
     * @throws SAXException                 if XML is invalid
     * @throws IOException                  if reading failed
     * @throws ParserConfigurationException if the parser is not properly configured
     */
    public static TrovitDocument createDocument(String xmlString) throws SAXException, IOException, ParserConfigurationException {
        return createDocument(XmlUtils.newDocument(xmlString, true));
    }

    /**
     * Creates a {@link TrovitDocument} from a {@link Document}.
     *
     * @param doc XML document
     * @return created document or null, of the document is not supported by this format
     */
    public static TrovitDocument createDocument(Document doc) {
        if (TrovitDocument.isReadable(doc))
            return new TrovitDocument(doc);
        else
            return null;
    }

    /**
     * Creates a {@link Marshaller} to write JAXB objects into XML.
     *
     * @return created marshaller
     * @throws JAXBException if a problem with JAXB occurred
     */
    @SuppressWarnings("unused")
    public static Marshaller createMarshaller() throws JAXBException {
        return createMarshaller(Charset.defaultCharset().name(), true);
    }

    /**
     * Creates a {@link Marshaller} to write JAXB objects into XML.
     *
     * @param encoding  encoding of written XML
     * @param formatted if written XML is pretty printed
     * @return created marshaller
     * @throws JAXBException if a problem with JAXB occurred
     */
    @SuppressWarnings("Duplicates")
    public static Marshaller createMarshaller(String encoding, boolean formatted) throws JAXBException {
        Marshaller m = getContext().createMarshaller();
        m.setProperty(Marshaller.JAXB_ENCODING, encoding);
        m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, formatted);
        m.setEventHandler(new XmlValidationHandler());
        return m;
    }

    /**
     * Creates a {@link Unmarshaller} to read JAXB objects from XML.
     *
     * @return created unmarshaller
     * @throws JAXBException if a problem with JAXB occurred
     */
    public static Unmarshaller createUnmarshaller() throws JAXBException {
        Unmarshaller m = getContext().createUnmarshaller();
        m.setEventHandler(new XmlValidationHandler());
        return m;
    }

    /**
     * Returns the {@link JAXBContext} for this format.
     *
     * @return context
     * @throws JAXBException if a problem with JAXB occurred
     */
    public synchronized static JAXBContext getContext() throws JAXBException {
        if (JAXB == null) initContext(Thread.currentThread().getContextClassLoader());
        return JAXB;
    }

    /**
     * Returns the {@link ObjectFactory} for this format.
     *
     * @return object factory
     */
    public synchronized static ObjectFactory getFactory() {
        return FACTORY;
    }

    /**
     * Initializes the {@link JAXBContext} for this format.
     *
     * @param classloader the classloader to load the generated JAXB classes with
     * @throws JAXBException if a problem with JAXB occurred
     */
    public synchronized static void initContext(ClassLoader classloader) throws JAXBException {
        JAXB = JAXBContext.newInstance(PACKAGE, classloader);
    }

    /**
     * Read a {@link AreaUnitValue} value from XML.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static AreaUnitValue parseAreaUnitValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;

        final AreaUnitValue unit = AreaUnitValue.fromXmlValue(value);
        if (unit != null) return unit;

        throw new IllegalArgumentException("Can't parse foreclosure type value '" + value + "'!");
    }

    /**
     * Read a {@link Boolean} value from XML.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static Boolean parseBooleanValue(String value) {
        value = StringUtils.lowerCase(StringUtils.trimToEmpty(value), Locale.ENGLISH);
        switch (value) {
            case "true":
            case "yes":
            case "si":
            case "1":
                return Boolean.TRUE;

            case "false":
            case "no":
            case "0":
                return Boolean.FALSE;

            default:
                throw new IllegalArgumentException("Can't parse boolean value '" + value + "'!");
        }
    }

    /**
     * Read a {@link String} value from XML for description.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static String parseContentValue(String value) {
        return StringUtils.trimToNull(value);
    }

    /**
     * Read a {@link String} value from XML for a country code.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static String parseCountryValue(String value) {
        return StringUtils.trimToNull(value);
    }

    /**
     * Read a {@link Calendar} value from XML.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static Calendar parseDateValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;

        final String[] patterns = new String[]{
                "dd/MM/yyyy",
                "dd/MM/yyyy hh:mm:ss",
                "dd-MM-yyyy",
                "dd-MM-yyyy hh:mm:ss",
                "yyyy/MM/dd",
                "yyyy/MM/dd hh:mm:ss",
                "yyyy-MM-dd",
                "yyyy-MM-dd hh:mm:ss"
        };

        try {
            Date date = DateUtils.parseDateStrictly(value, Locale.ENGLISH, patterns);
            Calendar cal = Calendar.getInstance(Locale.getDefault());
            cal.setTime(date);
            return cal;
        } catch (ParseException ex) {
            throw new IllegalArgumentException("Can't parse date value '" + value + "'!", ex);
        }
    }

    /**
     * Read a {@link BigInteger} value from XML for a floor area.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static BigInteger parseFloorAreaValue(String value) {
        try {
            value = StringUtils.trimToNull(value);
            return (value != null) ? DatatypeConverter.parseInteger(value) : null;
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Can't parse floor area value '" + value + "'!", ex);
        }
    }

    /**
     * Read a {@link ForeclosureTypeValue} value from XML.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static ForeclosureTypeValue parseForeclosureTypeValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;

        final ForeclosureTypeValue foreclosure = ForeclosureTypeValue.fromXmlValue(value);
        if (foreclosure != null) return foreclosure;

        throw new IllegalArgumentException("Can't parse foreclosure type value '" + value + "'!");
    }

    /**
     * Read a {@link BigDecimal} value from XML
     * with a valid latitude range.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static BigDecimal parseLatitudeValue(String value) {
        try {
            value = StringUtils.trimToNull(value);
            return (value != null) ? DatatypeConverter.parseDecimal(value) : null;
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Can't parse latitude value '" + value + "'!", ex);
        }
    }

    /**
     * Read a {@link BigDecimal} value from XML
     * with a valid longitude range.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static BigDecimal parseLongitudeValue(String value) {
        try {
            value = StringUtils.trimToNull(value);
            return (value != null) ? DatatypeConverter.parseDecimal(value) : null;
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Can't parse longitude value '" + value + "'!", ex);
        }
    }

    /**
     * Read a {@link OrientationValue} value from XML.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static OrientationValue parseOrientationValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;

        final OrientationValue orientation = OrientationValue.fromXmlValue(value);
        if (orientation != null) return orientation;

        throw new IllegalArgumentException("Can't parse orientation value '" + value + "'!");
    }

    /**
     * Read a {@link BigInteger} value from XML for a plot area.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static BigInteger parsePlotAreaValue(String value) {
        try {
            value = StringUtils.trimToNull(value);
            return (value != null) ? DatatypeConverter.parseInteger(value) : null;
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Can't parse plot area value '" + value + "'!", ex);
        }
    }

    /**
     * Read a {@link Currency} value from XML.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static Currency parsePriceCurrencyValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;

        try {
            return Currency.getInstance(value.toUpperCase());
        } catch (IllegalArgumentException ex) {
            throw new IllegalArgumentException("Can't parse price currency value '" + value + "'!", ex);
        }
    }

    /**
     * Read a {@link PricePeriodValue} value from XML.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static PricePeriodValue parsePricePeriodValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;

        final PricePeriodValue period = PricePeriodValue.fromXmlValue(value);
        if (period != null) return period;

        throw new IllegalArgumentException("Can't parse price period value '" + value + "'!");
    }

    /**
     * Read a {@link BigDecimal} value from XML for a price.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static BigDecimal parsePriceValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;
        try {
            return DatatypeConverter.parseDecimal(value);
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Can't parse price value '" + value + "'!", ex);
        }
    }

    /**
     * Read a {@link BigDecimal} value from XML for a number of rooms.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static BigDecimal parseRoomsValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;

        final Matcher m = ROOMS_INTERVAL.matcher(value);
        if (m.find()) {
            final int from = Integer.parseInt(m.group(1));
            final int to = Integer.parseInt(m.group(2));
            if ((to - from) != -1) {
                throw new IllegalArgumentException("Can't parse rooms value '" + value + "' because of an invalid interval!");
            }
            return DatatypeConverter.parseDecimal(to + ".5");
        }

        try {
            return DatatypeConverter.parseDecimal(value);
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Can't parse rooms value '" + value + "'!", ex);
        }
    }

    /**
     * Read an {@link URI} value from XML.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static URI parseUriValue(String value) {
        value = StringUtils.trimToNull(value);

        if (value != null && !StringUtils.startsWithIgnoreCase(value, "http://") && !StringUtils.startsWithIgnoreCase(value, "https://"))
            value = "http://" + value;

        try {
            return (value != null) ? new URI(value) : null;
        } catch (URISyntaxException ex) {
            throw new IllegalArgumentException("Can't parse URI '" + value + "'!", ex);
        }
    }

    /**
     * Read a {@link TypeValue} value from XML.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static TypeValue parseTypeValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;

        final TypeValue type = TypeValue.fromXmlValue(value);
        if (type != null) return type;

        throw new IllegalArgumentException("Can't parse type value '" + value + "'!");
    }

    /**
     * Read a {@link BigInteger} value from XML for a year number.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static BigInteger parseYearValue(String value) {
        try {
            value = StringUtils.trimToNull(value);
            return (value != null) ? DatatypeConverter.parseInteger(value) : null;
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Can't parse year value '" + value + "'!", ex);
        }
    }

    /**
     * Write a {@link AreaUnitValue} value into XML output.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printAreaUnitValue(AreaUnitValue value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty area unit value!");

        return value.write();
    }

    /**
     * Write a {@link Boolean} value into XML output.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printBooleanValue(Boolean value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty boolean value!");

        return DatatypeConverter.printBoolean(value);
    }

    /**
     * Write a {@link String} value for a description into XML output.
     * <p>
     * The description must contain at least 30 characters.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printContentValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null)
            throw new IllegalArgumentException("Can't print empty content value!");
        if (value.length() < 30)
            throw new IllegalArgumentException("Can't print content value '" + value + "' because it is shorter than 30 characters!");

        return value;
    }

    /**
     * Write a {@link String} value for a country code into XML output.
     * <p>
     * The country has to be represendet by a ISO-Code wirh two or three
     * characters.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printCountryValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null)
            throw new IllegalArgumentException("Can't print empty country value!");
        if (value.length() != 2 && value.length() != 3)
            throw new IllegalArgumentException("Can't print country value '" + value + "' because it is neither an ISO-2-Code nor an ISO-3-Code!");

        return StringUtils.upperCase(value, Locale.ENGLISH);
    }

    /**
     * Write a {@link Calendar} value into XML output.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printDateValue(Calendar value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty date value!");

        return new SimpleDateFormat("dd-MM-yyyy hh:mm:ss", Locale.ENGLISH)
                .format(value.getTime());
    }

    /**
     * Write a {@link BigInteger} value into XML output for a floor area.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printFloorAreaValue(BigInteger value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty floor area value!");
        if (value.compareTo(BigInteger.valueOf(20L)) < 0)
            throw new IllegalArgumentException("Can't print floor area value '" + value + "' because it is below 20!");
        if (value.compareTo(BigInteger.valueOf(50000L)) > 0)
            throw new IllegalArgumentException("Can't print floor area value '" + value + "' because it is above 50000!");

        return DatatypeConverter.printInteger(value);
    }

    /**
     * Write a {@link ForeclosureTypeValue} value into XML output.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printForeclosureTypeValue(ForeclosureTypeValue value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty foreclosure type value!");

        return value.write();
    }

    /**
     * Write a {@link BigDecimal} value into XML output
     * with a valid latitude range.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printLatitudeValue(BigDecimal value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty latitude value!");
        if (value.compareTo(new BigDecimal("-90")) < 0)
            throw new IllegalArgumentException("Can't print latitude value '" + value + "' because it is below -90!");
        if (value.compareTo(new BigDecimal("90")) > 0)
            throw new IllegalArgumentException("Can't print latitude value '" + value + "' because it is above 90!");

        value = value.setScale(10, BigDecimal.ROUND_HALF_UP);
        return DatatypeConverter.printDecimal(value);
    }

    /**
     * Write a {@link BigDecimal} value into XML output
     * with a valid longitude range.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printLongitudeValue(BigDecimal value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty longitude value!");
        if (value.compareTo(new BigDecimal("-180")) < 0)
            throw new IllegalArgumentException("Can't print longitude value '" + value + "' because it is below -180!");
        if (value.compareTo(new BigDecimal("180")) > 0)
            throw new IllegalArgumentException("Can't print longitude value '" + value + "' because it is above 180!");

        value = value.setScale(10, BigDecimal.ROUND_HALF_UP);
        return DatatypeConverter.printDecimal(value);
    }

    /**
     * Write a {@link OrientationValue} value into XML output.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printOrientationValue(OrientationValue value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty orientation value!");

        return value.write();
    }

    /**
     * Write a {@link BigInteger} value into XML output for a plot area.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printPlotAreaValue(BigInteger value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty plot area value!");
        if (value.compareTo(BigInteger.ONE) < 0)
            throw new IllegalArgumentException("Can't print floor plot value '" + value + "' because it is below 1!");
        if (value.compareTo(BigInteger.valueOf(1000000000L)) > 0)
            throw new IllegalArgumentException("Can't print floor plot value '" + value + "' because it is above 1000000000!");

        return DatatypeConverter.printInteger(value);
    }

    /**
     * Write a {@link Currency} value into XML output.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printPriceCurrencyValue(Currency value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty price currency value!");

        return value.getCurrencyCode();
    }

    /**
     * Write a {@link PricePeriodValue} value into XML output.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printPricePeriodValue(PricePeriodValue value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty price period value!");

        return value.write();
    }

    /**
     * Write a {@link BigDecimal} value into XML output for a price.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printPriceValue(BigDecimal value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty price value!");
        if (value.compareTo(BigDecimal.ZERO) < 0)
            throw new IllegalArgumentException("Can't print price value '" + value + "' because it is below 0!");
        if (value.compareTo(new BigDecimal("1000000000")) > 0)
            throw new IllegalArgumentException("Can't print price value '" + value + "' because it is above 1000000000!");

        value = value.setScale(2, BigDecimal.ROUND_HALF_UP);
        return DatatypeConverter.printDecimal(value);
    }

    /**
     * Write a {@link BigDecimal} value into XML output for a room number.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printRoomsValue(BigDecimal value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty rooms value!");
        if (value.compareTo(BigDecimal.ZERO) < 0)
            throw new IllegalArgumentException("Can't print rooms value '" + value + "' because it is below 0!");
        if (value.compareTo(new BigDecimal("20")) > 0)
            throw new IllegalArgumentException("Can't print rooms value '" + value + "' because it is above 20!");

        value = value.setScale(1, BigDecimal.ROUND_HALF_UP);
        //return DatatypeConverter.printDecimal( value );

        final BigInteger integerPart = value.toBigInteger();
        final BigInteger decimalPart = value.subtract(new BigDecimal(integerPart, 1)).multiply(BigDecimal.TEN).toBigInteger();
        if (decimalPart.compareTo(BigInteger.ZERO) != 0)
            return integerPart.toString() + ".5";

        return DatatypeConverter.printInteger(integerPart);
    }

    /**
     * Write a {@link TypeValue} value into XML output.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printTypeValue(TypeValue value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty type value!");

        return value.write();
    }

    /**
     * Write an {@link URI} value into XML output.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printUriValue(URI value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty URI value!");
        if ("http".equalsIgnoreCase(value.getScheme()))
            return value.toString();
        if ("https".equalsIgnoreCase(value.getScheme()))
            return value.toString();

        throw new IllegalArgumentException("Can't print URI '" + value + "' because of an unsupported scheme!");
    }

    /**
     * Write a {@link BigInteger} value into XML output for a year number.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printYearValue(BigInteger value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty year value!");
        if (value.compareTo(BigInteger.valueOf(1700L)) < 0)
            throw new IllegalArgumentException("Can't print year value '" + value + "' because it is below 1700!");
        if (value.compareTo(BigInteger.valueOf(9999L)) > 0)
            throw new IllegalArgumentException("Can't print year value '" + value + "' because it is above 9999!");

        return DatatypeConverter.printInteger(value);
    }
}


package org.openestate.io.immoxml.xml;

import java.io.Serializable;
import javax.annotation.Generated;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlEnum;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import org.jvnet.jaxb2_commons.lang.CopyStrategy2;
import org.jvnet.jaxb2_commons.lang.CopyTo2;
import org.jvnet.jaxb2_commons.lang.Equals2;
import org.jvnet.jaxb2_commons.lang.EqualsStrategy2;
import org.jvnet.jaxb2_commons.lang.JAXBCopyStrategy;
import org.jvnet.jaxb2_commons.lang.JAXBEqualsStrategy;
import org.jvnet.jaxb2_commons.lang.JAXBToStringStrategy;
import org.jvnet.jaxb2_commons.lang.ToString2;
import org.jvnet.jaxb2_commons.lang.ToStringStrategy2;
import org.jvnet.jaxb2_commons.locator.ObjectLocator;
import org.jvnet.jaxb2_commons.locator.util.LocatorUtils;

/**
 * Java class for <zimmer> element.
 *
 *
 *
 */
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "")
@XmlRootElement(name = "zimmer")
@Generated(value = "com.sun.tools.xjc.Driver", date = "2018-10-12T02:50:55+02:00", comments = "JAXB RI v2.2.11")
public class Zimmer implements Serializable, Cloneable, CopyTo2, Equals2, ToString2
{

    @XmlAttribute(name = "zimmertyp")
    @Generated(value = "com.sun.tools.xjc.Driver", date = "2018-10-12T02:50:55+02:00", comments = "JAXB RI v2.2.11")
    protected Zimmer.Zimmertyp zimmertyp;

    /**
     * Gets the value of the zimmertyp property.
     *
     * @return
     *     possible object is
     *     {@link Zimmer.Zimmertyp }
     *
     */
    @Generated(value = "com.sun.tools.xjc.Driver", date = "2018-10-12T02:50:55+02:00", comments = "JAXB RI v2.2.11")
    public Zimmer.Zimmertyp getZimmertyp() {
        return zimmertyp;
    }

    /**
     * Sets the value of the zimmertyp property.
     *
     * @param value
     *     allowed object is
     *     {@link Zimmer.Zimmertyp }
     *
     */
    @Generated(value = "com.sun.tools.xjc.Driver", date = "2018-10-12T02:50:55+02:00", comments = "JAXB RI v2.2.11")
    public void setZimmertyp(Zimmer.Zimmertyp value) {
        this.zimmertyp = value;
    }

    @Generated(value = "com.sun.tools.xjc.Driver", date = "2018-10-12T02:50:55+02:00", comments = "JAXB RI v2.2.11")
    public String toString() {
        final ToStringStrategy2 strategy = JAXBToStringStrategy.INSTANCE;
        final StringBuilder buffer = new StringBuilder();
        append(null, buffer, strategy);
        return buffer.toString();
    }

    @Generated(value = "com.sun.tools.xjc.Driver", date = "2018-10-12T02:50:55+02:00", comments = "JAXB RI v2.2.11")
    public StringBuilder append(ObjectLocator locator, StringBuilder buffer, ToStringStrategy2 strategy) {
        strategy.appendStart(locator, this, buffer);
        appendFields(locator, buffer, strategy);
        strategy.appendEnd(locator, this, buffer);
        return buffer;
    }

    @Generated(value = "com.sun.tools.xjc.Driver", date = "2018-10-12T02:50:55+02:00", comments = "JAXB RI v2.2.11")
    public StringBuilder appendFields(ObjectLocator locator, StringBuilder buffer, ToStringStrategy2 strategy) {
        {
            Zimmer.Zimmertyp theZimmertyp;
            theZimmertyp = this.getZimmertyp();
            strategy.appendField(locator, this, "zimmertyp", buffer, theZimmertyp, (this.zimmertyp!= null));
        }
        return buffer;
    }

    @Generated(value = "com.sun.tools.xjc.Driver", date = "2018-10-12T02:50:55+02:00", comments = "JAXB RI v2.2.11")
    public Object clone() {
        return copyTo(createNewInstance());
    }

    @Generated(value = "com.sun.tools.xjc.Driver", date = "2018-10-12T02:50:55+02:00", comments = "JAXB RI v2.2.11")
    public Object copyTo(Object target) {
        final CopyStrategy2 strategy = JAXBCopyStrategy.INSTANCE;
        return copyTo(null, target, strategy);
    }

    @Generated(value = "com.sun.tools.xjc.Driver", date = "2018-10-12T02:50:55+02:00", comments = "JAXB RI v2.2.11")
    public Object copyTo(ObjectLocator locator, Object target, CopyStrategy2 strategy) {
        final Object draftCopy = ((target == null)?createNewInstance():target);
        if (draftCopy instanceof Zimmer) {
            final Zimmer copy = ((Zimmer) draftCopy);
            {
                Boolean zimmertypShouldBeCopiedAndSet = strategy.shouldBeCopiedAndSet(locator, (this.zimmertyp!= null));
                if (zimmertypShouldBeCopiedAndSet == Boolean.TRUE) {
                    Zimmer.Zimmertyp sourceZimmertyp;
                    sourceZimmertyp = this.getZimmertyp();
                    Zimmer.Zimmertyp copyZimmertyp = ((Zimmer.Zimmertyp) strategy.copy(LocatorUtils.property(locator, "zimmertyp", sourceZimmertyp), sourceZimmertyp, (this.zimmertyp!= null)));
                    copy.setZimmertyp(copyZimmertyp);
                } else {
                    if (zimmertypShouldBeCopiedAndSet == Boolean.FALSE) {
                        copy.zimmertyp = null;
                    }
                }
            }
        }
        return draftCopy;
    }

    @Generated(value = "com.sun.tools.xjc.Driver", date = "2018-10-12T02:50:55+02:00", comments = "JAXB RI v2.2.11")
    public Object createNewInstance() {
        return new Zimmer();
    }

    @Generated(value = "com.sun.tools.xjc.Driver", date = "2018-10-12T02:50:55+02:00", comments = "JAXB RI v2.2.11")
    public boolean equals(ObjectLocator thisLocator, ObjectLocator thatLocator, Object object, EqualsStrategy2 strategy) {
        if ((object == null)||(this.getClass()!= object.getClass())) {
            return false;
        }
        if (this == object) {
            return true;
        }
        final Zimmer that = ((Zimmer) object);
        {
            Zimmer.Zimmertyp lhsZimmertyp;
            lhsZimmertyp = this.getZimmertyp();
            Zimmer.Zimmertyp rhsZimmertyp;
            rhsZimmertyp = that.getZimmertyp();
            if (!strategy.equals(LocatorUtils.property(thisLocator, "zimmertyp", lhsZimmertyp), LocatorUtils.property(thatLocator, "zimmertyp", rhsZimmertyp), lhsZimmertyp, rhsZimmertyp, (this.zimmertyp!= null), (that.zimmertyp!= null))) {
                return false;
            }
        }
        return true;
    }

    @Generated(value = "com.sun.tools.xjc.Driver", date = "2018-10-12T02:50:55+02:00", comments = "JAXB RI v2.2.11")
    public boolean equals(Object object) {
        final EqualsStrategy2 strategy = JAXBEqualsStrategy.INSTANCE;
        return equals(null, null, object, strategy);
    }

    /**
     *
     *                     Java enum for zimmertyp attribute in <zimmer> elements.
     *
     *
Java class for null.
     *
     *
The following schema fragment specifies the expected content contained within this class.
     *

     *

     * <simpleType>
     *   <restriction base="{http://www.w3.org/2001/XMLSchema}string">
     *     <enumeration value="ZIMMER"/>
     *   </restriction>
     * </simpleType>
     *

     *
     */
    @XmlType(name = "")
    @XmlEnum
    @Generated(value = "com.sun.tools.xjc.Driver", date = "2018-10-12T02:50:55+02:00", comments = "JAXB RI v2.2.11")
    public enum Zimmertyp {

        ZIMMER;

        public String value() {
            return name();
        }

        public static Zimmer.Zimmertyp fromValue(String v) {
            return valueOf(v);
        }

    }

}


/*
 * 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.core;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.io.Writer;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import javax.xml.bind.DatatypeConverter;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.jaxen.JaxenException;
import org.jaxen.XPath;
import org.jaxen.dom.DOMXPath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Some helper functions for XML processing.
 *
 * @author Andreas Rudolph
 * @since 1.0
 */
public final class XmlUtils {
    private final static Logger LOGGER = LoggerFactory.getLogger(XmlUtils.class);

    private XmlUtils() {
    }

    /**
     * Recursively remove any comments and unnecessary white spaces from a
     * {@link Node} and its children.
     *
     * @param node the node to clean
     */
    public static void clean(Node node) {
        NodeList childNodes = node.getChildNodes();
        for (int n = childNodes.getLength() - 1; n >= 0; n--) {
            Node child = childNodes.item(n);
            short nodeType = child.getNodeType();
            if (nodeType == Node.ELEMENT_NODE) {
                XmlUtils.clean(child);
            } else if (nodeType == Node.COMMENT_NODE) {
                node.removeChild(child);
            } else if (nodeType == Node.TEXT_NODE) {
                String value = StringUtils.trimToNull(child.getNodeValue());
                if (value == null)
                    node.removeChild(child);
                else
                    child.setNodeValue(value);
            }
        }
    }

    /**
     * Count the number of nodes, that are matching against an XPath expression.
     *
     * @param xpathExpression the XPath expression to match
     * @param doc             the document, on which the XPath expression is evaluated
     * @return number of matching nodes
     * @throws JaxenException if the XPath evaluation failed
     */
    public static int countNodes(String xpathExpression, Document doc) throws JaxenException {
        return XmlUtils.countNodes(xpathExpression, doc, doc);
    }

    /**
     * Count the number of nodes, that are matching against an XPath expression.
     *
     * @param xpathExpression the XPath expression to match
     * @param doc             the document, on which the XPath expression is evaluated
     * @param context         the node, on which the XPath expression is evaluated
     * @return number of matching nodes
     * @throws JaxenException if the XPath evaluation failed
     */
    public static int countNodes(String xpathExpression, Document doc, Object context) throws JaxenException {
        return XmlUtils.newXPath(xpathExpression, doc).selectNodes(context).size();
    }

    /**
     * Return the root {@link Element} of a {@link Document}.
     *
     * @param doc the document, where the root element is looked up
     * @return root element of the document or null, if not found
     */
    public static Element getRootElement(Document doc) {
        if (doc == null) return null;
        NodeList children = doc.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            Node node = children.item(i);
            if (node instanceof Element) return (Element) node;
        }
        return null;
    }

    /**
     * Create an empty {@link Document}.
     *
     * @return created document
     * @throws ParserConfigurationException if the parser is not properly configured
     */
    public static Document newDocument() throws ParserConfigurationException {
        return XmlUtils.newDocument(true);
    }

    /**
     * Create an empty {@link Document}.
     *
     * @param namespaceAware if namespaces are used in the created document
     * @return created document
     * @throws ParserConfigurationException if the parser is not properly configured
     */
    public static Document newDocument(boolean namespaceAware) throws ParserConfigurationException {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(namespaceAware);
        DocumentBuilder builder = factory.newDocumentBuilder();
        return builder.newDocument();
    }

    /**
     * Create a {@link Document} from a XML string.
     *
     * @param xmlString the XML string, the document is created from
     * @return created document
     * @throws SAXException                 if XML contains an error
     * @throws IOException                  if XML is not readable
     * @throws ParserConfigurationException if the parser is not properly configured
     */
    public static Document newDocument(String xmlString) throws SAXException, IOException, ParserConfigurationException {
        return XmlUtils.newDocument(xmlString, true);
    }

    /**
     * Create a {@link Document} from a XML string.
     *
     * @param xmlString      the XML string, the document is created from
     * @param namespaceAware if namespaces are used in the created document
     * @return created document
     * @throws SAXException                 if XML contains an error
     * @throws IOException                  if XML is not readable
     * @throws ParserConfigurationException if the parser is not properly configured
     */
    public static Document newDocument(String xmlString, boolean namespaceAware) throws SAXException, IOException, ParserConfigurationException {
        return XmlUtils.newDocument(
                new InputSource(new StringReader(xmlString)), namespaceAware);
    }

    /**
     * Create a {@link Document} from an {@link InputSource}.
     *
     * @param xmlSource the XML input, the document is created from
     * @return created document
     * @throws SAXException                 if XML contains an error
     * @throws IOException                  if XML is not readable
     * @throws ParserConfigurationException if the parser is not properly configured
     */
    public static Document newDocument(InputSource xmlSource) throws SAXException, IOException, ParserConfigurationException {
        return XmlUtils.newDocument(xmlSource, true);
    }

    /**
     * Create a {@link Document} from an {@link InputSource}.
     *
     * @param xmlSource      the XML input, the document is created from
     * @param namespaceAware if namespaces are used in the created document
     * @return created document
     * @throws SAXException                 if XML contains an error
     * @throws IOException                  if XML is not readable
     * @throws ParserConfigurationException if the parser is not properly configured
     */
    public static Document newDocument(InputSource xmlSource, boolean namespaceAware) throws SAXException, IOException, ParserConfigurationException {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(namespaceAware);
        DocumentBuilder builder = factory.newDocumentBuilder();
        return builder.parse(xmlSource);
    }

    /**
     * Create a {@link Document} from an {@link InputStream}.
     *
     * @param xmlStream the XML input, the document is created from
     * @return created document
     * @throws SAXException                 if XML contains an error
     * @throws IOException                  if XML is not readable
     * @throws ParserConfigurationException if the parser is not properly configured
     */
    public static Document newDocument(InputStream xmlStream) throws SAXException, IOException, ParserConfigurationException {
        return XmlUtils.newDocument(xmlStream, true);
    }

    /**
     * Create a {@link Document} from an {@link InputStream}.
     *
     * @param xmlStream      the XML input, the document is created from
     * @param namespaceAware if namespaces are used in the created document
     * @return created document
     * @throws SAXException                 if XML contains an error
     * @throws IOException                  if XML is not readable
     * @throws ParserConfigurationException if the parser is not properly configured
     */
    public static Document newDocument(InputStream xmlStream, boolean namespaceAware) throws SAXException, IOException, ParserConfigurationException {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(namespaceAware);
        DocumentBuilder builder = factory.newDocumentBuilder();
        return builder.parse(xmlStream);
    }

    /**
     * Create a {@link Document} from a {@link File}.
     *
     * @param xmlFile the XML file, the document is created from
     * @return created document
     * @throws SAXException                 if XML contains an error
     * @throws IOException                  if XML is not readable
     * @throws ParserConfigurationException if the parser is not properly configured
     */
    public static Document newDocument(File xmlFile) throws SAXException, IOException, ParserConfigurationException {
        return XmlUtils.newDocument(xmlFile, true);
    }

    /**
     * Create a {@link Document} from a {@link File}.
     *
     * @param xmlFile        the XML file, the document is created from
     * @param namespaceAware if namespaces are used in the created document
     * @return created document
     * @throws SAXException                 if XML contains an error
     * @throws IOException                  if XML is not readable
     * @throws ParserConfigurationException if the parser is not properly configured
     */
    public static Document newDocument(File xmlFile, boolean namespaceAware) throws SAXException, IOException, ParserConfigurationException {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(namespaceAware);
        DocumentBuilder builder = factory.newDocumentBuilder();
        return builder.parse(xmlFile);
    }

    /**
     * Create a {@link XPath} expression.
     *
     * @param expression string with the XPath expression to create
     * @return the created XPath expression
     * @throws JaxenException if the XPath is not creatable
     */
    public static XPath newXPath(String expression) throws JaxenException {
        return XmlUtils.newXPath(expression, null, null);
    }

    /**
     * Create a {@link XPath} expression.
     *
     * @param expression string with the XPath expression to create
     * @param doc        the document, whose namespace is bound to the XPath expression
     * @return the created XPath expression
     * @throws JaxenException if the XPath is not creatable
     */
    public static XPath newXPath(String expression, Document doc) throws JaxenException {
        return XmlUtils.newXPath(expression, doc, "io");
    }

    /**
     * Create a {@link XPath} expression.
     *
     * @param expression      string with the XPath expression to create
     * @param doc             the document, whose namespace is bound to the XPath expression
     * @param namespacePrefix prefix of the document namespace, that is bound to the XPath expression
     * @return the created XPath expression
     * @throws JaxenException if the XPath is not creatable
     */
    public static XPath newXPath(String expression, Document doc, String namespacePrefix) throws JaxenException {
        DOMXPath xpath = new DOMXPath(expression);
        //LOGGER.debug( "new xpath: " + xpath.debug() );
        if (doc != null && namespacePrefix != null) {
            Element root = XmlUtils.getRootElement(doc);
            String uri = StringUtils.trimToEmpty(root.getNamespaceURI());
            xpath.addNamespace(namespacePrefix, uri);
        }
        return xpath;
    }

    /**
     * Reads an xsd:date value.
     *

     * If the value is not a valid xsd:date, we're trying to parse the value
     * against other common formats. If this also fails, we're trying to parse the
     * value as xsd:dateTime.
     *

     * The functions throws an {@link IllegalArgumentException}, if the value is
     * not readable.
     *
     * @param value the XML value to parse
     * @return parsed date value
     */
    public static Calendar parseDate(String value) {
        return XmlUtils.parseDate(value, true);
    }

    private static Calendar parseDate(String value, boolean tryDateTimeOnError) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;
        try {
            return DatatypeConverter.parseDate(value);
        } catch (IllegalArgumentException ex) {
            //LOGGER.warn( "Can't parse value '" + value + "' as date!" );
            //LOGGER.warn( "> " + ex.getLocalizedMessage(), ex );
        }
        try {
            Date date = DateUtils.parseDateStrictly(value, new String[]{
                    "dd.MM.yyyy", "dd.MM.yy", "dd/MM/yyyy", "dd/MM/yy", "dd-MM-yyyy",
                    "dd-MMM-yyyy", "yyyy-MM-dd", "yyyy/MM/dd", "yyyy-D", "MM/yyyy",
                    "MMM yyyy", "MMMMM yyyy", "yyyy"});
            Calendar cal = Calendar.getInstance();
            cal.setTime(date);
            return cal;
        } catch (ParseException ex) {
            //LOGGER.warn( "Can't parse value '" + value + "' as date!" );
            //LOGGER.warn( "> " + ex.getLocalizedMessage(), ex );
        }

        // try to parse the value as xsd:dateTime instead
        if (tryDateTimeOnError) {
            try {
                return XmlUtils.parseDateTime(value, false);
            } catch (IllegalArgumentException ex) {
                //LOGGER.warn( "Can't parse value '" + value + "' as datetime!" );
                //LOGGER.warn( "> " + ex.getLocalizedMessage(), ex );
            }
        }

        throw new IllegalArgumentException("Can't parse date value '" + value + "'!");
    }

    /**
     * Reads an xsd:dateTime value.
     *

     * If the value is not a valid xsd:dateTime, we're trying to parse the value
     * against other common formats. If this also fails, we're trying to parse the
     * value as xsd:date.
     *

     * The functions throws an {@link IllegalArgumentException}, if the value is
     * not readable.
     *
     * @param value the XML value to parse
     * @return parsed date value
     */
    public static Calendar parseDateTime(String value) {
        return XmlUtils.parseDateTime(value, true);
    }

    private static Calendar parseDateTime(String value, boolean tryDateOnError) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;
        try {
            return DatatypeConverter.parseDateTime(value);
        } catch (IllegalArgumentException ex) {
            //LOGGER.warn( "Can't parse value '" + value + "' as datetime!" );
            //LOGGER.warn( "> " + ex.getLocalizedMessage(), ex );
        }
        try {
            Date date = DateUtils.parseDateStrictly(value, new String[]{
                    "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm"});
            Calendar cal = Calendar.getInstance();
            cal.setTime(date);
            return cal;
        } catch (ParseException ex) {
            //LOGGER.warn( "Can't parse value '" + value + "' as datetime!" );
            //LOGGER.warn( "> " + ex.getLocalizedMessage(), ex );
        }

        // try to parse the value as xsd:date instead
        if (tryDateOnError) {
            try {
                return XmlUtils.parseDate(value, false);
            } catch (IllegalArgumentException ex) {
                //LOGGER.warn( "Can't parse value '" + value + "' as datetime!" );
                //LOGGER.warn( "> " + ex.getLocalizedMessage(), ex );
            }
        }

        throw new IllegalArgumentException("Can't parse date-time value '" + value + "'!");
    }

    /**
     * Print nodes of a {@link Document} recursively to the local logger.
     *
     * @param doc the document to print
     */
    public static void printNodes(Document doc) {
        NodeList children = doc.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            Node node = children.item(i);
            if (node instanceof Element) printNode((Element) node);
        }
    }

    /**
     * Print an {@link Element} and its children to the local logger.
     *
     * @param node the element to print
     */
    public static void printNode(Element node) {
        XmlUtils.printNode(node, 0);
    }

    private static void printNode(Element node, int indent) {
        String prefix = (indent > 0) ? StringUtils.repeat(">", indent) + " " : "";
        LOGGER.debug(prefix + "<" + node.getTagName() + "> / " + node.getNamespaceURI() + " / " + node.getPrefix());

        prefix = StringUtils.repeat(">", indent + 1) + " ";
        NamedNodeMap attribs = node.getAttributes();
        for (int i = 0; i < attribs.getLength(); i++) {
            Attr attrib = (Attr) attribs.item(i);
            LOGGER.debug(prefix + "@" + attrib.getName() + " / " + attrib.getNamespaceURI() + " / " + attrib.getPrefix());
        }
        NodeList children = node.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            Node child = children.item(i);
            if (child instanceof Element) {
                XmlUtils.printNode((Element) child, indent + 1);
            }
        }
    }

    /**
     * Replace the namespace of any {@link Element} in a {@link Document}.
     *
     * @param doc             the document to update
     * @param newNamespaceURI the new namespace URI
     */
    public static void replaceNamespace(Document doc, String newNamespaceURI) {
        NodeList children = doc.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            XmlUtils.replaceNamespace(doc, children.item(i), newNamespaceURI);
        }
    }

    /**
     * Replace the namespace of a {@link Node} and its children.
     *
     * @param doc             the document to update
     * @param node            the node to update
     * @param newNamespaceURI the new namespace URI
     */
    public static void replaceNamespace(Document doc, Node node, String newNamespaceURI) {
        if (node instanceof Attr) {
            doc.renameNode(node, newNamespaceURI, node.getLocalName());
        } else if (node instanceof Element) {
            doc.renameNode(node, newNamespaceURI, node.getLocalName());
            NodeList children = node.getChildNodes();
            for (int i = 0; i < children.getLength(); i++) {
                XmlUtils.replaceNamespace(doc, children.item(i), newNamespaceURI);
            }
        }
    }

    /**
     * Replace all text values of a {@link Document} with CDATA values.
     *
     * @param doc the document to update
     */
    public static void replaceTextWithCData(Document doc) {
        NodeList children = doc.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            XmlUtils.replaceTextWithCData(doc, children.item(i));
        }
    }

    /**
     * Replace all text values of a {@link Node} with CDATA values.
     *
     * @param doc  the document to update
     * @param node the node to update
     */
    public static void replaceTextWithCData(Document doc, Node node) {
        if (node instanceof Text) {
            Text text = (Text) node;
            CDATASection cdata = doc.createCDATASection(text.getTextContent());
            Element parent = (Element) text.getParentNode();
            parent.replaceChild(cdata, text);
        } else if (node instanceof Element) {
            //LOGGER.debug( "ELEMENT " + element.getTagName() );
            NodeList children = node.getChildNodes();
            for (int i = 0; i < children.getLength(); i++) {
                //LOGGER.debug( "> " + children.item( i ).getClass().getName() );
                XmlUtils.replaceTextWithCData(doc, children.item(i));
            }
        }
    }

    /**
     * Write a {@link Document} to a {@link File}.
     *
     * @param doc  the document to write
     * @param file the file, where the document is written to
     * @throws TransformerException if XML transformation failed
     * @throws IOException          if writing failed
     */
    public static void write(Document doc, File file) throws TransformerException, IOException {
        XmlUtils.write(doc, file, true);
    }

    /**
     * Write a {@link Document} to a {@link File}.
     *
     * @param doc         the document to write
     * @param file        the file, where the document is written to
     * @param prettyPrint if pretty printing is enabled for the generated XML code
     * @throws TransformerException if XML transformation failed
     * @throws IOException          if writing failed
     */
    public static void write(Document doc, File file, boolean prettyPrint) throws TransformerException, IOException {
        try (OutputStream output = new FileOutputStream(file)) {
            XmlUtils.write(doc, output, prettyPrint);
        }
    }

    /**
     * Write a {@link Document} to an {@link OutputStream}.
     *
     * @param doc    the document to write
     * @param output the output, where the document is written to
     * @throws TransformerException if XML transformation failed
     */
    public static void write(Document doc, OutputStream output) throws TransformerException {
        XmlUtils.write(doc, output, true);
    }

    /**
     * Write a {@link Document} to an {@link OutputStream}.
     *
     * @param doc         the document to write
     * @param output      the output, where the document is written to
     * @param prettyPrint if pretty printing is enabled for the generated XML code
     * @throws TransformerException if XML transformation failed
     */
    public static void write(Document doc, OutputStream output, boolean prettyPrint) throws TransformerException {
        XmlUtils.clean(doc);
        Transformer transformer = TransformerFactory.newInstance().newTransformer();
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
        transformer.setOutputProperty(OutputKeys.METHOD, "xml");
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
        transformer.setOutputProperty(OutputKeys.INDENT, (prettyPrint) ? "yes" : "no");
        if (prettyPrint) {
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
        }
        transformer.transform(new DOMSource(doc), new StreamResult(output));
    }

    /**
     * Write a {@link Document} to a {@link Writer}.
     *
     * @param doc    the document to write
     * @param output the output, where the document is written to
     * @throws TransformerException if XML transformation failed
     */
    public static void write(Document doc, Writer output) throws TransformerException {
        XmlUtils.write(doc, output, true);
    }

    /**
     * Write a {@link Document} to a {@link Writer}.
     *
     * @param doc         the document to write
     * @param output      the output, where the document is written to
     * @param prettyPrint if pretty printing is enabled for the generated XML code
     * @throws TransformerException if XML transformation failed
     */
    public static void write(Document doc, Writer output, boolean prettyPrint) throws TransformerException {
        XmlUtils.clean(doc);
        Transformer transformer = TransformerFactory.newInstance().newTransformer();
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
        transformer.setOutputProperty(OutputKeys.METHOD, "xml");
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
        transformer.setOutputProperty(OutputKeys.INDENT, (prettyPrint) ? "yes" : "no");
        if (prettyPrint) {
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
        }
        transformer.transform(new DOMSource(doc), new StreamResult(output));
    }
}


/*
 * 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.core;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.Writer;
import javax.xml.bind.JAXBException;
import javax.xml.transform.TransformerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;

/**
 * A container for a XML {@link Document}, that provides functions for
 * conversion from / to Java objects.
 *
 * @param <JavaType> the class of a (via JAXB generated) Java object, that the contained
 *                   {@link Document} is mapped to
 * @author Andreas Rudolph
 * @since 1.0
 */
public abstract class XmlDocument<JavaType> {
    private final static Logger LOGGER = LoggerFactory.getLogger(XmlDocument.class);
    private final Document document;
    private boolean textWrittenAsCDATA = false;

    /**
     * Create from a {@link Document}.
     *
     * @param document the contained document
     */
    protected XmlDocument(Document document) {
        this.document = document;
    }

    /**
     * Returns the {@link Document}, that is contained by this object.
     *
     * @return the contained document
     */
    public final Document getDocument() {
        return this.document;
    }

    /**
     * Check, if text elements of generated XML should use CDATA.
     *
     * @return true, if CDATA is used for XML generation
     */
    public final boolean isTextWrittenAsCDATA() {
        return this.textWrittenAsCDATA;
    }

    /**
     * Make some modifications to a {@link Document}, before it is written to XML.
     *
     * @param doc the document, that will be written to XML
     * @throws IOException if an error occurred and XML generation should be stopped
     */
    protected void prepareDocumentBeforeWritingToXml(Document doc) throws IOException {
    }

    /**
     * Enable CDATA for text elements in generated XML.
     *
     * @param textsAsCDATA true, if CDATA is used for XML generation
     */
    public void setTextWrittenAsCDATA(boolean textsAsCDATA) {
        this.textWrittenAsCDATA = textsAsCDATA;
    }

    /**
     * Creates an object from the contained {@link Document}.
     *
     * @return created object, that represents the contained {@link Document}
     * @throws JAXBException if the Java object is not creatable
     */
    public abstract JavaType toObject() throws JAXBException;

    /**
     * Generate XML for the contained {@link Document}.
     *
     * @param xmlFile the file, where the XML code is written to
     * @throws TransformerException if XML transformation failed
     * @throws IOException          if writing failed
     */
    public final void toXml(File xmlFile) throws TransformerException, IOException {
        toXml(xmlFile, true);
    }

    /**
     * Generate XML for the contained {@link Document}.
     *
     * @param xmlFile     the file, where the XML code is written to
     * @param prettyPrint if pretty printing is enabled for the generated XML code
     * @throws TransformerException if XML transformation failed
     * @throws IOException          if writing failed
     */
    public void toXml(File xmlFile, boolean prettyPrint) throws TransformerException, IOException {
        if (this.isTextWrittenAsCDATA()) {
            XmlUtils.replaceTextWithCData(this.getDocument());
        }
        prepareDocumentBeforeWritingToXml(this.getDocument());
        XmlUtils.write(this.getDocument(), xmlFile, prettyPrint);
    }

    /**
     * Generate XML for the contained {@link Document}.
     *
     * @param output the output, where the XML code is written to
     * @throws TransformerException if XML transformation failed
     * @throws IOException          if writing failed
     */
    public final void toXml(OutputStream output) throws TransformerException, IOException {
        toXml(output, true);
    }

    /**
     * Generate XML for the contained {@link Document}.
     *
     * @param output      the output, where the XML code is written to
     * @param prettyPrint if pretty printing is enabled for the generated XML code
     * @throws TransformerException if XML transformation failed
     * @throws IOException          if writing failed
     */
    public void toXml(OutputStream output, boolean prettyPrint) throws TransformerException, IOException {
        if (this.isTextWrittenAsCDATA()) {
            XmlUtils.replaceTextWithCData(this.getDocument());
        }
        prepareDocumentBeforeWritingToXml(this.getDocument());
        XmlUtils.write(this.getDocument(), output, prettyPrint);
    }

    /**
     * Generate XML for the contained {@link Document}.
     *
     * @param output the output, where the XML code is written to
     * @throws TransformerException if XML transformation failed
     * @throws IOException          if writing failed
     */
    public final void toXml(Writer output) throws TransformerException, IOException {
        toXml(output, true);
    }

    /**
     * Generate XML for the contained {@link Document}.
     *
     * @param output      the output, where the XML code is written to
     * @param prettyPrint if pretty printing is enabled for the generated XML code
     * @throws TransformerException if XML transformation failed
     * @throws IOException          if writing failed
     */
    public void toXml(Writer output, boolean prettyPrint) throws TransformerException, IOException {
        if (this.isTextWrittenAsCDATA()) {
            XmlUtils.replaceTextWithCData(this.getDocument());
        }
        prepareDocumentBeforeWritingToXml(this.getDocument());
        XmlUtils.write(this.getDocument(), output, prettyPrint);
    }

    /**
     * Returns XML for the contained {@link Document}.
     *
     * @return the generated XML code
     * @throws TransformerException if XML transformation failed
     * @throws IOException          if writing failed
     */
    public String toXmlString() throws TransformerException, IOException {
        return toXmlString(true);
    }

    /**
     * Returns XML for the contained {@link Document}.
     *
     * @param prettyPrint if pretty printing is enabled for the generated XML code
     * @return the generated XML code
     * @throws TransformerException if XML transformation failed
     * @throws IOException          if writing failed
     */
    public final String toXmlString(boolean prettyPrint) throws TransformerException, IOException {
        try (StringWriter w = new StringWriter()) {
            this.toXml(w, prettyPrint);
            return w.toString();
        }
    }
}


/*
 * 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.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import javax.xml.bind.DatatypeConverter;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.openestate.io.core.XmlUtils;
import org.openestate.io.core.XmlValidationHandler;
import org.openestate.io.wis_it.xml.ObjectFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

/**
 * Some helper functions for the XML format of
 * wohnen-in-suedtirol.it.
 *
 * @author Andreas Rudolph
 * @since 1.0
 */
@SuppressWarnings("WeakerAccess")
public class WisItUtils {
    @SuppressWarnings("unused")
    private final static Logger LOGGER = LoggerFactory.getLogger(WisItUtils.class);
    private static JAXBContext JAXB = null;

    /*
     * the latest implemented version of this format
     *
     * public final static String VERSION = "1.0";
     */

    /**
     * the XML target namespace of this format
     */
    @SuppressWarnings("unused")
    public final static String NAMESPACE = StringUtils.EMPTY;

    /**
     * the package, where generated JAXB classes are located
     */
    @SuppressWarnings("unused")
    public final static String PACKAGE = "org.openestate.io.wis_it.xml";

    /**
     * the factory for creation of JAXB objects
     */
    @SuppressWarnings("unused")
    private final static ObjectFactory FACTORY = new ObjectFactory();

    private WisItUtils() {
    }

    /**
     * Creates a {@link WisItDocument} from an {@link InputStream}.
     *
     * @param input XML input
     * @return created document or null, of the document is not supported by this format
     * @throws SAXException                 if XML is invalid
     * @throws IOException                  if reading failed
     * @throws ParserConfigurationException if the parser is not properly configured
     */
    public static WisItDocument createDocument(InputStream input) throws SAXException, IOException, ParserConfigurationException {
        return createDocument(XmlUtils.newDocument(input, true));
    }

    /**
     * Creates a {@link WisItDocument} from a {@link File}.
     *
     * @param xmlFile XML file
     * @return created document or null, of the document is not supported by this format
     * @throws SAXException                 if XML is invalid
     * @throws IOException                  if reading failed
     * @throws ParserConfigurationException if the parser is not properly configured
     */
    public static WisItDocument createDocument(File xmlFile) throws SAXException, IOException, ParserConfigurationException {
        return createDocument(XmlUtils.newDocument(xmlFile, true));
    }

    /**
     * Creates a {@link WisItDocument} from a {@link String}.
     *
     * @param xmlString XML string
     * @return created document or null, of the document is not supported by this format
     * @throws SAXException                 if XML is invalid
     * @throws IOException                  if reading failed
     * @throws ParserConfigurationException if the parser is not properly configured
     */
    public static WisItDocument createDocument(String xmlString) throws SAXException, IOException, ParserConfigurationException {
        return createDocument(XmlUtils.newDocument(xmlString, true));
    }

    /**
     * Creates a {@link WisItDocument} from a {@link Document}.
     *
     * @param doc XML document
     * @return created document or null, of the document is not supported by this format
     */
    public static WisItDocument createDocument(Document doc) {
        if (WisItDocument.isReadable(doc))
            return new WisItDocument(doc);
        else
            return null;
    }

    /**
     * Creates a {@link Marshaller} to write JAXB objects into XML.
     *
     * @return created marshaller
     * @throws JAXBException if a problem with JAXB occurred
     */
    @SuppressWarnings("unused")
    public static Marshaller createMarshaller() throws JAXBException {
        return createMarshaller(Charset.defaultCharset().name(), true);
    }

    /**
     * Creates a {@link Marshaller} to write JAXB objects into XML.
     *
     * @param encoding  encoding of written XML
     * @param formatted if written XML is pretty printed
     * @return created marshaller
     * @throws JAXBException if a problem with JAXB occurred
     */
    @SuppressWarnings("Duplicates")
    public static Marshaller createMarshaller(String encoding, boolean formatted) throws JAXBException {
        Marshaller m = getContext().createMarshaller();
        m.setProperty(Marshaller.JAXB_ENCODING, encoding);
        m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, formatted);
        m.setEventHandler(new XmlValidationHandler());
        return m;
    }

    /**
     * Creates a {@link Unmarshaller} to read JAXB objects from XML.
     *
     * @return created unmarshaller
     * @throws JAXBException if a problem with JAXB occurred
     */
    public static Unmarshaller createUnmarshaller() throws JAXBException {
        Unmarshaller m = getContext().createUnmarshaller();
        m.setEventHandler(new XmlValidationHandler());
        return m;
    }

    /**
     * Returns the {@link JAXBContext} for this format.
     *
     * @return context
     * @throws JAXBException if a problem with JAXB occurred
     */
    public synchronized static JAXBContext getContext() throws JAXBException {
        if (JAXB == null) initContext(Thread.currentThread().getContextClassLoader());
        return JAXB;
    }

    /**
     * Returns the preferred date-time format for this format.
     *
     * @return date format
     */
    public static DateFormat getDateTimeFormat() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }

    /**
     * Returns the {@link ObjectFactory} for this format.
     *
     * @return object factory
     */
    public synchronized static ObjectFactory getFactory() {
        return FACTORY;
    }

    /**
     * Initializes the {@link JAXBContext} for this format.
     *
     * @param classloader the classloader to load the generated JAXB classes with
     * @throws JAXBException if a problem with JAXB occurred
     */
    public synchronized static void initContext(ClassLoader classloader) throws JAXBException {
        JAXB = JAXBContext.newInstance(PACKAGE, classloader);
    }

    public static Calendar parseDateTime(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;
        try {
            Date date = DateUtils.parseDateStrictly(value, "yyyy-MM-dd mm:hh:ss", "yyyy-MM-dd mm:hh", "yyyy-MM-dd");
            Calendar cal = Calendar.getInstance();
            cal.setTime(date);
            return cal;
        } catch (ParseException ex) {
            //LOGGER.warn( "Can't parse value '" + value + "' as date-time!" );
            //LOGGER.warn( "> " + ex.getLocalizedMessage(), ex );
        }

        // try to parse the value as xsd:dateTime instead
        try {
            return XmlUtils.parseDateTime(value);
        } catch (IllegalArgumentException ex) {
            //LOGGER.warn( "Can't parse value '" + value + "' as date-time!" );
            //LOGGER.warn( "> " + ex.getLocalizedMessage(), ex );
        }

        throw new IllegalArgumentException("Can't parse date-time value '" + value + "'!");
    }

    public static BigDecimal parseDecimal(String value) {
        value = StringUtils.trimToNull(value);
        return (value != null) ? DatatypeConverter.parseDecimal(value) : null;
    }

    public static BigDecimal parseDouble(String value) {
        value = StringUtils.trimToNull(value);
        return (value != null) ? DatatypeConverter.parseDecimal(value) : null;
    }

    public static BigInteger parsePositiveInteger(String value) {
        value = StringUtils.trimToNull(value);
        return (value != null) ? DatatypeConverter.parseInteger(value) : null;
    }

    public static Boolean parseYesNo(String value) {
        value = StringUtils.trimToNull(value);
        if ("ja".equalsIgnoreCase(value))
            return Boolean.TRUE;
        else if ("nein".equalsIgnoreCase(value))
            return Boolean.FALSE;
        else if (value != null)
            return DatatypeConverter.parseBoolean(value);
        else
            throw new IllegalArgumentException("Can't parse boolean value!");
    }

    public static String printDateTime(Calendar value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print date-time value!");
        else
            return getDateTimeFormat().format(value.getTime());
    }

    public static String printDecimal(BigDecimal value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print decimal value!");
        else
            return DatatypeConverter.printDecimal(value.setScale(2, BigDecimal.ROUND_HALF_UP));
    }

    public static String printDouble(BigDecimal value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print double value!");
        else
            return DatatypeConverter.printDecimal(value);
    }

    public static String printPositiveInteger(BigInteger value) {
        if (value == null || value.compareTo(BigInteger.ZERO) < 1)
            throw new IllegalArgumentException("Can't print positive integer value!");
        else
            return DatatypeConverter.printInteger(value);
    }

    public static String printYesNo(Boolean value) {
        if (Boolean.TRUE.equals(value))
            return "ja";
        else if (Boolean.FALSE.equals(value))
            return "nein";
        else
            throw new IllegalArgumentException("Can't print boolean value!");
    }
}