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

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

import org.apache.axiom.om.format.xop.ContentTransferEncodingPolicy;
import org.apache.axiom.om.format.xop.ContentTypeProvider;
import org.apache.axiom.om.impl.MTOMConstants;
import org.apache.axiom.om.util.StAXWriterConfiguration;
import org.apache.axiom.soap.SOAPVersion;
import org.apache.axiom.util.UIDGenerator;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Formats options for OM Output.
 * <p>
 * Setting of all the properties in a OMOutputFormat should be done before calling the
 * getContentType() method. It is advised to set all the properties at the creation time of the
 * OMOutputFormat and not to change them later.
 */
public class OMOutputFormat {
    
    private static final Log log = LogFactory.getLog(OMOutputFormat.class);
    
    private String mimeBoundary;
    private String rootContentId;
    private int nextid;
    private boolean doOptimize;
    private boolean doingSWA;
    private boolean isSoap11;
    private int optimizedThreshold;
    
    /** Field DEFAULT_CHAR_SET_ENCODING. Specifies the default character encoding scheme to be used. */
    public static final String DEFAULT_CHAR_SET_ENCODING = "utf-8";

    private String charSetEncoding;
    private String xmlVersion;
    private String contentType;
    
    /**
     * Flag set if {@link #contentType} has been set explicitly through
     * {@link #setContentType(String)}. If this attribute is <code>false</code> and
     * {@link #contentType} is non null, then it was calculated by {@link #getContentType()}.
     */
    private boolean contentTypeSet;
    
    private boolean ignoreXMLDeclaration;
    private boolean autoCloseWriter;

    public static final String ACTION_PROPERTY = "action";
    
    @SuppressWarnings("deprecation")
    private StAXWriterConfiguration writerConfiguration;
    
    private ContentTypeProvider contentTypeProvider;
    private ContentTransferEncodingPolicy contentTransferEncodingPolicy;

    /**
     * @deprecated Use {@link ContentTransferEncodingPolicy#USE_BASE64_FOR_NON_TEXTUAL_PARTS} instead.
     */
    public static final String USE_CTE_BASE64_FOR_NON_TEXTUAL_ATTACHMENTS = 
        "org.apache.axiom.om.OMFormat.use.cteBase64.forNonTextualAttachments";
    
    /**
     * @deprecated As of version 1.2.13, Axiom always respects the order of attachments.
     */
    public static final String RESPECT_SWA_ATTACHMENT_ORDER =
        "org.apache.axiom.om.OMFormat.respectSWAAttachmentOrder";
    
    /**
     * @deprecated As of version 1.2.13, Axiom always respects the order of attachments.
     */
    public static final Boolean RESPECT_SWA_ATTACHMENT_ORDER_DEFAULT =
        Boolean.TRUE;
    
    
    private Map<String,Object> map;  // Map of generic properties


    public OMOutputFormat() {
        isSoap11 = true;
    }
    
    /**
     * Constructs a new instance by copying the configuration from an existing instance. Note that
     * this will only copy configuration data, but not information that is subject to
     * auto-generation, such as the root content ID or the MIME boundary.
     * 
     * @param format
     *            the existing instance
     */
    public OMOutputFormat(OMOutputFormat format) {
        doOptimize = format.doOptimize;
        doingSWA = format.doingSWA;
        isSoap11 = format.isSoap11;
        optimizedThreshold = format.optimizedThreshold;
        charSetEncoding = format.charSetEncoding;
        xmlVersion = format.xmlVersion;
        if (format.contentTypeSet) {
            contentTypeSet = true;
            contentType = format.contentType;
        }
        ignoreXMLDeclaration = format.ignoreXMLDeclaration;
        autoCloseWriter = format.autoCloseWriter;
        writerConfiguration = format.writerConfiguration;
        contentTypeProvider = format.contentTypeProvider;
        contentTransferEncodingPolicy = format.contentTransferEncodingPolicy;
        if (format.map != null) {
            map = new HashMap<String,Object>(format.map);
        }
    }
    
    /**
     * @param key String
     * @return property or null
     */
    public Object getProperty(String key) {
        if (map == null) {
            return null;
        }
        return map.get(key);
    }

    /**
     * @param key String
     * @param value Object
     * @return old value or null
     */
    public Object setProperty(String key, Object value) {
        if (map == null) {
            map = new HashMap<String,Object>();
        }
        return map.put(key, value);
    }
    
    /**
     * @param key
     * @return true if known key
     */
    public boolean containsKey(String key) {
        if (map == null) {
            return false;
        } 
        return map.containsKey(key);
    }

    /**
     * Indicates whether the document should be serialized using MTOM.
     * 
     * @return <code>true</code> if the document should be serialized using MTOM; <code>false</code>
     *         otherwise; the return value is always <code>false</code> if {@link #isDoingSWA()}
     *         returns <code>true</code>
     */
    public boolean isOptimized() {
        return doOptimize && !doingSWA;  // optimize is disabled if SWA
    }

