org.openestate.is24.restapi.utils.ExportHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.openestate.is24.restapi.utils.ExportHandler.java

Source

/*
 * Copyright 2014-2016 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.is24.restapi.utils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.xml.bind.JAXBException;
import oauth.signpost.exception.OAuthException;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOExceptionWithCause;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.openestate.is24.restapi.AbstractClient;
import org.openestate.is24.restapi.ImportExport;
import org.openestate.is24.restapi.xml.common.Attachment;
import org.openestate.is24.restapi.xml.common.Attachments;
import org.openestate.is24.restapi.xml.common.Link;
import org.openestate.is24.restapi.xml.common.Message;
import org.openestate.is24.restapi.xml.common.MessageCode;
import org.openestate.is24.restapi.xml.common.Messages;
import org.openestate.is24.restapi.xml.common.PDFDocument;
import org.openestate.is24.restapi.xml.common.Picture;
import org.openestate.is24.restapi.xml.common.PublishChannel;
import org.openestate.is24.restapi.xml.common.PublishChannels;
import org.openestate.is24.restapi.xml.common.PublishObject;
import org.openestate.is24.restapi.xml.common.PublishObjects;
import org.openestate.is24.restapi.xml.common.RealEstateState;
import org.openestate.is24.restapi.xml.common.RealtorContactDetails;
import org.openestate.is24.restapi.xml.common.StreamingVideo;
import org.openestate.is24.restapi.xml.offerlistelement.OfferRealEstateForList;
import org.openestate.is24.restapi.xml.offerlistelement.RealEstateList;
import org.openestate.is24.restapi.xml.realestates.RealEstate;
import org.openestate.is24.restapi.xml.realestates.RealEstates;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Handler for bulk exports.
 * <p>
 * The {@link ExportHandler} is part of the high level API for bulk exports of real
 * estate data.
 * <p>
 * A previously created {@link ExportPool} can be processed with the
 * {@link ExportHandler#export(org.openestate.is24.restapi.AbstractClient, org.openestate.is24.restapi.utils.ExportPool, boolean, boolean)}
 * method. The {@link ExportHandler} will launch the required low level
 * operations of the {@link ImportExport}-API for each pooled object.
 *
 * @since 0.2
 * @author Andreas Rudolph <andy@openindex.de>
 */
public class ExportHandler {
    private final static Logger LOGGER = LoggerFactory.getLogger(ExportHandler.class);
    private final List<ExportMessage> messages = new ArrayList<ExportMessage>();
    private final List<String> savedContactIds = new ArrayList<String>();
    private final Map<String, Long> duplicatedContactIds = new HashMap<String, Long>();
    private AbstractClient client = null;
    private ExportPool pool = null;
    private long progress = 0;
    private long totalProgress = 0;
    private boolean useNewEnergySourceEnev2014Values = true;

    /**
     * Create a new {@link ExportHandler}.
     */
    public ExportHandler() {
    }

    /**
     * Calback method to track progress during the export process.
     * <p>
     * This method may be overridden by inheriting classes in order to track the
     * progress of the export process.
     *
     * @param value
     * value of additional progress
     */
    protected final void addProgress(long value) {
        this.setProgress(this.progress + Math.abs(value));
    }

    /**
     * Callback method to check, if a contact should be updated in the export process.
     *
     * @param contact
     * contact person to update
     *
     * @param poolContactId
     * id of the contact person within export pool
     *
     * @return
     * true, if the contact person can be ignored in the export process
     */
    protected boolean canIgnoreContact(RealtorContactDetails contact, String poolContactId) {
        return false;
    }

    /**
     * Callback method to check, if a real estate should be updated in the export process.
     *
     * @param object
     * real estate to update
     *
     * @param poolObjectId
     * id of the real estate within export pool
     *
     * @return
     * true, if the real estate can be ignored in the export process
     */
    protected boolean canIgnoreObject(RealEstate object, String poolObjectId) {
        return false;
    }

