org.onebusaway.siri.core.SiriCommon.java Source code

Java tutorial

Introduction

Here is the source code for org.onebusaway.siri.core.SiriCommon.java

Source

/**
 * Copyright (C) 2011 Brian Ferris <bdferris@onebusaway.org>
 * Copyright (C) 2011 Google, Inc.
 *
 * 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.onebusaway.siri.core;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.Duration;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.params.ConnManagerParams;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.onebusaway.siri.core.exceptions.SiriConnectionException;
import org.onebusaway.siri.core.exceptions.SiriException;
import org.onebusaway.siri.core.exceptions.SiriSerializationException;
import org.onebusaway.siri.core.handlers.SiriRawHandler;
import org.onebusaway.siri.core.services.HttpClientService;
import org.onebusaway.siri.core.services.SchedulingService;
import org.onebusaway.siri.core.versioning.SiriVersioning;
import org.onebusaway.status_exporter.StatusProviderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import uk.org.siri.siri.AbstractFunctionalServiceRequestStructure;
import uk.org.siri.siri.AbstractRequestStructure;
import uk.org.siri.siri.AbstractSubscriptionStructure;
import uk.org.siri.siri.CheckStatusResponseStructure;
import uk.org.siri.siri.MessageQualifierStructure;
import uk.org.siri.siri.ProducerResponseStructure;
import uk.org.siri.siri.RequestStructure;
import uk.org.siri.siri.ResponseEndpointStructure;
import uk.org.siri.siri.ResponseStructure;
import uk.org.siri.siri.ServiceDelivery;
import uk.org.siri.siri.ServiceRequest;
import uk.org.siri.siri.Siri;
import uk.org.siri.siri.SubscriptionContextStructure;
import uk.org.siri.siri.SubscriptionRequest;
import uk.org.siri.siri.VehicleMonitoringSubscriptionStructure;

/**
 * Common base configuration and methods used by both {@link SiriClient} and
 * {@link SiriServer}.
 * 
 * @author bdferris
 * 
 */
public class SiriCommon implements SiriRawHandler, StatusProviderService {

    public enum ELogRawXmlType {
        NONE, CONTROL, DATA, ALL
    }

    private static Logger _log = LoggerFactory.getLogger(SiriCommon.class);

    private static DatatypeFactory _dataTypeFactory = SiriTypeFactory.createDataTypeFactory();

    private JAXBContext _jaxbContext;

    protected SchedulingService _schedulingService;

    /**
     * We break this out as a separate service to assist with unit testing
     */
    protected HttpClientService _httpClientService;

    private DefaultHttpClient _client;

    private String _identity;

    private String _url;

    private String _privateUrl;

    private String _expandedUrl;

    /**
     * If not null, overrides the default local address used by outgoing http
     * client connections. Useful if the connection needs to appear to come from a
     * specific port.
     */
    private InetAddress _localAddress;

    protected ELogRawXmlType _logRawXmlType = ELogRawXmlType.NONE;

    protected boolean _formatOutputXmlByDefault = false;

    private AtomicInteger _requestCount = new AtomicInteger();

    private int _connectionTimeout = 0;

    public SiriCommon() {
        _identity = UUID.randomUUID().toString();
    }

    @Inject
    public void setSchedulingService(SchedulingService schedulingService) {
        _schedulingService = schedulingService;
    }

    @Inject
    public void setHttpClientService(HttpClientService httpClientService) {
        _httpClientService = httpClientService;
    }

    @Inject
    public void setJAXBContext(JAXBContext jaxbContext) {
        _jaxbContext = jaxbContext;
    }

    /**
     * @return your SIRI participant identity, used to identify your SIRI endpoint
     *         in most requests and responses
     */
    public String getIdentity() {
        return _identity;
    }

    /**
     * Set the SIRI participant identity used to identify your SIRI endpoint in
     * most requests and responses.
     * 
     * @param identity your SIRI participant identity
     */
    public void setIdentity(String identity) {
        _identity = identity;
    }

    /**
     * The public url our client will listen to and expose for url callbacks from
     * the SIRI server. Only used when using publish / subscribe methods. See also
     * {@link #getPrivateUrl()}.
     * 
     * @return the client url
     */
    public String getUrl() {
        return _url;
    }

