/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.camel.component.cxf;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.camel.Exchange;
import org.apache.camel.impl.DefaultHeaderFilterStrategy;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.headers.Header;
import org.apache.cxf.message.Message;
import org.apache.cxf.service.model.BindingInfo;
import org.apache.cxf.service.model.BindingOperationInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The default CXF header filter strategy.
 * 
 * @version 
 */
public class CxfHeaderFilterStrategy extends DefaultHeaderFilterStrategy {
    private static final Logger LOG = LoggerFactory.getLogger(CxfHeaderFilterStrategy.class);
    private Map<String, MessageHeaderFilter> messageHeaderFiltersMap;
 
    private List<MessageHeaderFilter> messageHeaderFilters;

    private boolean relayHeaders = true;
    private boolean allowFilterNamespaceClash;
    private boolean relayAllMessageHeaders;

    public CxfHeaderFilterStrategy() {
        initialize();  
    }

    protected void initialize() {
        //filter the operationName and operationName
        getOutFilter().add(CxfConstants.OPERATION_NAME.toLowerCase());
        getOutFilter().add(CxfConstants.OPERATION_NAMESPACE.toLowerCase());
        
        // Request and response context Maps will be passed to CXF Client APIs
        getOutFilter().add(Client.REQUEST_CONTEXT.toLowerCase());
        getOutFilter().add(Client.RESPONSE_CONTEXT.toLowerCase());

        // protocol headers are stored as a Map.  DefaultCxfBinding
        // read the Map and send each entry to the filter.  Therefore,
        // we need to filter the header of this name.
        getOutFilter().add(Message.PROTOCOL_HEADERS.toLowerCase());
        getInFilter().add(Message.PROTOCOL_HEADERS.toLowerCase());
        
        // Add the filter for the Generic Message header
        // http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.5
        getOutFilter().add("cache-control");
        getOutFilter().add("connection");
        getOutFilter().add("date");
        getOutFilter().add("pragma");
        getOutFilter().add("trailer");
        getOutFilter().add("transfer-encoding");
        getOutFilter().add("upgrade");
        getOutFilter().add("via");
        getOutFilter().add("warning");
        
        // Since CXF can take the content-type from the protocol header
        // we need to filter this header of this name.
        getOutFilter().add("Content-Type".toLowerCase());

        // Filter out Content-Length since it can fool Jetty (HttpGenerator) to 
        // close response output stream prematurely.  (It occurs when the
        // message size (e.g. with attachment) is large and response content length 
        // is bigger than request content length.)
        getOutFilter().add("Content-Length".toLowerCase());
        
        // Filter Content-Length as it will cause some trouble when the message 
        // is passed to the other endpoint
        getInFilter().add("content-length".toLowerCase());
        
        setLowerCase(true);

        // initialize message header filter map with default SOAP filter
        messageHeaderFiltersMap = new HashMap<String, MessageHeaderFilter>();
        addToMessageHeaderFilterMap(new SoapMessageHeaderFilter());
        
        // filter headers begin with "Camel" or "org.apache.camel"
        setOutFilterPattern("(Camel|org\\.apache\\.camel)[\\.|a-z|A-z|0-9]*");
    }

    @SuppressWarnings("unchecked")
    @Override
    protected boolean extendedFilter(Direction direction, String key, Object value, Exchange exchange) {
        // Currently only handles Header.HEADER_LIST message header relay/filter
        if (!Header.HEADER_LIST.equals(key) || value == null) { 
            return false;
        }
        
        if (!relayHeaders) {
            // not propagating Header.HEADER_LIST at all
            return true;
        }
        
        if (relayAllMessageHeaders) {
            // all message headers will be relayed (no filtering)
            return false;
        }

        // get filter
        MessageHeaderFilter messageHeaderfilter = getMessageHeaderFilter(exchange);
        if (messageHeaderfilter == null) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("No CXF Binding namespace can be resolved.  Message headers are intact.");
            }
            return false;
        }
        
        if (LOG.isTraceEnabled()) {
            LOG.trace("messageHeaderfilter = " + messageHeaderfilter);
        }

        try {
            messageHeaderfilter.filter(direction, (List<Header>)value);
        } catch (Throwable t) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Failed to cast value to Header<List> due to " + t.toString(), t);
            }
        }
        
        // return false since the header list (which has been filtered) should be propagated
        return false;
    }

    private void addToMessageHeaderFilterMap(MessageHeaderFilter filter) {
        for (String ns : filter.getActivationNamespaces()) {
            if (messageHeaderFiltersMap.containsKey(ns) && messageHeaderFiltersMap.get(ns) 
                != messageHeaderFiltersMap && !allowFilterNamespaceClash) {
                throw new IllegalArgumentException("More then one MessageHeaderRelay activates "
                                                   + "for the same namespace: " + ns);
            }
            messageHeaderFiltersMap.put(ns, filter);
        }
    }
    
    private MessageHeaderFilter getMessageHeaderFilter(Exchange exchange) {
        BindingOperationInfo boi = exchange.getProperty(BindingOperationInfo.class.getName(), 
                                                        BindingOperationInfo.class);
        String ns = null;
        if (boi != null) {
            BindingInfo b = boi.getBinding();
            if (b != null) {
                ns = b.getBindingId();
            }
        }
        
        MessageHeaderFilter answer = null;
        if (ns != null) {
            answer = messageHeaderFiltersMap.get(ns);
        }
        
        return answer;
    }

    /**
     * @param messageHeaderFilters the messageHeaderFilters to set
     */
    public void setMessageHeaderFilters(List<MessageHeaderFilter> messageHeaderFilters) {
        this.messageHeaderFilters = messageHeaderFilters;
        // clear the amp to allow removal of default filter
        messageHeaderFiltersMap.clear();
        for (MessageHeaderFilter filter : messageHeaderFilters) {
            addToMessageHeaderFilterMap(filter);
        }
    }

    /**
     * @return the messageHeaderFilters
     */
    public List<MessageHeaderFilter> getMessageHeaderFilters() {
        return messageHeaderFilters;
    }

    /**
     * @return the allowFilterNamespaceClash
     */
    public boolean isAllowFilterNamespaceClash() {
        return allowFilterNamespaceClash;
    }

    /**
     * @param allowFilterNamespaceClash the allowFilterNamespaceClash to set
     */
    public void setAllowFilterNamespaceClash(boolean allowFilterNamespaceClash) {
        this.allowFilterNamespaceClash = allowFilterNamespaceClash;
    }
    
    /**
     * @return the messageHeaderFiltersMap
     */
    public Map<String, MessageHeaderFilter> getMessageHeaderFiltersMap() {
        return messageHeaderFiltersMap;
    }

    /**
     * @param relayHeaders the relayHeaders to set
     */
    public void setRelayHeaders(boolean relayHeaders) {
        this.relayHeaders = relayHeaders;
    }

    /**
     * @return the relayHeaders
     */
    public boolean isRelayHeaders() {
        return relayHeaders;
    }

    /**
     * @return the relayAllMessageHeaders
     */
    public boolean isRelayAllMessageHeaders() {
        return relayAllMessageHeaders;
    }

    /**
     * @param relayAllMessageHeaders the relayAllMessageHeaders to set
     */
    public void setRelayAllMessageHeaders(boolean relayAllMessageHeaders) {
        this.relayAllMessageHeaders = relayAllMessageHeaders;
    }

}