    /**
     * Archivate a real estate object at the Webservice.
     *
     * @param externalObjectId
     * external real estate ID
     *
     * @throws IOException
     * if the operation failed
     */
    protected final void doArchiveObject(String externalObjectId) throws IOException {
        // Immobilie ermitteln
        final RealEstate is24Object;
        try {
            is24Object = ImportExport.RealEstateService.getByExternalId(this.client, externalObjectId);
            if (is24Object == null) {
                this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_NOT_FOUND_FOR_ARCHIVING,
                        "Property '" + externalObjectId + "' is not available anymore at the Webservice!");
                return;
            }
        } catch (JAXBException ex) {
            throw new IOExceptionWithCause("Can't read / write XML while communicating with the Webservice!", ex);
        } catch (OAuthException ex) {
            throw new IOExceptionWithCause("Authorization failed!", ex);
        } catch (RequestFailedException ex) {
            LOGGER.error("Can't get property '" + externalObjectId + "' from the Webservice!");
            if (ex.requestRefNumber != null)
                LOGGER.error("> referring request: " + ex.requestRefNumber);
            logMessagesAsError(ex.responseMessages);
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_NOT_FOUND_FOR_ARCHIVING, ex);
            return;
        }
        this.doArchiveObject(is24Object);
    }

    /**
     * Archivate a real estate object at the Webservice.
     *
     * @param is24ObjectId
     * real estate ID by IS24
     *
     * @param externalObjectId
     * external real estate ID
     *
     * @throws IOException
     * if the operation failed
     */
    protected final void doArchiveObject(long is24ObjectId, String externalObjectId) throws IOException {
        // Immobilie ermitteln
        final RealEstate is24Object;
        try {
            is24Object = ImportExport.RealEstateService.getByIs24Id(this.client, is24ObjectId);
            if (is24Object == null) {
                if (!StringUtils.isBlank(externalObjectId)) {
                    this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_NOT_FOUND_FOR_ARCHIVING,
                            "Property '" + externalObjectId + "' is not available anymore at the Webservice!");
                } else {
                    this.putGeneralMessage(ExportMessage.Code.OBJECT_NOT_FOUND_FOR_ARCHIVING,
                            "Property (" + is24ObjectId + ") is not available anymore at the Webservice!");
                }
                return;
            }
        } catch (JAXBException ex) {
            throw new IOExceptionWithCause("Can't read / write XML while communicating with the Webservice!", ex);
        } catch (OAuthException ex) {
            throw new IOExceptionWithCause("Authorization failed!", ex);
        } catch (RequestFailedException ex) {
            if (!StringUtils.isBlank(externalObjectId)) {
                LOGGER.error("Can't get property '" + externalObjectId + "' (" + is24ObjectId
                        + ") from the Webservice!");
                if (ex.requestRefNumber != null)
                    LOGGER.error("> referring request: " + ex.requestRefNumber);
                logMessagesAsError(ex.responseMessages);
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_NOT_FOUND_FOR_ARCHIVING, ex);
            } else {
                LOGGER.error("Can't get property (" + is24ObjectId + ") from the Webservice!");
                if (ex.requestRefNumber != null)
                    LOGGER.error("> referring request: " + ex.requestRefNumber);
                logMessagesAsError(ex.responseMessages);
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                this.putGeneralMessage(ExportMessage.Code.OBJECT_NOT_FOUND_FOR_ARCHIVING, ex);
            }
            return;
        }
        this.doArchiveObject(is24Object);
    }

    /**
     * Archivate a real estate object at the Webservice.
     *
     * @param is24Object
     * real estate to archivate
     *
     * @throws IOException
     * if the operation failed
     */
    protected void doArchiveObject(RealEstate is24Object) throws IOException {
        try {
            final Long is24ObjectId = is24Object.getId();
            final String externalObjectId = StringUtils.trimToNull(is24Object.getExternalId());

            // aktuelle Verffentlichungen zur Immobilie ermitteln
            PublishObjects is24Publishings = null;
            try {
                is24Publishings = ImportExport.PublishService.get(this.client, is24ObjectId, 0);
            } catch (RequestFailedException ex) {
                if (!StringUtils.isBlank(externalObjectId)) {
                    LOGGER.error("Can't get publishings of property '" + externalObjectId + "' (" + is24ObjectId
                            + ") from the Webservice!");
                    if (ex.requestRefNumber != null)
                        LOGGER.error("> referring request: " + ex.requestRefNumber);
                    logMessagesAsError(ex.responseMessages);
                    LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                    this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_PUBLISHINGS_NOT_FOUND, ex);
                } else {
                    LOGGER.error("Can't get publishings of property (" + is24ObjectId + ") from the Webservice!");
                    if (ex.requestRefNumber != null)
                        LOGGER.error("> referring request: " + ex.requestRefNumber);
                    logMessagesAsError(ex.responseMessages);
                    LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                    this.putGeneralMessage(ExportMessage.Code.OBJECT_PUBLISHINGS_NOT_FOUND, ex);
                }
            }

            // keine Verffentlichungen gefunden
            if (is24Publishings == null) {
                if (!StringUtils.isBlank(externalObjectId)) {
                    this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_PUBLISHINGS_NOT_FOUND,
                            "Can't get publishings of property '" + externalObjectId + "' (" + is24ObjectId
                                    + ") from the Webservice!");
                } else {
                    this.putGeneralMessage(ExportMessage.Code.OBJECT_PUBLISHINGS_NOT_FOUND,
                            "Can't get publishings of property (" + is24ObjectId + ") from the Webservice!");
                }
            }

            // ggf. Verffentlichungen der Immobilie entfernen
            else {
                for (PublishObject publishing : is24Publishings.getPublishObject()) {
                    Long is24ChannelId = publishing.getPublishChannel().getId();
                    String publishId = StringUtils.trimToNull(publishing.getId());
                    try {
                        ImportExport.PublishService.delete(this.client, publishId);
                    } catch (RequestFailedException ex) {
                        if (!StringUtils.isBlank(externalObjectId)) {
                            LOGGER.error("Can't unpublish property '" + externalObjectId + "' (" + is24ObjectId
                                    + ") " + "in channel '" + publishing.getPublishChannel().getTitle() + "' ("
                                    + is24ChannelId + ") at the Webservice!");
                            if (ex.requestRefNumber != null)
                                LOGGER.error("> referring request: " + ex.requestRefNumber);
                            logMessagesAsError(ex.responseMessages);
                            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                            this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_NOT_UNPUBLISHED, ex);
                        } else {
                            LOGGER.error("Can't unpublish property (" + is24ObjectId + ") " + "in channel '"
                                    + publishing.getPublishChannel().getTitle() + "' (" + is24ChannelId
                                    + ") at the Webservice!");
                            logMessagesAsError(ex.responseMessages);
                            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                            this.putGeneralMessage(ExportMessage.Code.OBJECT_NOT_UNPUBLISHED, ex);
                        }
                    }
                }
            }
        } catch (JAXBException ex) {
            throw new IOExceptionWithCause("Can't read / write XML while communicating with the Webservice!", ex);
        } catch (OAuthException ex) {
            throw new IOExceptionWithCause("Authorization failed!", ex);
        } catch (IOException ex) {
            throw new IOExceptionWithCause("Communication failed!", ex);
        }
    }

    /**
     * Download an {@link URL} into a {@link File}.
     *
     * @param url
     * URL to download
     *
     * @return
     * downloaded file
     *
     * @throws IOException
     * if the operation failed
     */
    protected File doDownloadFile(URL url) throws IOException {
        if (url == null)
            return null;
        LOGGER.info("downloading " + url);
        InputStream input = null;
        OutputStream output = null;
        try {
            input = url.openStream();
            File tempFile = File.createTempFile("is24-export-attachment-", ".bin");
            tempFile.deleteOnExit();
            output = new FileOutputStream(tempFile);
            IOUtils.copy(input, output);
            output.flush();
            return tempFile;
        } finally {
            IOUtils.closeQuietly(output);
            IOUtils.closeQuietly(input);
        }
    }

    /**
     * Return internal and external ID's of published real estates from the
     * Webservice, that were not changed during the current export process.
     *
     * @return
     * mapping of internal and external ID's of untouched real estates
     *
     * @throws IOException
     * if the operation failed
     */
    protected Map<Long, String> doListUntouchedObjects() throws IOException {
        try {
            final Map<Long, String> ids = new TreeMap<Long, String>();

            // Immobilien im Bestand ermitteln
            int page = 1;
            while (true) {
                RealEstates is24Objects;
                try {
                    is24Objects = ImportExport.RealEstateService.getAll(this.client, null, null, 0, page, false);
                    if (is24Objects == null) {
                        this.putGeneralMessage(ExportMessage.Code.OBJECTS_NOT_FOUND,
                                "Can't get available properties from the Webservice!");
                        return new HashMap<Long, String>();
                    }
                } catch (RequestFailedException ex) {
                    LOGGER.error("Can't get available properties from the Webservice!");
                    if (ex.requestRefNumber != null)
                        LOGGER.error("> referring request: " + ex.requestRefNumber);
                    logMessagesAsError(ex.responseMessages);
                    LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                    this.putGeneralMessage(ExportMessage.Code.OBJECTS_NOT_FOUND, ex);
                    return new HashMap<Long, String>();
                }

                Long totalPages = is24Objects.getPaging().getNumberOfPages();
                if (totalPages == null)
                    totalPages = 0L;

                RealEstateList is24ObjectList = is24Objects.getRealEstateList();
                if (is24ObjectList != null && !is24ObjectList.getRealEstateElement().isEmpty()) {
                    for (OfferRealEstateForList is24Object : is24ObjectList.getRealEstateElement()) {
                        // Immobilien nur lschen,
                        // wenn diese als "aktiv" bei IS24 markiert sind.
                        //
                        // TODO: Es ist hier nicht ganz klar, aus welchem XML-Element
                        // der Immobilien-Status zu ermitteln ist.
                        boolean isActive = RealEstateState.ACTIVE.equals(is24Object.getState())
                                || RealEstateState.ACTIVE.equals(is24Object.getRealEstateState());
                        if (!isActive)
                            continue;

                        // Immobilien nur zurckliefern,
                        // wenn diese beim Exportvorgang nicht verndert wurden.
                        long is24ObjectId = is24Object.getId();
                        String externalObjectId = is24Object.getExternalId();
                        if (!ids.containsKey(is24ObjectId) && !this.pool.hasObjectForExport(externalObjectId)) {
                            ids.put(is24ObjectId, externalObjectId);
                        }
                    }
                }
                if (page >= totalPages)
                    break;
                page++;
            }

            return ids;
        } catch (JAXBException ex) {
            throw new IOExceptionWithCause("Can't read / write XML while communicating with the Webservice!", ex);
        } catch (OAuthException ex) {
            throw new IOExceptionWithCause("Authorization failed!", ex);
        } catch (IOException ex) {
            throw new IOExceptionWithCause("Communication failed!", ex);
        }
    }

    /**
     * Return publish channels for the authorized agency.
     *
     * @return
     * publish channels
     *
     * @throws IOException
     * if the operation failed
     */
    protected PublishChannels doLoadPublishChannels() throws IOException {
        try {
            return ImportExport.PublishChannelService.get(this.client);
        } catch (RequestFailedException ex) {
            LOGGER.error("Can't get publish channels from the Webservice!");
            if (ex.requestRefNumber != null)
                LOGGER.error("> referring request: " + ex.requestRefNumber);
            logMessagesAsError(ex.responseMessages);
            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
            this.putGeneralMessage(ExportMessage.Code.PUBLISH_CHANNELS_NOT_FOUND, ex);
            return null;
        } catch (JAXBException ex) {
            //LOGGER.error( "Can't read / write XML while communicating with the Webservice!" );
            //LOGGER.error( "> " + ex.getLocalizedMessage(), ex );
            throw new IOExceptionWithCause("Can't read / write XML while communicating with the Webservice!", ex);
        } catch (OAuthException ex) {
            //LOGGER.error( "Can't authorize at the Webservice!" );
            //LOGGER.error( "> " + ex.getLocalizedMessage(), ex );
            throw new IOExceptionWithCause("Authorization failed!", ex);
        } catch (IOException ex) {
            //LOGGER.error( "Can't communicate with the Webservice!" );
            //LOGGER.error( "> " + ex.getLocalizedMessage(), ex );
            throw new IOExceptionWithCause("Communication failed!", ex);
        }
    }

    /**
     * Publish a real estate at the Webservice.
     *
     * @param is24ObjectId
     * real estate ID by IS24
     *
     * @param externalObjectId
     * external real estate ID
     *
     * @param is24PublishChannels
     * channels, where the real estate should be published
     *
     * @throws IOException
     * if the operation failed
     */
    protected void doPublishObject(long is24ObjectId, String externalObjectId, PublishChannels is24PublishChannels)
            throws IOException {
        final org.openestate.is24.restapi.xml.common.ObjectFactory commonFactory = new org.openestate.is24.restapi.xml.common.ObjectFactory();

        try {
            // derzeitige Verffentlichungen zur Immobilie ermitteln
            final List<Long> is24PublishedChannels = new ArrayList<Long>();
            try {
                PublishObjects is24Publishings = ImportExport.PublishService.get(this.client, is24ObjectId, 0);
                if (is24Publishings != null) {
                    for (PublishObject is24Publishing : is24Publishings.getPublishObject()) {
                        is24PublishedChannels.add(is24Publishing.getPublishChannel().getId());
                    }
                }
            } catch (RequestFailedException ex) {
                LOGGER.error("Can't get publishings of property '" + externalObjectId + "' (" + is24ObjectId
                        + ") from the Webservice!");
                if (ex.requestRefNumber != null)
                    LOGGER.error("> referring request: " + ex.requestRefNumber);
                logMessagesAsError(ex.responseMessages);
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_PUBLISHINGS_NOT_FOUND, ex);
            }

            if (is24PublishChannels == null) {
                this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_NOT_PUBLISHED,
                        "No channels for publishing found!");
            } else {
                // Verffentlichungen zur Immobilie aktualisieren,
                // wenn diese zu einem Kanal noch nicht gesetzt wurde
                for (PublishChannel is24Channel : is24PublishChannels.getPublishChannel()) {
                    Long is24ChannelId = is24Channel.getId();
                    if (is24PublishedChannels.contains(is24ChannelId))
                        continue;

                    PublishObject is24Publishing = commonFactory.createPublishObject();
                    is24Publishing.setPublishChannel(is24Channel);
                    is24Publishing.setRealEstate(commonFactory.createPublishObjectRealEstate());
                    is24Publishing.getRealEstate().setId(is24ObjectId);

                    try {
                        ImportExport.PublishService.post(this.client, is24Publishing);
                    } catch (RequestFailedException ex) {
                        LOGGER.error("Can't publish property '" + externalObjectId + "' (" + is24ObjectId + ") "
                                + "in channel '" + is24Channel.getTitle() + "' (" + is24ChannelId + ")!");
                        if (ex.requestRefNumber != null)
                            LOGGER.error("> referring request: " + ex.requestRefNumber);
                        logMessagesAsError(ex.responseMessages);
                        LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                        this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_NOT_PUBLISHED, ex);
                    }
                }
            }
        } catch (JAXBException ex) {
            //LOGGER.error( "Can't read / write XML while communicating with the Webservice!" );
            //LOGGER.error( "> " + ex.getLocalizedMessage(), ex );
            throw new IOExceptionWithCause("Can't read / write XML while communicating with the Webservice!", ex);
        } catch (OAuthException ex) {
            //LOGGER.error( "Can't authorize at the Webservice!" );
            //LOGGER.error( "> " + ex.getLocalizedMessage(), ex );
            throw new IOExceptionWithCause("Authorization failed!", ex);
        } catch (IOException ex) {
            //LOGGER.error( "Can't communicate with the Webservice!" );
            //LOGGER.error( "> " + ex.getLocalizedMessage(), ex );
            throw new IOExceptionWithCause("Communication failed!", ex);
        }
    }

    /**
     * Remove a real estate object from the Webservice.
     *
     * @param externalObjectId
     * external real estate ID
     *
     * @throws IOException
     * if the operation failed
     */
    protected void doRemoveObject(String externalObjectId) throws IOException {
        try {
            // Lschung durchfhren
            try {
                ImportExport.RealEstateService.deleteByExternalId(this.client, externalObjectId);
            } catch (RequestFailedException ex) {
                LOGGER.error("Can't delete property '" + externalObjectId + "' at the Webservice!");
                if (ex.requestRefNumber != null)
                    LOGGER.error("> referring request: " + ex.requestRefNumber);
                logMessagesAsError(ex.responseMessages);
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_NOT_REMOVED, ex);
            }
        } catch (JAXBException ex) {
            throw new IOExceptionWithCause("Can't read / write XML while communicating with the Webservice!", ex);
        } catch (OAuthException ex) {
            throw new IOExceptionWithCause("Authorization failed!", ex);
        } catch (IOException ex) {
            throw new IOExceptionWithCause("Communication failed!", ex);
        }
    }

    /**
     * Remove a real estate object from the Webservice.
     *
     * @param is24ObjectId
     * real estate ID by IS24
     *
     * @param externalObjectId
     * external real estate ID
     *
     * @throws IOException
     * if the operation failed
     */
    protected void doRemoveObject(long is24ObjectId, String externalObjectId) throws IOException {
        try {
            // Lschung durchfhren
            try {
                ImportExport.RealEstateService.deleteByIs24Id(this.client, is24ObjectId);
            } catch (RequestFailedException ex) {
                if (!StringUtils.isBlank(externalObjectId)) {
                    LOGGER.error("Can't delete property '" + externalObjectId + "' (" + is24ObjectId
                            + ") at the Webservice!");
                    if (ex.requestRefNumber != null)
                        LOGGER.error("> referring request: " + ex.requestRefNumber);
                    logMessagesAsError(ex.responseMessages);
                    LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                    this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_NOT_REMOVED, ex);
                } else {
                    LOGGER.error("Can't delete property (" + is24ObjectId + ") at the Webservice!");
                    if (ex.requestRefNumber != null)
                        LOGGER.error("> referring request: " + ex.requestRefNumber);
                    logMessagesAsError(ex.responseMessages);
                    LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                    this.putGeneralMessage(ExportMessage.Code.OBJECT_NOT_REMOVED, ex);
                }
            }
        } catch (JAXBException ex) {
            throw new IOExceptionWithCause("Can't read / write XML while communicating with the Webservice!", ex);
        } catch (OAuthException ex) {
            throw new IOExceptionWithCause("Authorization failed!", ex);
        } catch (IOException ex) {
            throw new IOExceptionWithCause("Communication failed!", ex);
        }
    }

    /**
     * Save a contact person to the Webservice.
     *
     * @param contact
     * contact to save
     *
     * @param poolContactId
     * contact ID within export pool
     *
     * @return
     * ID of the processed contact person in the Webservice or null, if the object
     * was not updated
     *
     * @throws IOException
     * if the operation failed
     */
    protected Long doUpdateContact(RealtorContactDetails contact, String poolContactId) throws IOException {
        final String externalContactId = contact.getExternalId();
        try {
            // prfen, ob ein Ansprechpartner mit der externen ID bereits im Webservice existiert
            final RealtorContactDetails oldIs24Contact;
            try {
                oldIs24Contact = ImportExport.ContactAddressService.getByExternalId(this.client, externalContactId);
            } catch (RequestFailedException ex) {
                LOGGER.error("Can't get contact person '" + externalContactId + "' from the Webservice!");
                if (ex.requestRefNumber != null)
                    LOGGER.error("> referring request: " + ex.requestRefNumber);
                logMessagesAsError(ex.responseMessages);
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                this.putContactMessage(externalContactId, ExportMessage.Code.CONTACT_NOT_FOUND, ex);
                return null;
            }

            Long is24ContactId;

            // neuen Ansprechpartner erstellen
            if (oldIs24Contact == null) {
                try {
                    is24ContactId = ImportExport.ContactAddressService.post(this.client, contact);
                } catch (RequestFailedException ex) {
                    Resource resource = Resource.getMessageResource(ex.responseMessages);
                    if (resource != null && "duplicated contactDetails".equalsIgnoreCase(resource.type)
                            && resource.id > 0) {
                        LOGGER.info("contact '" + externalContactId + "' is already "
                                + "available at the Webservice with ID " + resource.id + ".");
                        this.duplicatedContactIds.put(externalContactId, resource.id);
                        is24ContactId = resource.id;
                    } else {
                        LOGGER.error("Can't add contact person '" + externalContactId + "' to the Webservice!");
                        if (ex.requestRefNumber != null)
                            LOGGER.error("> referring request: " + ex.requestRefNumber);
                        logMessagesAsError(ex.responseMessages);
                        LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                        this.putContactMessage(externalContactId, ExportMessage.Code.CONTACT_NOT_SAVED, ex);
                        return null;
                    }
                }
            }

            // bestehenden Ansprechpartner aktualisieren
            else {
                is24ContactId = oldIs24Contact.getId();
                contact.setId(is24ContactId);

                try {
                    ImportExport.ContactAddressService.putByIs24Id(this.client, contact, is24ContactId);
                } catch (RequestFailedException ex) {
                    Resource resource = Resource.getMessageResource(ex.responseMessages);
                    if (resource != null && "duplicated contactDetails".equalsIgnoreCase(resource.type)
                            && resource.id > 0) {
                        LOGGER.info("contact '" + externalContactId + "' is already "
                                + "available at the Webservice with ID " + resource.id + ".");
                        this.duplicatedContactIds.put(externalContactId, resource.id);
                        return resource.id;
                    } else {
                        LOGGER.error("Can't update contact person '" + externalContactId + "' at the Webservice!");
                        if (ex.requestRefNumber != null)
                            LOGGER.error("> referring request: " + ex.requestRefNumber);
                        logMessagesAsError(ex.responseMessages);
                        LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                        this.putContactMessage(externalContactId, ExportMessage.Code.CONTACT_NOT_SAVED, ex);
                        return null;
                    }
                }
            }

            // ID des Ansprechpartners als erfolgreich exportiert vormerken
            this.savedContactIds.add(externalContactId);

            // ID des verarbeiteten Ansprechpartners bei IS24 zurckliefern
            return is24ContactId;
        } catch (JAXBException ex) {
            throw new IOExceptionWithCause("Can't read / write XML while communicating with the Webservice!", ex);
        } catch (OAuthException ex) {
            throw new IOExceptionWithCause("Authorization failed!", ex);
        } catch (IOException ex) {
            throw new IOExceptionWithCause("Communication failed!", ex);
        } finally {
            // Fortschritt protokollieren
            this.addProgress(this.pool.getContactSize(poolContactId, true));
        }
    }

    /**
     * Save a real estate to the Webservice.
     *
     * @param object
     * real estate to save
     *
     * @param poolObjectId
     * real estate ID within export pool
     *
     * @return
     * ID of the processed real estate in the Webservice or null, if the object
     * was not updated
     *
     * @throws IOException
     * if the operation failed
     */
    protected Long doUpdateObject(RealEstate object, String poolObjectId) throws IOException {
        final String externalObjectId = object.getExternalId();
        final org.openestate.is24.restapi.xml.realestates.ObjectFactory realEstatesFactory = new org.openestate.is24.restapi.xml.realestates.ObjectFactory();
        final org.openestate.is24.restapi.xml.attachmentsorder.ObjectFactory attachmentsorderFactory = new org.openestate.is24.restapi.xml.attachmentsorder.ObjectFactory();

        object.setRealEstateState(RealEstateState.ACTIVE);

        // Ansprechpartner zuweisen, wenn nicht bereits explizit eine intene
        // Ansprechpartner-ID hinterlegt wurde
        Long is24ContactId = (object.getContact() != null) ? object.getContact().getId() : null;
        if (is24ContactId == null || is24ContactId < 1) {
            String externalContactId = (object.getContact() != null)
                    ? StringUtils.trimToNull(object.getContact().getExternalId())
                    : null;

            // Duplikat des Ansprechpartners verwenden
            if (externalContactId != null && this.duplicatedContactIds.containsKey(externalContactId)) {
                is24ContactId = this.duplicatedContactIds.get(externalContactId);
                if (object.getContact() == null)
                    object.setContact(realEstatesFactory.createRealEstateContact());
                object.getContact().setId(is24ContactId);
                object.getContact().setExternalId(null);
            }

            // sicherstellen, dass der Ansprechpartner vorher erfolgreich whrend des Transports exportiert wurde
            else if (externalContactId != null && !this.savedContactIds.contains(externalContactId)) {
                this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_WITHOUT_CONTACT,
                        "The contact '" + externalContactId + "' was not saved during the export process.");
                object.setContact(null);
            }
        }

        try {
            // prfen, ob eine Immobilie mit der externen ID bereits im Webservice existiert
            RealEstate oldIs24Object;
            try {
                oldIs24Object = ImportExport.RealEstateService.getByExternalId(this.client, externalObjectId);
            } catch (RequestFailedException ex) {
                LOGGER.error("Can't get property '" + externalObjectId + "' from the Webservice!");
                if (ex.requestRefNumber != null)
                    LOGGER.error("> referring request: " + ex.requestRefNumber);
                logMessagesAsError(ex.responseMessages);
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_NOT_FOUND, ex);

                // Fortschritt protokollieren
                this.addProgress(this.pool.getObjectSize(poolObjectId, true));

                return null;
            }

            //Messages responseMessages;
            final Long is24ObjectId;

            // Immobilie im Portal lschen,
            // wenn diese bereits im Portal existiert
            // und einer anderen Rubrik zugewiesen ist
            if (oldIs24Object != null && !oldIs24Object.getClass().getName().equals(object.getClass().getName())) {
                //LOGGER.debug( "RUBRIK GENDERT" );
                //LOGGER.debug( "> fr Immobilie #" + oldIs24Object.getId() );
                //LOGGER.debug( "> alte Rubrik " + oldIs24Object.getClass().getName() );
                //LOGGER.debug( "> neue Rubrik " + object.getClass().getName() );
                try {
                    ImportExport.RealEstateService.deleteByIs24Id(this.client, oldIs24Object.getId());
                    oldIs24Object = null;
                } catch (RequestFailedException ex) {
                    LOGGER.error("Can't delete property '" + externalObjectId + "' (" + oldIs24Object.getId()
                            + ") at the Webservice!");
                    if (ex.requestRefNumber != null)
                        LOGGER.error("> referring request: " + ex.requestRefNumber);
                    logMessagesAsError(ex.responseMessages);
                    LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                    this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_NOT_SAVED, ex);

                    // Fortschritt protokollieren
                    this.addProgress(this.pool.getObjectSize(poolObjectId, true));

                    return null;
                }
            }

            // neue Immobilie erstellen
            if (oldIs24Object == null) {
                try {
                    is24ObjectId = ImportExport.RealEstateService.post(this.client, object,
                            this.isUseNewEnergySourceEnev2014Values());
                    //LOGGER.debug( "created object with IS24-ID #" + is24ObjectId );
                } catch (RequestFailedException ex) {
                    LOGGER.error("Can't add property '" + externalObjectId + "' to the Webservice!");
                    if (ex.requestRefNumber != null)
                        LOGGER.error("> referring request: " + ex.requestRefNumber);
                    logMessagesAsError(ex.responseMessages);
                    LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                    this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_NOT_SAVED, ex);

                    // Fortschritt protokollieren
                    this.addProgress(this.pool.getObjectSize(poolObjectId, true));

                    return null;
                }
            }

            // bestehende Immobilie aktualisieren
            else {
                is24ObjectId = oldIs24Object.getId();
                object.setId(is24ObjectId);

                try {
                    ImportExport.RealEstateService.putByIs24Id(this.client, object, is24ObjectId,
                            this.isUseNewEnergySourceEnev2014Values());
                    //LOGGER.debug( "updated object with IS24-ID #" + is24ObjectId );
                } catch (RequestFailedException ex) {
                    LOGGER.error("Can't update property '" + externalObjectId + "' (" + is24ObjectId
                            + ") at the Webservice!");
                    if (ex.requestRefNumber != null)
                        LOGGER.error("> referring request: " + ex.requestRefNumber);
                    logMessagesAsError(ex.responseMessages);
                    LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                    this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_NOT_SAVED, ex);

                    // Fortschritt protokollieren
                    this.addProgress(this.pool.getObjectSize(poolObjectId, true));

                    return null;
                }
            }

            // Fortschritt protokollieren
            this.addProgress(this.pool.getObjectSize(poolObjectId, false));

            // bestehende Anhnge / Web-Links ermitteln
            final Map<String, Attachment> oldIs24Attachments = new HashMap<String, Attachment>();
            boolean ignoreAttachments = false;
            try {
                Attachments attachments = ImportExport.AttachmentService.getAll(this.client, externalObjectId);
                if (attachments != null && !attachments.getAttachment().isEmpty()) {
                    for (Attachment attachment : attachments.getAttachment()) {
                        Long is24AttachmentId = attachment.getId();
                        String externalAttachmentId = StringUtils.trimToNull(attachment.getExternalId());

                        // Anhang bernehmen, wenn ein Hashwert hinterlegt ist
                        // und dieser noch nicht bernommen wurde
                        if (externalAttachmentId != null && !oldIs24Attachments.containsKey(externalAttachmentId)) {
                            //LOGGER.debug( "> found old attachment #" + is24AttachmentId + " / " + externalAttachmentId + " / " + externalAttachmentId.length() );
                            oldIs24Attachments.put(externalAttachmentId, attachment);
                            continue;
                        }

                        // alten Anhang entfernen
                        try {
                            //LOGGER.debug( "> removing old attachment #" + is24AttachmentId + " without external id" );
                            ImportExport.AttachmentService.deleteById(this.client, externalObjectId,
                                    is24AttachmentId);
                        } catch (RequestFailedException ex) {
                            LOGGER.error("Can't remove old attachment (" + is24AttachmentId + ") " + "of property '"
                                    + externalObjectId + "' (" + is24ObjectId + ") from the Webservice!");
                            if (ex.requestRefNumber != null)
                                LOGGER.error("> referring request: " + ex.requestRefNumber);
                            logMessagesAsError(ex.responseMessages);
                            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                            this.putObjectMessage(externalObjectId,
                                    ExportMessage.Code.OBJECT_OLD_ATTACHMENT_NOT_REMOVED, ex);
                        }
                    }
                }
            } catch (RequestFailedException ex) {
                LOGGER.error("Can't get attachments of property '" + externalObjectId + "' (" + is24ObjectId
                        + ") from the Webservice!");
                if (ex.requestRefNumber != null)
                    LOGGER.error("> referring request: " + ex.requestRefNumber);
                logMessagesAsError(ex.responseMessages);
                LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_WITHOUT_ATTACHMENTS, ex);
                ignoreAttachments = true;
            }

            if (ignoreAttachments) {
                // Fortschritt protokollieren
                long totalAttachmentSize = this.pool.getObjectSize(poolObjectId, true)
                        - this.pool.getObjectSize(poolObjectId, false);
                this.addProgress(totalAttachmentSize);
            }

            else {
                // Anhnge zur bertragung ermitteln und zugehrige Hash-Werte berechnen
                List<String> attachmentHashes = new ArrayList<String>();
                Map<String, Attachment> attachments = new TreeMap<String, Attachment>(new AlphanumComparator());
                Map<String, File> attachmentFiles = new HashMap<String, File>();
                for (String attachmentKey : this.pool.getObjectAttachmentIds(poolObjectId)) {
                    Attachment is24Attachment = this.pool.getObjectAttachment(poolObjectId, attachmentKey);
                    if (is24Attachment == null) {
                        LOGGER.error("Can't read the XML file for attachment!");
                        this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_ATTACHMENT_NOT_SAVED,
                                "Can't read the XML file for attachment!");

                        // Fortschritt protokollieren
                        this.addProgress(this.pool.getObjectAttachmentSize(poolObjectId, attachmentKey));
                        continue;
                    }

                    // Anhang als Web-Link verarbeiten
                    if (is24Attachment instanceof Link) {
                        Link link = (Link) is24Attachment;

                        // Hashwert zur Identifizierung des Anhangs errechnen
                        URL url = link.getUrl();
                        String externalAttachmentId = (url != null)
                                ? DigestUtils.sha1Hex(is24ObjectId + "-" + url.toString())
                                : DigestUtils.sha1Hex(is24ObjectId + "-" + attachmentKey);

                        // Sicherstellen, dass der gleiche Anhang nicht mehrfach hochgeladen wird
                        if (attachmentHashes.contains(externalAttachmentId))
                            continue;
                        attachmentHashes.add(externalAttachmentId);

                        link.setExternalId(externalAttachmentId);
                    }

                    // Anhang als Datei verarbeiten
                    else {
                        // Datei ermitteln
                        File attachFile = this.pool.getObjectAttachmentFile(poolObjectId, is24Attachment);

                        // ggf. Datei herunterladen, wenn noch nicht im Pool hinterlegt
                        if (attachFile == null) {
                            URL attachUrl = this.pool.getObjectAttachmentURL(poolObjectId, attachmentKey);
                            if (attachUrl != null) {
                                try {
                                    attachFile = doDownloadFile(attachUrl);
                                } catch (Exception ex) {
                                    LOGGER.warn("Can't download file from URL: " + attachUrl);
                                    LOGGER.warn("> " + ex.getLocalizedMessage(), ex);
                                }
                            }
                        }

                        if (attachFile == null || !attachFile.isFile()) {
                            LOGGER.error("Can't find file for attachment!");
                            this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_ATTACHMENT_NOT_SAVED,
                                    "Can't find file for attachment!");

                            // Fortschritt protokollieren
                            this.addProgress(this.pool.getObjectAttachmentSize(poolObjectId, attachmentKey));

                            continue;
                        }

                        // Datei des Anhangs vormerken
                        attachmentFiles.put(attachmentKey, attachFile.getAbsoluteFile());

                        // Hashwert zur Identifizierung des Anhangs errechnen
                        final String externalAttachmentId;
                        InputStream input = null;
                        try {
                            input = new FileInputStream(attachFile);
                            String attachFileHash = DigestUtils.sha1Hex(input);
                            externalAttachmentId = DigestUtils.sha1Hex(is24ObjectId + "-" + attachFileHash);

                            // Sicherstellen, dass der gleiche Anhang nicht mehrfach hochgeladen wird
                            if (attachmentHashes.contains(externalAttachmentId))
                                continue;
                            attachmentHashes.add(externalAttachmentId);

                            is24Attachment.setExternalId(externalAttachmentId);
                        } finally {
                            IOUtils.closeQuietly(input);
                        }
                    }

                    attachments.put(attachmentKey, is24Attachment);
                }

                // alte Anhnge entfernen
                String[] oldIs24AttachmentIds = oldIs24Attachments.keySet()
                        .toArray(new String[oldIs24Attachments.size()]);
                for (String oldIs24AttachmentId : oldIs24AttachmentIds) {
                    if (attachmentHashes.contains(oldIs24AttachmentId))
                        continue;
                    Attachment is24Attachment = oldIs24Attachments.remove(oldIs24AttachmentId);
                    Long is24AttachmentId = is24Attachment.getId();
                    try {
                        //LOGGER.debug( "> removing old attachment #" + is24AttachmentId );
                        ImportExport.AttachmentService.deleteById(this.client, externalObjectId, is24AttachmentId);
                    } catch (RequestFailedException ex) {
                        LOGGER.error("Can't remove old attachment (" + is24AttachmentId + ") " + "of property '"
                                + externalObjectId + "' (" + is24ObjectId + ") from the Webservice!");
                        if (ex.requestRefNumber != null)
                            LOGGER.error("> referring request: " + ex.requestRefNumber);
                        logMessagesAsError(ex.responseMessages);
                        LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                        this.putObjectMessage(externalObjectId,
                                ExportMessage.Code.OBJECT_OLD_ATTACHMENT_NOT_REMOVED, ex);
                    }
                }

                // Anhnge aus dem Exportverzeichnis der Immobilie ermitteln
                Map<Integer, Long> attachmentsOrder = new TreeMap<Integer, Long>();
                for (Map.Entry<String, Attachment> entry : attachments.entrySet()) {
                    final String attachmentKey = entry.getKey();
                    final Attachment is24Attachment = entry.getValue();
                    final String externalAttachmentId = is24Attachment.getExternalId();

                    int pos;
                    try {
                        pos = Math.abs(Integer.parseInt(attachmentKey));
                    } catch (NumberFormatException ex) {
                        LOGGER.warn("Can't read attachment position!");
                        LOGGER.warn("> " + ex.getLocalizedMessage(), ex);
                        pos = 999;
                    }

                    // Anhang als Web-Link verarbeiten
                    if (is24Attachment instanceof Link) {
                        Link link = (Link) is24Attachment;
                        try {
                            // zuvor gespeicherten Web-Link mit gleichem Hashwert aktualisieren
                            if (oldIs24Attachments.containsKey(externalAttachmentId)) {
                                Attachment oldAttachment = oldIs24Attachments.get(externalAttachmentId);
                                long is24AttachmentId = oldAttachment.getId();
                                //LOGGER.debug( "> updating attached link #" + is24AttachmentId );
                                //LOGGER.debug( ">> " + externalAttachmentId + " / " + externalAttachmentId.length() );
                                ImportExport.AttachmentService.putById(client, is24ObjectId, is24AttachmentId,
                                        link);
                                oldIs24Attachments.remove(externalAttachmentId);
                            }
                            // neuen Web-Link erzeugen
                            else {
                                //LOGGER.debug( "> adding attached link" );
                                //LOGGER.debug( ">> " + externalAttachmentId + " / " + externalAttachmentId.length() );
                                ImportExport.AttachmentService.post(this.client, externalObjectId, is24Attachment,
                                        null, null, null);
                            }
                        } catch (RequestFailedException ex) {
                            LOGGER.error("Can't save attachment of property '" + externalObjectId + "' ("
                                    + is24ObjectId + ") to the Webservice!");
                            if (ex.requestRefNumber != null)
                                LOGGER.error("> referring request: " + ex.requestRefNumber);
                            logMessagesAsError(ex.responseMessages);
                            LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                            this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_ATTACHMENT_NOT_SAVED,
                                    ex);
                        }

                        // Fortschritt protokollieren
                        this.addProgress(this.pool.getObjectAttachmentSize(poolObjectId, attachmentKey));

                        continue;
                    }

                    // Datei ermitteln
                    File attachFile = attachmentFiles.get(attachmentKey);
                    if (attachFile == null || !attachFile.isFile()) {
                        LOGGER.error("Can't find file for attachment!");
                        this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_ATTACHMENT_NOT_SAVED,
                                "Can't find file for attachment!");

                        // Fortschritt protokollieren
                        this.addProgress(this.pool.getObjectAttachmentSize(poolObjectId, attachmentKey));

                        continue;
                    }

                    // Name und Gre des Dateianhangs ermitteln
                    final String attachFileName = attachFile.getName();
                    final long attachFileSize = attachFile.length();

                    InputStream attachFileInput = null;
                    try {
                        // zuvor gespeicherten Anhang mit gleichem Hashwert aktualisieren
                        if (oldIs24Attachments.containsKey(externalAttachmentId)) {
                            Attachment oldAttachment = oldIs24Attachments.get(externalAttachmentId);
                            long is24AttachmentId = oldAttachment.getId();
                            //LOGGER.debug( "> updating attached file #" + is24AttachmentId );
                            //LOGGER.debug( ">> " + externalAttachmentId + " / " + externalAttachmentId.length() );
                            ImportExport.AttachmentService.putById(client, is24ObjectId, is24AttachmentId,
                                    is24Attachment);
                            oldIs24Attachments.remove(externalAttachmentId);

                            // Sortierung des Anhangs vormerken
                            if (!StreamingVideo.class.isInstance(is24Attachment)) {
                                while (attachmentsOrder.containsKey(pos)) {
                                    pos++;
                                }
                                attachmentsOrder.put(pos, is24AttachmentId);
                                //LOGGER.debug( "untouched attachment #" + is24AttachmentId + " (" + StringUtils.trimToEmpty( is24Attachment.getTitle() ) + ") at " + pos );
                            }
                        }
                        // neuen Anhang erzeugen
                        else {
                            //LOGGER.debug( "> adding attached file" );
                            //LOGGER.debug( ">> " + externalAttachmentId + " / " + externalAttachmentId.length() );

                            // MIME-Type des Dateianhangs ermitteln
                            final String attachFileMimeType;
                            if (is24Attachment instanceof PDFDocument) {
                                attachFileMimeType = "application/pdf";
                            } else if (is24Attachment instanceof Picture) {
                                if (attachFileName.toLowerCase().endsWith(".png"))
                                    attachFileMimeType = "image/png";
                                else if (attachFileName.toLowerCase().endsWith(".gif"))
                                    attachFileMimeType = "image/gif";
                                else
                                    attachFileMimeType = "image/jpeg";
                            }
                            //else if (is24Attachment instanceof VideoFile)
                            //{
                            //  mimeType = "application/octet-stream";
                            //}
                            else {
                                //mimeType = "application/octet-stream";
                                attachFileMimeType = null;
                            }

                            attachFileInput = new FileInputStream(attachFile);

                            // Video auf separaten Webservice bertragen
                            if (is24Attachment instanceof StreamingVideo) {
                                // Videodatei via UploadService bertragen
                                //LOGGER.debug( "UPLOAD STREAMING VIDEO '" + attachFileName + "'" );
                                String videoId = ImportExport.VideoUploadService.doVideoUpload(this.client,
                                        attachFileInput, attachFileName, attachFileSize);

                                // Anhang mit ID des bertragenen Videos zum Webservice senden
                                //LOGGER.debug( "POST STREAMING VIDEO WITH ID '" + videoId + "'" );
                                StreamingVideo streamingVideo = (StreamingVideo) is24Attachment;
                                streamingVideo.setVideoId(videoId);
                                ImportExport.AttachmentService.post(this.client, externalObjectId, streamingVideo,
                                        null, null, null);
                            }

                            // Anhang direkt bertragen
                            else {
                                long is24AttachmentId = ImportExport.AttachmentService.post(this.client,
                                        externalObjectId, is24Attachment, attachFileInput, attachFileName,
                                        attachFileMimeType);

                                // Sortierung des Anhangs vormerken
                                while (attachmentsOrder.containsKey(pos)) {
                                    pos++;
                                }
                                attachmentsOrder.put(pos, is24AttachmentId);
                                //LOGGER.debug( "new attachment #" + is24AttachmentId + " (" + StringUtils.trimToEmpty( is24Attachment.getTitle() ) + ") at " + pos );
                            }
                        }
                    } catch (RequestFailedException ex) {
                        LOGGER.error("Can't save attachment of property '" + externalObjectId + "' (" + is24ObjectId
                                + ") to the Webservice!");
                        if (ex.requestRefNumber != null)
                            LOGGER.error("> referring request: " + ex.requestRefNumber);
                        logMessagesAsError(ex.responseMessages);
                        LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                        this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_ATTACHMENT_NOT_SAVED, ex);
                    } finally {
                        IOUtils.closeQuietly(attachFileInput);

                        // Fortschritt protokollieren
                        this.addProgress(
                                this.pool.getObjectAttachmentSize(poolObjectId, attachmentKey) + attachFileSize);
                    }
                }

                // nicht aktualisierte Anhnge entfernen
                for (Attachment is24Attachment : oldIs24Attachments.values()) {
                    Long is24AttachmentId = is24Attachment.getId();
                    try {
                        //LOGGER.debug( "> removing untouched attachment #" + is24AttachmentId );
                        ImportExport.AttachmentService.deleteById(this.client, externalObjectId, is24AttachmentId);
                    } catch (RequestFailedException ex) {
                        LOGGER.error(
                                "Can't remove untouched attachment (" + is24AttachmentId + ") " + "of property '"
                                        + externalObjectId + "' (" + is24ObjectId + ") from the Webservice!");
                        if (ex.requestRefNumber != null)
                            LOGGER.error("> referring request: " + ex.requestRefNumber);
                        logMessagesAsError(ex.responseMessages);
                        LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                        this.putObjectMessage(externalObjectId,
                                ExportMessage.Code.OBJECT_OLD_ATTACHMENT_NOT_REMOVED, ex);
                    }
                }

                // Reihenfolge der Bild-Anhnge setzen
                if (!attachmentsOrder.isEmpty()) {
                    //LOGGER.debug( "update attachment order for property '" + externalObjectId + "' (" + is24ObjectId + ")" );
                    //LOGGER.debug( "> " + StringUtils.join( attachmentsOrder.values(), ", " ) );
                    org.openestate.is24.restapi.xml.attachmentsorder.List list = attachmentsorderFactory
                            .createList();
                    for (Long is24AttachmentId : attachmentsOrder.values()) {
                        list.getAttachmentId().add(is24AttachmentId);
                    }
                    try {
                        ImportExport.AttachmentsOrderService.put(this.client, externalObjectId, list);
                    } catch (RequestFailedException ex) {
                        LOGGER.error("Can't order attachments of property '" + externalObjectId + "' ("
                                + is24ObjectId + ")!");
                        if (ex.requestRefNumber != null)
                            LOGGER.error("> referring request: " + ex.requestRefNumber);
                        logMessagesAsError(ex.responseMessages);
                        LOGGER.error("> " + ex.getLocalizedMessage(), ex);
                        this.putObjectMessage(externalObjectId, ExportMessage.Code.OBJECT_UNORDERED_ATTACHMENTS,
                                ex);
                    }
                }
            }

            return is24ObjectId;
        } catch (JAXBException ex) {
            //LOGGER.error( "Can't read / write XML while communicating with the Webservice!" );
            //LOGGER.error( "> " + ex.getLocalizedMessage(), ex );
            throw new IOExceptionWithCause("Can't read / write XML while communicating with the Webservice!", ex);
        } catch (OAuthException ex) {
            //LOGGER.error( "Can't authorize at the Webservice!" );
            //LOGGER.error( "> " + ex.getLocalizedMessage(), ex );
            throw new IOExceptionWithCause("Authorization failed!", ex);
        } catch (IOException ex) {
            //LOGGER.error( "Can't communicate with the Webservice!" );
            //LOGGER.error( "> " + ex.getLocalizedMessage(), ex );
            throw new IOExceptionWithCause("Communication failed!", ex);
        }
    }

    /**
     * Start the bulk export of an {@link ExportPool}.
     *
     * @param client
     * client, that is used for transfers
     *
     * @param pool
     * pool with exportable data
     *
     * @param disableUnpublishedObjects
     * disable old real estates instead of removal
     *
     * @param unpublishUntouchedObjects
     * archivate or remove untouched real estates (means full transfer instead of
     * incremental)
     *
     * @return
     * messages, that occured during the export process
     *
     * @throws IOException
     * if the operation failed
     */
    public ExportMessage[] export(AbstractClient client, ExportPool pool, boolean disableUnpublishedObjects,
            boolean unpublishUntouchedObjects) throws IOException {
        this.client = client;
        this.pool = pool;
        this.messages.clear();
        this.savedContactIds.clear();
        this.duplicatedContactIds.clear();

        // init progress
        this.totalProgress = this.pool.getTotalSize();
        this.setProgress(0);

        // updating contacts
        Map<Long, String> is24ObjectIds = new HashMap<Long, String>();
        String[] ids = this.pool.getContactIds();
        if (!ArrayUtils.isEmpty(ids)) {
            LOGGER.info("updating contacts");
            int counter = 0;
            for (String poolContactId : ids) {
                counter++;

                // Ansprechpartner ermitteln
                final RealtorContactDetails contact = this.pool.getContact(poolContactId);
                if (contact == null) {
                    // Fortschritt protokollieren
                    this.addProgress(this.pool.getContactSize(poolContactId, true));

                    this.putGeneralMessage(ExportMessage.Code.XML_NOT_READABLE,
                            "Can't read XML file for contact '" + poolContactId + "'!");
                } else if (this.canIgnoreContact(contact, poolContactId)) {
                    LOGGER.info("[" + counter + " / " + ids.length + "] " + "ignoring contact '"
                            + contact.getExternalId() + "'");
                    this.savedContactIds.add(contact.getExternalId());

                    // Fortschritt protokollieren
                    this.addProgress(this.pool.getContactSize(poolContactId, true));
                } else {
                    LOGGER.info("[" + counter + " / " + ids.length + "] " + "updating contact '"
                            + contact.getExternalId() + "'");
                    this.doUpdateContact(contact, poolContactId);
                }
            }
        }

        // updating objects
        ids = this.pool.getObjectIds();
        if (!ArrayUtils.isEmpty(ids)) {
            LOGGER.info("updating objects");
            int counter = 0;
            for (String poolObjectId : ids) {
                counter++;

                // Immobilie  aus ExportPool ermitteln
                RealEstate object = this.pool.getObject(poolObjectId);
                if (object == null) {
                    // Fortschritt protokollieren
                    this.addProgress(this.pool.getObjectSize(poolObjectId, true));

                    this.putGeneralMessage(ExportMessage.Code.XML_NOT_READABLE,
                            "Can't read XML file for property '" + poolObjectId + "'!");
                } else if (this.canIgnoreObject(object, poolObjectId)) {
                    LOGGER.info("[" + counter + " / " + ids.length + "] " + "ignoring object '"
                            + object.getExternalId() + "'");

                    // Fortschritt protokollieren
                    this.addProgress(this.pool.getObjectSize(poolObjectId, true));
                } else {
                    LOGGER.info("[" + counter + " / " + ids.length + "] " + "updating object '"
                            + object.getExternalId() + "'");
                    Long is24ObjectId = this.doUpdateObject(object, poolObjectId);
                    if (is24ObjectId != null) {
                        is24ObjectIds.put(is24ObjectId, StringUtils.trimToNull(object.getExternalId()));
                    }
                }
            }
        }

        // removing objects
        ids = this.pool.getObjectIdsForRemoval();
        if (!ArrayUtils.isEmpty(ids)) {
            LOGGER.info("removing objects");
            int counter = 0;
            for (String externalObjectId : ids) {
                counter++;
                if (disableUnpublishedObjects) {
                    LOGGER.info("[" + counter + " / " + ids.length + "] " + "archiving object '" + externalObjectId
                            + "'");
                    this.doArchiveObject(externalObjectId);
                } else {
                    LOGGER.info("[" + counter + " / " + ids.length + "] " + "removing object '" + externalObjectId
                            + "'");
                    this.doRemoveObject(externalObjectId);
                }
            }
        }

        // removing untouched objects
        if (unpublishUntouchedObjects) {
            LOGGER.info("looking for untouched objects");
            Map<Long, String> untouchedObjectIds = this.doListUntouchedObjects();
            final int untouchedObjectCount = untouchedObjectIds.size();
            int counter = 0;
            for (Map.Entry<Long, String> entry : untouchedObjectIds.entrySet()) {
                counter++;
                long is24ObjectId = entry.getKey();
                String externalObjectId = entry.getValue();
                if (disableUnpublishedObjects) {
                    LOGGER.info("[" + counter + " / " + untouchedObjectCount + "] " + "archiving untouched object '"
                            + externalObjectId + "' (" + is24ObjectId + ")");
                    this.doArchiveObject(is24ObjectId, externalObjectId);
                } else {
                    LOGGER.info("[" + counter + " / " + untouchedObjectCount + "] " + "removing untouched object '"
                            + externalObjectId + "' (" + is24ObjectId + ")");
                    this.doRemoveObject(is24ObjectId, externalObjectId);
                }
            }
        }

        if (!is24ObjectIds.isEmpty()) {
            // load available publish channels
            LOGGER.info("loading publish channels");
            final PublishChannels publishChannels = doLoadPublishChannels();

            // publishing objects
            LOGGER.info("publishing objects");
            int counter = 0;
            for (Map.Entry<Long, String> entry : is24ObjectIds.entrySet()) {
                counter++;
                Long is24ObjectId = entry.getKey();
                String externalObjectId = entry.getValue();

                LOGGER.info("[" + counter + " / " + is24ObjectIds.size() + "] " + "publishing object '"
                        + externalObjectId + "' (" + is24ObjectId + ")");
                doPublishObject(is24ObjectId, externalObjectId, publishChannels);
            }
        }

        return this.getMessages();
    }

    /**
     * Return the client of the current export process.
     *
     * @return
     * client
     */
    protected final AbstractClient getClient() {
        return client;
    }

    /**
     * Return messages, that occured during the last export process.
     *
     * @return
     * messages
     */
    public final ExportMessage[] getMessages() {
        return this.messages.toArray(new ExportMessage[this.messages.size()]);
    }

    /**
     * Return messages for a certain contact person, that occured during the last
     * export process.
     *
     * @param externalContactId
     * external contact ID
     *
     * @return
     * messages
     */
    public final ExportMessage[] getMessagesForContact(String externalContactId) {
        externalContactId = StringUtils.trimToNull(externalContactId);
        if (externalContactId == null)
            return new ExportMessage[] {};
        List<ExportMessage> msgs = new ArrayList<ExportMessage>();
        for (ExportMessage msg : this.messages) {
            if (externalContactId.equals(msg.getContactId()))
                msgs.add(msg);
        }
        return msgs.toArray(new ExportMessage[msgs.size()]);
    }

    /**
     * Return messages for a certain real estate, that occured during the last
     * export process.
     *
     * @param externalObjectId
     * real estate ID
     *
     * @return
     * messages
     */
    public final ExportMessage[] getMessagesForObject(String externalObjectId) {
        externalObjectId = StringUtils.trimToNull(externalObjectId);
        if (externalObjectId == null)
            return new ExportMessage[] {};
        List<ExportMessage> msgs = new ArrayList<ExportMessage>();
        for (ExportMessage msg : this.messages) {
            if (externalObjectId.equals(msg.getObjectId()))
                msgs.add(msg);
        }
        return msgs.toArray(new ExportMessage[msgs.size()]);
    }

    /**
     * Return general messages, that occured during the last export process.
     *
     * @return
     * messages
     */
    public final ExportMessage[] getMessagesGeneral() {
        List<ExportMessage> msgs = new ArrayList<ExportMessage>();
        for (ExportMessage msg : this.messages) {
            if (msg.isGeneral())
                msgs.add(msg);
        }
        return msgs.toArray(new ExportMessage[msgs.size()]);
    }

    /**
     * Return the {@link ExportPool} of the current export process.
     *
     * @return
     * pool
     */
    protected final ExportPool getPool() {
        return pool;
    }

    /**
     * Return the progress of the current export process.
     *
     * @return
     * current progress value
     */
    protected final long getProgress() {
        return progress;
    }

    /**
     * Return the total progress of the current export process.
     *
     * @return
     * total progress value
     */
    protected final long getTotalProgress() {
        return totalProgress;
    }

    /**
     * Check, if all values for "energySourceEnev2014" are enabled.
     *
     * @return
     * true, if all values for "energySourceEnev2014" are enabled
     *
     * @see <a href="http://api.immobilienscout24.de/useful/energy-certificate-2014.html">notes about Energy Certificate 2014</a>
     */
    public boolean isUseNewEnergySourceEnev2014Values() {
        return useNewEnergySourceEnev2014Values;
    }

    /**
     * Send {@link Messages} from a Webservice response to the local logger.
     *
     * @param messages
     * messages
     */
    private void logMessagesAsError(Messages messages) {
        if (messages == null || messages.getMessage().isEmpty())
            return;
        for (Message m : messages.getMessage()) {
            LOGGER.error("> " + m.getMessageCode() + " | " + m.getMessage());
        }
    }

    /**
     * Calback method, that is called after the progress has changed.
     * <p>
     * This method may be overridden by inheriting classes in order to track the
     * progress of the export process.
     *
     * @param progress
     * current progress value
     *
     * @param totalProgress
     * total progress value
     */
    protected void progressUpdated(long progress, long totalProgress) {
    }

    /**
     * Register a message for a contact person.
     *
     * @param externalContactId
     * external contact ID
     *
     * @param code
     * message code
     *
     * @param msg
     * message text
     */
    protected final void putContactMessage(String externalContactId, ExportMessage.Code code, String msg) {
        this.putContactMessage(externalContactId, code, msg, null);
    }

    /**
     * Register a message for a contact person.
     *
     * @param externalContactId
     * external contact ID
     *
     * @param code
     * message code
     *
     * @param msg
     * message text
     *
     * @param errorRequestRefNumber
     * unique identifier of the failed HTTP request
     */
    protected void putContactMessage(String externalContactId, ExportMessage.Code code, String msg,
            String errorRequestRefNumber) {
        this.messages.add(ExportMessage.newContactMessage(externalContactId, code, msg, errorRequestRefNumber));
    }

    /**
     * Register error messages for a contact person.
     *
     * @param externalContactId
     * external contact ID
     *
     * @param code
     * message code
     *
     * @param ex
     * exception of the failed request
     */
    protected final void putContactMessage(String externalContactId, ExportMessage.Code code,
            RequestFailedException ex) {
        if (ex == null || ex.responseMessages == null)
            return;
        for (Message message : ex.responseMessages.getMessage()) {
            MessageCode is24Code = message.getMessageCode();
            String is24Msg = StringUtils.trimToNull(message.getMessage());

            String txt = StringUtils.EMPTY;
            if (is24Code != null)
                txt += is24Code.value();
            if (is24Msg != null) {
                if (!StringUtils.isBlank(txt))
                    txt += " | ";
                txt += is24Msg;
            }

            this.putContactMessage(externalContactId, code, txt, ex.requestRefNumber);
        }
    }

    /**
     * Register a general message.
     *
     * @param code
     * message code
     *
     * @param msg
     * message text
     */
    protected final void putGeneralMessage(ExportMessage.Code code, String msg) {
        this.putGeneralMessage(code, msg, null);
    }

    /**
     * Register a general message.
     *
     * @param code
     * message code
     *
     * @param msg
     * message text
     *
     * @param errorRequestRefNumber
     * unique identifier of the failed HTTP request
     */
    protected void putGeneralMessage(ExportMessage.Code code, String msg, String errorRequestRefNumber) {
        this.messages.add(ExportMessage.newGeneralMessage(msg, code, errorRequestRefNumber));
    }

    /**
     * Register general error messages.
     *
     * @param code
     * message code
     *
     * @param ex
     * exception of the failed request
     */
    protected final void putGeneralMessage(ExportMessage.Code code, RequestFailedException ex) {
        if (ex == null || ex.responseMessages == null)
            return;
        for (Message message : ex.responseMessages.getMessage()) {
            MessageCode is24Code = message.getMessageCode();
            String is24Msg = StringUtils.trimToNull(message.getMessage());

            String txt = StringUtils.EMPTY;
            if (is24Code != null)
                txt += is24Code.value();
            if (is24Msg != null) {
                if (!StringUtils.isBlank(txt))
                    txt += " | ";
                txt += is24Msg;
            }

            this.putGeneralMessage(code, txt, ex.requestRefNumber);
        }
    }

    /**
     * Register a message for a real estate.
     *
     * @param externalObjectId
     * external real estate ID
     *
     * @param code
     * message code
     *
     * @param msg
     * message text
     */
    protected final void putObjectMessage(String externalObjectId, ExportMessage.Code code, String msg) {
        this.putObjectMessage(externalObjectId, code, msg, null);
    }

    /**
     * Register a message for a real estate.
     *
     * @param externalObjectId
     * external real estate ID
     *
     * @param code
     * message code
     *
     * @param msg
     * message text
     *
     * @param errorRequestRefNumber
     * unique identifier of the failed HTTP request
     */
    protected void putObjectMessage(String externalObjectId, ExportMessage.Code code, String msg,
            String errorRequestRefNumber) {
        this.messages.add(ExportMessage.newObjectMessage(externalObjectId, code, msg, errorRequestRefNumber));
    }

    /**
     * Register error messages for a real estate.
     *
     * @param externalObjectId
     * external real estate ID
     *
     * @param code
     * message code
     *
     * @param ex
     * exception of the failed request
     */
    protected final void putObjectMessage(String externalObjectId, ExportMessage.Code code,
            RequestFailedException ex) {
        if (ex == null || ex.responseMessages == null)
            return;
        for (Message message : ex.responseMessages.getMessage()) {
            MessageCode is24Code = message.getMessageCode();
            String is24Msg = StringUtils.trimToNull(message.getMessage());

            String txt = StringUtils.EMPTY;
            if (is24Code != null)
                txt += is24Code.value();
            if (is24Msg != null) {
                if (!StringUtils.isBlank(txt))
                    txt += " | ";
                txt += is24Msg;
            }

            this.putObjectMessage(externalObjectId, code, txt, ex.requestRefNumber);
        }
    }

    /**
     * Set the progress of the current export process.
     *
     * @param progress
     * current progress value
     */
    protected final void setProgress(long progress) {
        progress = Math.abs(progress);
        this.progress = (progress <= this.totalProgress) ? progress : this.totalProgress;

        // launch callback function for progress
        progressUpdated(this.progress, this.totalProgress);
    }

    /**
     * Enable / disable all values for "energySourceEnev2014".
     *
     * @param useNewEnergySourceEnev2014Values
     * enabled / disabled
     *
     * @see <a href="http://api.immobilienscout24.de/useful/energy-certificate-2014.html">notes about Energy Certificate 2014</a>
     */
    public void setUseNewEnergySourceEnev2014Values(boolean useNewEnergySourceEnev2014Values) {
        this.useNewEnergySourceEnev2014Values = useNewEnergySourceEnev2014Values;
    }
}