    /**
     * The public url our client will listen to and expose for url callbacks from
     * the SIRI server. Only used when using publish / subscribe methods. See also
     * {@link #setPrivateUrl(String)}.
     * 
     * @param url
     * 
     */
    public void setUrl(String url) {
        _url = url;
        /**
         * Perform wildcard hostname expansion on our consumer address
         */
        _expandedUrl = replaceHostnameWildcardWithPublicHostnameInUrl(_url);
        checkLocalAddress();
    }

    /**
     * See the discussion for {@link #setPrivateUrl(String)}.
     * 
     * @return the private client url
     */
    public String getPrivateUrl() {
        return _privateUrl;
    }

    /**
     * In some cases, we may wish to listen for incoming SIRI data from the server
     * on a different local URL than the URL we publish externally to the SIRI
     * server (see {@link #setUrl(String)}). For example, your firewall or NAT
     * setup might require a separate public and private client url. If set, the
     * privateClientUrl will control how we actually listen for incoming SIRI
     * service deliveries, separate from the url we announce to the server.
     * 
     * If privateClientUrl is not set, we'll default to using the public clientUrl
     * (see {@link #setUrl(String)}).
     * 
     * @param privateClientUrl
     */
    public void setPrivateUrl(String privateClientUrl) {
        _privateUrl = privateClientUrl;
        checkLocalAddress();
    }

    /**
     * The internal URL that the client should bind to for incoming pub-sub
     * connection. This defaults to {@link #getUrl()}, unless
     * {@link #getPrivateUrl()} has been specified.
     * 
     * @param expandHostnameWildcard
     * 
     * @return
     */
    public URL getInternalUrlToBind(boolean expandHostnameWildcard) {

        String clientUrl = _url;
        if (_privateUrl != null)
            clientUrl = _privateUrl;

        if (expandHostnameWildcard)
            clientUrl = replaceHostnameWildcardWithPublicHostnameInUrl(clientUrl);

        return url(clientUrl);
    }

    /**
     * If your machine has multiple addresses and you need your SIRI HTTP requests
     * to look like they are coming from a specific address, this address will be
     * used.
     * 
     * @return
     */
    public InetAddress getLocalAddress() {
        return _localAddress;
    }

    /**
     * If your machine has multiple addresses and you need your SIRI HTTP requests
     * to look like they are coming from a specific address, use this method to
     * indicate which local address to use.
     * 
     * @param localAddress the address to use
     */
    public void setLocalAddress(InetAddress localAddress) {
        _localAddress = localAddress;
    }

    /**
     * 
     * @return how raw XML of SIRI messages is logged
     */
    public ELogRawXmlType getLogRawXmlType() {
        return _logRawXmlType;
    }

    /**
     * Determine how raw XML of SIRI messages is internally logged
     * 
     * @param logRawXmlType the logging type
     */
    public void setLogRawXmlType(ELogRawXmlType logRawXmlType) {
        _logRawXmlType = logRawXmlType;
    }

    /**
     * @return true if serialized XML messages will be pretty-print formatted by
     *         default.
     */
    public boolean isFormatOutputXmlByDefault() {
        return _formatOutputXmlByDefault;
    }

    /**
     * Determine if serialized XML messages will be pretty-print formatted by
     * default.
     * 
     * @param formatOutputXmlByDefault true if output formatting should be applied
     */
    public void setFormatOutputXmlByDefault(boolean formatOutputXmlByDefault) {
        _formatOutputXmlByDefault = formatOutputXmlByDefault;
    }

    /**
     * When specified, indicates connection timeout to be used when establishing
     * connections to a remote endpoint using an HTTP client and for socket
     * timeouts once that connection is established.
     * 
     * @param connectionTimeout time, in seconds
     */
    public void setConnectionTimeout(int connectionTimeout) {
        _connectionTimeout = connectionTimeout;
    }

    /****
     * Setup Methods
     ****/