    /**
     * Return the content-type value that should be written with the message.
     * (i.e. if optimized, then a multipart/related content-type is returned).
     * @return content-type value
     */
    public String getContentType() {
       
        String ct = null;
        if (log.isDebugEnabled()) {
            log.debug("Start getContentType: " + toString());
        }
        if (contentType == null) {
            if (isSoap11) {
                contentType = SOAPVersion.SOAP11.getMediaType().toString();
            } else {
                contentType = SOAPVersion.SOAP12.getMediaType().toString();
            }
        }
        // If MTOM or SWA, the returned content-type is an 
        // appropriate multipart/related content type.
        if (isOptimized()) {
            ct = this.getContentTypeForMTOM(contentType);
        } else if (isDoingSWA()) {
            ct = this.getContentTypeForSwA(contentType);
        } else {
            ct = contentType;
        }
        if (log.isDebugEnabled()) {
            log.debug("getContentType= {" + ct + "}   " + toString());
        }
        return ct;
    }
    
    /**
     * Set a raw content-type 
     * (i.e. "text/xml" (SOAP 1.1) or "application/xml" (REST))
     * If this method is not invoked, OMOutputFormat will choose
     * a content-type value consistent with the soap version.
     * @param c
     */
    public void setContentType(String c) {
        contentTypeSet = true;
        contentType = c;
    }

    public String getMimeBoundary() {
        if (mimeBoundary == null) {
            mimeBoundary = UIDGenerator.generateMimeBoundary();
        }
        return mimeBoundary;
    }

    public String getRootContentId() {
        if (rootContentId == null) {
            rootContentId = "0." + UIDGenerator.generateContentId();
        }
        return rootContentId;
    }

    public String getNextContentId() {
        nextid++;
        return nextid + "." + UIDGenerator.generateContentId();
    }

    /**
     * Returns the character set encoding scheme.
     *
     * @return Returns encoding string or null if it has not been set.
     */
    public String getCharSetEncoding() {
        return this.charSetEncoding;
    }

    public void setCharSetEncoding(String charSetEncoding) {
        this.charSetEncoding = charSetEncoding;
    }

    public String getXmlVersion() {
        return xmlVersion;
    }

    public void setXmlVersion(String xmlVersion) {
        this.xmlVersion = xmlVersion;
    }

    public void setSOAP11(boolean b) {
        isSoap11 = b;
    }

    public boolean isSOAP11() {
        return isSoap11;
    }

    public boolean isIgnoreXMLDeclaration() {
        return ignoreXMLDeclaration;
    }

    public void setIgnoreXMLDeclaration(boolean ignoreXMLDeclaration) {
        this.ignoreXMLDeclaration = ignoreXMLDeclaration;
    }

    /**
     * Specifies that the document should be serialized using MTOM. Note that this setting is
     * ignored if SwA is enabled using {@link #setDoingSWA(boolean)}.
     * 
     * @param optimize
     *            <code>true</code> if the document should be serialized using MTOM;
     *            <code>false</code> otherwise
     */
    public void setDoOptimize(boolean optimize) {
        doOptimize = optimize;
    }

    /**
     * Indicates whether the document should be serialized using SwA.
     * 
     * @return <code>true</code> if the document should be serialized using SwA; <code>false</code>
     *         otherwise
     */
    public boolean isDoingSWA() {
        return doingSWA;
    }

    /**
     * Specifies that the document should be serialized using SwA (SOAP with Attachments). When SwA
     * is enabled, then any configuration done using {@link #setDoOptimize(boolean)} is ignored.
     * 
     * @param doingSWA
     *            <code>true</code> if the document should be serialized using SwA;
     *            <code>false</code> otherwise
     */
    public void setDoingSWA(boolean doingSWA) {
        this.doingSWA = doingSWA;
    }

    /**
     * Generates a Content-Type value for MTOM messages.  This is a MIME Multipart/Related
     * Content-Type value as defined by RFC 2387 and the XOP specification.  The generated
     * header will look like the following:
     * 
     *   Content-Type: multipart/related; boundary="[MIME BOUNDARY VALUE]"; 
     *      type="application/xop+xml"; 
     *      start="[MESSAGE CONTENT ID]"; 
     *      start-info="[MESSAGE CONTENT TYPE]";
     * 
     * @param SOAPContentType
     * @return TODO
     */
    public String getContentTypeForMTOM(String SOAPContentType) {
        // If an action was set, we need to include it within the value 
        // for the start-info attribute.  
        if (containsKey(ACTION_PROPERTY)) {
            String action = (String) getProperty(ACTION_PROPERTY);
            if (action != null && action.length() > 0) {
                SOAPContentType = SOAPContentType + "; action=\\\"" + action + "\\\"";   
            }                     
        }
        
        StringBuffer sb = new StringBuffer();
        sb.append("multipart/related");
        sb.append("; ");
        sb.append("boundary=");
        // The value of the boundary parameter must be enclosed in double quotation  
        // marks, according to the Basic Profile 2.0 Specification, Rule R1109:
        // "Parameters on the Content-Type MIME header field-value in a request 
        // MESSAGE MUST be a quoted string."
        sb.append("\"");
        sb.append(getMimeBoundary());
        sb.append("\"");
        sb.append("; ");
        sb.append("type=\"" + MTOMConstants.MTOM_TYPE + "\"");
        sb.append("; ");
        sb.append("start=\"<").append(getRootContentId()).append(">\"");
        sb.append("; ");
        sb.append("start-info=\"").append(SOAPContentType).append("\"");
        return sb.toString();
    }