    @PostConstruct
    public void start() {
        HttpParams params = new BasicHttpParams();

        /**
         * Override the default local address used for outgoing http client
         * connections, if specified
         */
        if (_localAddress != null) {
            params.setParameter(ConnRoutePNames.LOCAL_ADDRESS, _localAddress);
        }

        /**
         * Set the timeout for both connections to a socket and for reading from a
         * socket.
         */
        if (_connectionTimeout != 0) {
            HttpConnectionParams.setConnectionTimeout(params, _connectionTimeout * 1000);
            HttpConnectionParams.setSoTimeout(params, _connectionTimeout * 1000);
        }

        /**
         * We want to allow a fair-amount of concurrent connections. TODO: Can we
         * auto-tune this somehow?
         */
        ConnManagerParams.setMaxTotalConnections(params, 50);

        /**
         * We want to create a connection manager that can pool multiple
         * connections.
         */
        SchemeRegistry schemeRegistry = new SchemeRegistry();
        schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
        schemeRegistry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
        ClientConnectionManager connectionManager = new ThreadSafeClientConnManager(params, schemeRegistry);

        _client = new DefaultHttpClient(connectionManager, params);
    }

    @PreDestroy
    public void stop() {
        _client.getConnectionManager().shutdown();
    }

    /****
     * {@link StatusProviderService} Interface
     ****/

    @Override
    public void getStatus(Map<String, String> status) {
        status.put("siri.common.requestCounter", Integer.toString(_requestCount.get()));
    }

    /***
     * Core Methods
     ****/

    /**
     * It's up to sub-classes to implement this properly. We just provide it here
     * to make {@link SiriCommon} appropriate as for exporting, as opposed to
     * working with SiriClient and SiriServer by themselves.
     */
    @Override
    public void handleRawRequest(Reader reader, Writer writer) {

    }

    /****
     * Protected Methods
     ****/

    /**
     * Submit the specified {@link SiriClientRequest}, filling in any Siri
     * data-structures with appropriate default values, versioning the payload as
     * appropriate for the endpoint, and submitting the request over HTTP. Any
     * response is parsed, convered back to the latest SIRI version, and returned.
     * 
     * @param <T>
     * @param request the SIRI client request
     * @param asynchronous true if this is an asynchronous request
     * @return any response from the endpoint, or null if none received
     */
    @SuppressWarnings("unchecked")
    protected <T> T processRequestWithResponse(SiriClientRequest request, boolean asynchronous) {

        if (!request.isSubscribe() && request.getPollInterval() > 0) {
            AsynchronousClientRequest asyncAttempt = new AsynchronousClientRequest(request);
            _schedulingService.schedule(asyncAttempt, request.getPollInterval(), TimeUnit.SECONDS);
        }

        _requestCount.incrementAndGet();

        Siri payload = request.getPayload();

        /**
         * We make a deep copy of the SIRI payload so that when we fill in values
         * for the request, the original is unmodified, making it reusable.
         */
        payload = SiriLibrary.copy(payload);

        fillAllSiriStructures(payload);

        if (payload.getSubscriptionRequest() != null)
            fillSubscriptionRequestStructure(request, payload.getSubscriptionRequest());

        /**
         * We potentially need to translate the Siri payload to an older version of
         * the specification, as requested by the caller
         */
        SiriVersioning versioning = SiriVersioning.getInstance();
        Object versionedPayload = versioning.getPayloadAsVersion(payload, request.getTargetVersion());

        String content = marshallToString(versionedPayload);

        if (isRawDataLogged(payload)) {
            _log.info("logging raw xml request:\n=== REQUEST BEGIN ===\n" + content + "\n=== REQUEST END ===");
        }

        HttpResponse response = processRawContentRequestWithResponse(request, payload, content);

        if (response == null)
            return null;

        HttpEntity entity = response.getEntity();

        String responseContent = null;
        Object responseData = null;

        try {

            Reader responseReader = new InputStreamReader(entity.getContent());

            _log.debug("response content length: {}", entity.getContentLength());

            /**
             * Opportunistically skip capturing the response in an intermediate string
             * if we don't have to
             */
            if (_logRawXmlType != ELogRawXmlType.NONE) {
                StringBuilder b = new StringBuilder();
                responseReader = copyReaderToStringBuilder(responseReader, b);
                responseContent = b.toString();

            }

            if (entity.getContentLength() != 0) {
                responseData = unmarshall(responseReader);
                responseData = versioning.getPayloadAsVersion(responseData, versioning.getDefaultVersion());
            }

        } catch (Exception ex) {
            throw new SiriSerializationException(ex);
        } finally {
            try {
                entity.consumeContent();
            } catch (IOException ex) {

            }
        }

        if (responseData != null && responseData instanceof Siri) {

            Siri siri = (Siri) responseData;

            if (isRawDataLogged(siri)) {
                _log.info("logging raw xml response:\n=== RESPONSE BEGIN ===\n" + responseContent
                        + "\n=== RESPONSE END ===");
            }

            handleSiriResponse(siri, asynchronous, request);
        }

        return (T) responseData;
    }

    /**
     * The specified client request is processed asynchronously on another thread.
     * 
     * @param request the SIRI client request
     */
    protected void processRequestWithAsynchronousResponse(SiriClientRequest request) {

        AsynchronousClientRequest attempt = new AsynchronousClientRequest(request);
        _schedulingService.submit(attempt);
    }

    /**
     * Override this method to provide custom behavior for processing a response
     * from a SIRI endoint.
     * 
     * @param siri the payload
     * @param asynchronousResponse true if the response was received
     *          asynchronously, otherwise false
     * @param siriClientRequest the request that initiated this response, or
     *          potentially null if asynchronous
     */
    protected void handleSiriResponse(Siri siri, boolean asynchronousResponse,
            SiriClientRequest siriClientRequest) {

    }

    /****
     * 
     ****/

    /**
     * Fill in all SIRI data-strutures with appropriate default values as needed.
     * 
     * @param siri the SIRI payload
     */
    protected void fillAllSiriStructures(Siri siri) {

        fillRequestStructure(siri.getCapabilitiesRequest());
        fillRequestStructure(siri.getCheckStatusRequest());
        fillRequestStructure(siri.getFacilityRequest());
        fillRequestStructure(siri.getInfoChannelRequest());
        fillRequestStructure(siri.getLinesRequest());
        fillRequestStructure(siri.getProductCategoriesRequest());
        fillRequestStructure(siri.getServiceFeaturesRequest());
        fillRequestStructure(siri.getStopPointsRequest());
        fillRequestStructure(siri.getSubscriptionRequest());
        fillRequestStructure(siri.getTerminateSubscriptionRequest());
        fillRequestStructure(siri.getVehicleFeaturesRequest());

        fillServiceRequestStructure(siri.getServiceRequest());

        fillResponseStructure(siri.getCapabilitiesResponse());
        fillResponseStructure(siri.getCheckStatusResponse());
        fillServiceDelivery(siri.getServiceDelivery());
        fillResponseStructure(siri.getSubscriptionResponse());
        fillResponseStructure(siri.getTerminateSubscriptionResponse());
    }

    /**
     * Fill in the {@link SubscriptionRequest} with appropriate default values as
     * needed.
     * 
     * @param request the SIRI client request associated with the subscription
     *          request
     * @param subscriptionRequest the subscription request to fill in
     */
    protected void fillSubscriptionRequestStructure(SiriClientRequest request,
            SubscriptionRequest subscriptionRequest) {

        if (subscriptionRequest == null)
            return;

        int heartbeatInterval = request.getHeartbeatInterval();
        if (heartbeatInterval > 0) {

            Duration interval = _dataTypeFactory.newDuration(heartbeatInterval * 1000);
            SubscriptionContextStructure context = new SubscriptionContextStructure();
            context.setHeartbeatInterval(interval);
            subscriptionRequest.setSubscriptionContext(context);
        }

        /**
         * Fill in subscription ids
         */
        for (ESiriModuleType moduleType : ESiriModuleType.values()) {

            List<AbstractSubscriptionStructure> subs = SiriLibrary
                    .getSubscriptionRequestsForModule(subscriptionRequest, moduleType);

            for (AbstractSubscriptionStructure sub : subs) {

                if (sub.getSubscriberRef() == null)
                    sub.setSubscriberRef(SiriTypeFactory.particpantRef(_identity));

                if (sub.getSubscriptionIdentifier() == null)
                    sub.setSubscriptionIdentifier(SiriTypeFactory.randomSubscriptionId());

                if (sub.getInitialTerminationTime() == null) {
                    Date initialTerminationTime = new Date(
                            System.currentTimeMillis() + request.getInitialTerminationDuration());
                    sub.setInitialTerminationTime(initialTerminationTime);
                }

                /**
                 * TODO: Fill all these in
                 */
                if (sub instanceof VehicleMonitoringSubscriptionStructure)
                    fillAbstractFunctionalServiceRequestStructure(
                            ((VehicleMonitoringSubscriptionStructure) sub).getVehicleMonitoringRequest());
            }
        }
    }