    public String getContentTypeForSwA(String SOAPContentType) {
        StringBuffer sb = new StringBuffer();
        sb.append("multipart/related");
        sb.append("; ");
        sb.append("boundary=");
        // The value of the boundary parameter must be enclosed in double quotation  
        // marks, according to the Basic Profile 2.0 Specification, Rule R1109:
        // "Parameters on the Content-Type MIME header field-value in a request 
        // MESSAGE MUST be a quoted string."
        sb.append("\"");
        sb.append(getMimeBoundary());
        sb.append("\"");
        sb.append("; ");
        sb.append("type=\"").append(SOAPContentType).append("\"");
        sb.append("; ");
        sb.append("start=\"<").append(getRootContentId()).append(">\"");
        return sb.toString();
    }

    /**
     * @deprecated
     */
    public boolean isAutoCloseWriter() {
        return autoCloseWriter;
    }

    /**
     * @deprecated
     */
    public void setAutoCloseWriter(boolean autoCloseWriter) {
        this.autoCloseWriter = autoCloseWriter;
    }

    public void setMimeBoundary(String mimeBoundary) {
        this.mimeBoundary = mimeBoundary;
    }
    public void setRootContentId(String rootContentId) {
		this.rootContentId = rootContentId;
	}

    
    /**
     * Use toString for logging state of the OMOutputFormat
     */
    @Override
    public String toString() {
        StringBuffer sb = new StringBuffer();
        sb.append("OMOutputFormat [");
        
        sb.append(" mimeBoundary =");
        sb.append(mimeBoundary);
        
        sb.append(" rootContentId=");
        sb.append(rootContentId);
        
        sb.append(" doOptimize=");
        sb.append(doOptimize);
        
        sb.append(" doingSWA=");
        sb.append(doingSWA);
        
        sb.append(" isSOAP11=");
        sb.append(isSoap11);
        
        sb.append(" charSetEncoding=");
        sb.append(charSetEncoding);
        
        sb.append(" xmlVersion=");
        sb.append(xmlVersion);
        
        sb.append(" contentType=");
        sb.append(contentType);
        
        sb.append(" ignoreXmlDeclaration=");
        sb.append(ignoreXMLDeclaration);
        
        sb.append(" autoCloseWriter=");
        sb.append(autoCloseWriter);
        
        // TODO Print all properties
        sb.append(" actionProperty=");
        sb.append(getProperty(ACTION_PROPERTY));

        sb.append(" optimizedThreshold=");
        sb.append(optimizedThreshold);
        
        sb.append("]");
        return sb.toString();
        
    }

    public void setOptimizedThreshold(int optimizedThreshold) {
        this.optimizedThreshold = optimizedThreshold;
    }
    
    public int getOptimizedThreshold() {
        return optimizedThreshold;
    }

    /**
     * Get the currently configured StAX writer configuration.
     * 
     * @return the current configuration; {@link StAXWriterConfiguration#DEFAULT} if none has been
     *         set explicitly
     * @deprecated
     */
    public StAXWriterConfiguration getStAXWriterConfiguration() {
        return writerConfiguration == null ? StAXWriterConfiguration.DEFAULT : writerConfiguration;
    }
     
    /**
     * Set the StAX writer configuration that will be used when requesting an
     * {@link javax.xml.stream.XMLStreamWriter} from {@link org.apache.axiom.om.util.StAXUtils}.
     * 
     * @param writerConfiguration
     *            the configuration
     * @deprecated
     */
    public void setStAXWriterConfiguration(StAXWriterConfiguration writerConfiguration) {
        this.writerConfiguration = writerConfiguration;
    }

    public ContentTypeProvider getContentTypeProvider() {
        return contentTypeProvider;
    }

    public void setContentTypeProvider(ContentTypeProvider contentTypeProvider) {
        this.contentTypeProvider = contentTypeProvider;
    }

    public ContentTransferEncodingPolicy getContentTransferEncodingPolicy() {
        return contentTransferEncodingPolicy;
    }

    public void setContentTransferEncodingPolicy(
            ContentTransferEncodingPolicy contentTransferEncodingPolicy) {
        this.contentTransferEncodingPolicy = contentTransferEncodingPolicy;
    }
}