    /**
     * Fill in the request with appropriate default values as needed.
     * 
     * @param request the request
     */
    protected void fillRequestStructure(RequestStructure request) {

        if (request == null)
            return;

        request.setRequestorRef(SiriTypeFactory.particpantRef(_identity));

        request.setAddress(_expandedUrl);

        if (request.getMessageIdentifier() == null || request.getMessageIdentifier().getValue() == null) {
            MessageQualifierStructure messageIdentifier = SiriTypeFactory.randomMessageId();
            request.setMessageIdentifier(messageIdentifier);
        }

        if (request.getRequestTimestamp() == null)
            request.setRequestTimestamp(new Date());
    }

    /**
     * Fill in the request with appropriate default values as needed.
     * 
     * TODO: It sure would be nice if {@link ServiceRequest} was a sub-class of
     * {@link RequestStructure}
     * 
     * @param request
     */
    protected void fillServiceRequestStructure(ServiceRequest request) {

        if (request == null)
            return;

        request.setRequestorRef(SiriTypeFactory.particpantRef(_identity));

        request.setAddress(_expandedUrl);

        if (request.getRequestTimestamp() == null)
            request.setRequestTimestamp(new Date());

        fillAbstractFunctionalServiceRequestStructures(request.getConnectionMonitoringRequest());
        fillAbstractFunctionalServiceRequestStructures(request.getConnectionTimetableRequest());
        fillAbstractFunctionalServiceRequestStructures(request.getEstimatedTimetableRequest());
        fillAbstractFunctionalServiceRequestStructures(request.getFacilityMonitoringRequest());
        fillAbstractFunctionalServiceRequestStructures(request.getGeneralMessageRequest());
        fillAbstractFunctionalServiceRequestStructures(request.getProductionTimetableRequest());
        fillAbstractFunctionalServiceRequestStructures(request.getSituationExchangeRequest());
        fillAbstractFunctionalServiceRequestStructures(request.getStopMonitoringMultipleRequest());
        fillAbstractFunctionalServiceRequestStructures(request.getStopMonitoringRequest());
        fillAbstractFunctionalServiceRequestStructures(request.getVehicleMonitoringRequest());
    }

    /**
     * Fill in the requests with appropriate default values as needed.
     * 
     * @param <T>
     * @param requests
     */
    protected <T extends AbstractFunctionalServiceRequestStructure> void fillAbstractFunctionalServiceRequestStructures(
            List<T> requests) {
        for (AbstractFunctionalServiceRequestStructure request : requests)
            fillAbstractFunctionalServiceRequestStructure(request);
    }

    /**
     * Fill in the request with appropriate default values as needed.
     * 
     * @param request
     */
    protected void fillAbstractFunctionalServiceRequestStructure(
            AbstractFunctionalServiceRequestStructure request) {

        fillAbstractRequestStructure(request);
    }

    /**
     * Fill in the request with appropriate default values as needed.
     * 
     * @param request
     */
    protected void fillAbstractRequestStructure(AbstractRequestStructure request) {
        if (request.getRequestTimestamp() == null)
            request.setRequestTimestamp(new Date());
    }

    protected void fillServiceDelivery(ServiceDelivery serviceDelivery) {
        fillResponseStructure(serviceDelivery);
    }

    /****
     * Private Methods
     ****/

    private void fillResponseStructure(ResponseEndpointStructure response) {

        if (response == null)
            return;

        if (response.getAddress() != null)
            response.setAddress(_expandedUrl);

        if (response.getResponderRef() != null)
            response.setResponderRef(SiriTypeFactory.particpantRef(_identity));

        fillGenericResponseStructure(response);
    }

    private void fillResponseStructure(ProducerResponseStructure response) {

        if (response == null)
            return;

        if (response.getAddress() == null)
            response.setAddress(_expandedUrl);

        if (response.getProducerRef() == null)
            response.setProducerRef(SiriTypeFactory.particpantRef(_identity));

        if (response.getResponseMessageIdentifier() == null)
            response.setResponseMessageIdentifier(SiriTypeFactory.randomMessageId());

        fillGenericResponseStructure(response);
    }

    private void fillResponseStructure(CheckStatusResponseStructure response) {

        if (response == null)
            return;

        if (response.getAddress() != null)
            response.setAddress(_expandedUrl);

        if (response.getProducerRef() != null)
            response.setProducerRef(SiriTypeFactory.particpantRef(_identity));

        if (response.getResponseMessageIdentifier() == null)
            response.setResponseMessageIdentifier(SiriTypeFactory.randomMessageId());

        fillGenericResponseStructure(response);
    }

    private void fillGenericResponseStructure(ResponseStructure response) {

        if (response == null)
            return;

        if (response.getResponseTimestamp() == null)
            response.setResponseTimestamp(new Date());
    }

    /****
     * 
     ****/

    /**
     * 
     * @param payload the SIRI payload
     * @return true if the specified payload should be logged, as determined by
     *         the {@link #getLogRawXmlType()} settings.
     */
    protected boolean isRawDataLogged(Siri payload) {
        switch (_logRawXmlType) {
        case NONE:
            return false;
        case ALL:
            return true;
        case DATA:
            return payload.getServiceDelivery() != null;
        case CONTROL:
            return payload.getServiceDelivery() == null;
        default:
            throw new IllegalStateException("unknown ELogRawXmlType=" + _logRawXmlType);
        }
    }

    /****
     * 
     ****/

    /**
     * Method to unmarshall an {@link InputStream} into a high-level SIRI
     * data-structure using JAXB. Typically, you don't call this directly.
     * 
     * @param <T>
     * @param in
     * @return
     */
    @SuppressWarnings("unchecked")
    public <T> T unmarshall(InputStream in) {
        try {
            Unmarshaller unmarshaller = _jaxbContext.createUnmarshaller();
            return (T) unmarshaller.unmarshal(in);
        } catch (Exception ex) {
            throw new SiriSerializationException(ex);
        }
    }

    /**
     * Method to unmarshall an {@link Reader} into a high-level SIRI
     * data-structure using JAXB. Typically, you don't call this directly.
     * 
     * @param <T>
     * @param reader
     * @return
     */
    @SuppressWarnings("unchecked")
    public <T> T unmarshall(Reader reader) {
        try {
            Unmarshaller unmarshaller = _jaxbContext.createUnmarshaller();
            return (T) unmarshaller.unmarshal(reader);
        } catch (Exception ex) {
            throw new SiriSerializationException(ex);
        }
    }

    /**
     * Marshall the specified object to the target {@link Writer} using JAXB.
     * 
     * @param object
     * @param writer
     */
    public void marshall(Object object, Writer writer) {
        marshall(object, writer, _formatOutputXmlByDefault);
    }

    /**
     * Marshall the specified object to the target {@link Writer} using JAXB.
     * 
     * @param object
     * @param writer
     * @param formatOutput if true, the serialized XML will be pretty-printed
     */
    public void marshall(Object object, Writer writer, boolean formatOutput) {
        try {
            Marshaller m = _jaxbContext.createMarshaller();
            if (formatOutput) {
                m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
            }
            m.marshal(object, writer);
        } catch (JAXBException ex) {
            throw new SiriSerializationException(ex);
        }
    }

    /**
     * Marshall the specified object as a String using JAXB.
     * 
     * @param object
     * @return the String representation of the specified Object
     */
    public String marshallToString(Object object) {
        return marshallToString(object, _formatOutputXmlByDefault);
    }

    /**
     * Marshall the specified object as a String using JAXB.
     * 
     * @param object
     * @param formatOutput if true, the serialized XML will be pretty-printed
     * @return the String representation of the specified Object
     */
    public String marshallToString(Object object, boolean formatOutput) {
        StringWriter writer = new StringWriter();
        marshall(object, writer, formatOutput);
        return writer.toString();
    }

    /****
     * Protected Methods
     ****/

    protected ScheduledExecutorService createExecutor() {
        return Executors.newSingleThreadScheduledExecutor();
    }

    /**
     * This method encapsulates our reconnection behavior around the call to
     * {@link #sendHttpRequestWithResponse(String, String)}.
     * 
     * @param request
     * @param payload
     * @param content
     * @return
     */
    protected HttpResponse processRawContentRequestWithResponse(SiriClientRequest request, Siri payload,
            String content) {

        String url = getUrlForRequest(request);

        if (request.getReconnectionAttempts() != 0) {

            try {

                HttpResponse response = sendHttpRequestWithResponse(url, content);

                /**
                 * Reset our connection error count and note that we've successfully
                 * reconnected if the we've had problems before
                 */
                if (request.getConnectionErrorCount() > 0)
                    _log.info("successfully reconnected to " + url);
                request.resetConnectionErrorCount();

                return response;

            } catch (SiriConnectionException ex) {

                String message = "error connecting to " + url + " (remainingConnectionAttempts="
                        + request.getRemainingReconnectionAttempts() + " connectionErrorCount="
                        + request.getConnectionErrorCount() + ")";

                /**
                 * We display the full exception on the first connection error, but hide
                 * it on recurring errors
                 */
                if (request.getConnectionErrorCount() == 0) {
                    _log.warn(message, ex);
                } else {
                    _log.warn(message);
                }

                request.incrementConnectionErrorCount();

                cleanupFailedRequest(request, payload);
                reattemptRequestIfApplicable(request);

                /**
                 * Note: we swallow up the exception here, meaning the client won't know
                 * there is an error. Might there be situations where the client wants
                 * to know it was an error, even if they've specified reconnection
                 * semantics?
                 */
                return null;
            }

        } else {

            return sendHttpRequestWithResponse(url, content);
        }
    }

    /**
     * Determine which URL should be used for a request. Recall that a SIRI
     * endpoint can have separate URLs for subscription management and
     * check-status requests.
     * 
     * @param request
     * @return
     */
    protected String getUrlForRequest(SiriClientRequest request) {
        Siri payload = request.getPayload();
        if (payload.getCheckStatusRequest() != null && request.getCheckStatusUrl() != null)
            return request.getCheckStatusUrl();
        if (payload.getTerminateSubscriptionRequest() != null && request.getManageSubscriptionUrl() != null)
            return request.getManageSubscriptionUrl();
        return request.getTargetUrl();
    }

    /**
     * Construct an HTTP POST request, send it, and ignore any content in the
     * response.
     * 
     * @param url the target url where we will POST
     * @param content the content of the POST request
     */
    protected void sendHttpRequest(String url, String content) {
        HttpResponse response = sendHttpRequestWithResponse(url, content);
        /**
         * Make sure we consume the response content so that the connection might be
         * reused.
         */
        HttpEntity entity = response.getEntity();
        if (entity != null) {
            try {
                entity.consumeContent();
            } catch (IOException e) {

            }
        }
    }

    /**
     * Construct an HTTP POST request, send it, and decode the response.
     * 
     * TODO: We could probably encapsulate this even further to make it easier to
     * replace the HTTP client implementation or support additional transport
     * mechanisms.
     * 
     * @param url the target url where we will POST
     * @param content the content of the POST request
     * @return the response
     */
    protected HttpResponse sendHttpRequestWithResponse(String url, String content) {

        HttpPost post = new HttpPost(url);

        try {
            post.setEntity(new StringEntity(content));
        } catch (UnsupportedEncodingException ex) {
            throw new SiriSerializationException(ex);
        }

        HttpResponse response = _httpClientService.executeHttpMethod(_client, post);
        StatusLine statusLine = response.getStatusLine();

        if (statusLine.getStatusCode() != HttpStatus.SC_OK) {
            HttpEntity entity = response.getEntity();
            if (entity != null) {
                try {
                    BufferedReader reader = new BufferedReader(new InputStreamReader(entity.getContent()));
                    StringBuilder b = new StringBuilder();
                    String line = null;

                    while ((line = reader.readLine()) != null)
                        b.append(line).append('\n');
                    _log.warn(
                            "error connecting to url " + post.getURI() + " statusCode=" + statusLine.getStatusCode()
                                    + "\nrequestBody=" + content + "\nresponseBody=" + b.toString());
                    entity.consumeContent();
                } catch (IOException ex) {
                    _log.warn("error reading http response", ex);
                }
            }
            /**
             * TODO: This is a temporary hack to keep a bad client from bailing...
             */
            if (statusLine.getStatusCode() != HttpStatus.SC_BAD_REQUEST) {
                throw new SiriConnectionException(
                        "error connecting to url " + post.getURI() + " statusCode=" + statusLine.getStatusCode());
            } else {
                _log.warn("statusCode=" + statusLine.getStatusCode() + " so ignoring for now");
            }
        }

        return response;
    }

    /**
     * Method provides opportunity to clean up a failed client request.
     * 
     * @param request
     * @param payload
     */
    protected void cleanupFailedRequest(SiriClientRequest request, Siri payload) {

    }

    /**
     * In an instance where a {@link SiriClientRequest} has a connection error, or
     * perhaps when an appropriate response is not received before a timeout, this
     * method will attempt to resend the request based on the
     * {@link SiriClientRequest#getRemainingReconnectionAttempts()} behavior.
     * 
     * The reconnect will be attempted asynchronously after the
     * {@link SiriClientRequest#getReconnectionInterval()} has passed.
     * 
     * @param request the client request to potentially reconnect
     */
    protected void reattemptRequestIfApplicable(SiriClientRequest request) {

        if (request.getRemainingReconnectionAttempts() == 0)
            return;

        /**
         * We have some reconnection attempts remaining, so we schedule another
         * connection attempt
         */
        request.decrementRemainingReconnctionAttempts();

        AsynchronousClientRequest asyncAttempt = new AsynchronousClientRequest(request);
        _schedulingService.schedule(asyncAttempt, request.getReconnectionInterval(), TimeUnit.SECONDS);
    }

    protected Reader copyReaderToStringBuilder(Reader responseReader, StringBuilder b) throws IOException {
        char[] buffer = new char[1024];
        while (true) {
            int rc = responseReader.read(buffer);
            if (rc == -1)
                break;
            b.append(buffer, 0, rc);
        }

        responseReader.close();
        responseReader = new StringReader(b.toString());
        return responseReader;
    }

    /**
     * Convenience method that hides the {@link MalformedURLException} thrown when
     * creating a URL object.
     * 
     * @param url string representation of a URL
     * @return the actual URL
     */
    protected URL url(String url) {
        try {
            return new URL(url);
        } catch (MalformedURLException ex) {
            throw new SiriException("bad url " + url, ex);
        }
    }

    /**
     * If the user has specified a wildcard "*" in their {@link #getUrl()},
     * indicating that they want to bind to all interfaces on their machine, we
     * still need a hostname for when we broadcast that URL to SIRI endpoints for
     * pub-sub callbacks. This method constructs a URL with the "*" replaced with
     * the machine's hostname.
     * 
     * @param url
     * @return
     */
    protected String replaceHostnameWildcardWithPublicHostnameInUrl(String url) {

        try {
            URL asURL = new URL(url);
            if (asURL.getHost().equals("*")) {
                InetAddress address = Inet4Address.getLocalHost();
                String hostname = address.getHostName();
                return url.replace("*", hostname);
            }
        } catch (UnknownHostException e) {

        } catch (MalformedURLException e) {

        }

        return url;
    }

    /**
     * Determine if a specific local address has been specified in the internal
     * URL, indicating that we should use that as our source address when making
     * HTTP client requests. Important if you need your SIRI HTTP requests to look
     * like they are coming from a particular address, especially when your
     * machine has more than one address.
     */
    protected void checkLocalAddress() {

        URL bindUrl = getInternalUrlToBind(false);

        if (!(bindUrl.getHost().equals("*") || bindUrl.getHost().equals("localhost"))) {
            try {
                InetAddress address = InetAddress.getByName(bindUrl.getHost());
                setLocalAddress(address);
            } catch (UnknownHostException ex) {
                _log.warn("error resolving hostname: " + bindUrl.getHost(), ex);
            }
        }
    }

    /**
     * A runnable task that attempts a connection from a SIRI client to a SIRI
     * server.
     * 
     * @author bdferris
     */
    class AsynchronousClientRequest implements Runnable {

        private final SiriClientRequest request;

        public AsynchronousClientRequest(SiriClientRequest request) {
            this.request = request;
        }

        SiriClientRequest getRequest() {
            return request;
        }

        @Override
        public void run() {

            try {
                processRequestWithResponse(request, true);
            } catch (Throwable ex) {
                _log.error("error executing asynchronous client request", ex);
            }
        }
    }
}