/*
 * Copyright 2004,2005 The Apache Software Foundation.
 *
 * 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.apache.rahas;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Reader;
import java.io.StringReader;
import java.text.DateFormat;
import java.util.Date;
import java.util.Properties;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMException;
import org.apache.axiom.om.OMFactory;
import org.apache.axiom.om.OMMetaFactory;
import org.apache.axiom.om.OMXMLBuilderFactory;
import org.apache.axiom.om.OMXMLParserWrapper;
import org.apache.axiom.om.OMXMLStreamReaderConfiguration;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.wss4j.common.util.DateUtil;
import org.apache.wss4j.dom.WSConstants;

/**
 * This represents a security token which can have either one of 4 states. <ul> <li>ISSUED</li> <li>EXPIRED</li>
 * <li>CACELLED</li> <li>RENEWED</li> </ul> Also this holds the <code>OMElement</code>s representing the token in its
 * present state and the previous state.
 * <p>
 * These tokens are stored using the storage mechanism provided via the <code>TokenStorage</code> interface.
 *
 * @see org.apache.rahas.TokenStorage
 */
public class Token implements Externalizable {

    private static Log log = LogFactory.getLog(Token.class);

    public final static int ISSUED = 1;

    public final static int EXPIRED = 2;

    public final static int CANCELLED = 3;

    public final static int RENEWED = 4;

    /**
     * Token identifier
     */
    private String id;

    /**
     * Current state of the token
     */
    private int state = -1;

    /**
     * The actual token in its current state
     */
    private OMElement token;

    /**
     * The token in its previous state
     */
    private OMElement previousToken;

    /**
     * The RequestedAttachedReference element NOTE : The oasis-200401-wss-soap-message-security-1.0 spec allows an
     * extensibility mechanism for wsse:SecurityTokenReference and wsse:Reference. Hence we cannot limit to the
     * wsse:SecurityTokenReference\wsse:Reference case and only hold the URI and the ValueType values.
     */
    private OMElement attachedReference;

    /**
     * The RequestedUnattachedReference element NOTE : The oasis-200401-wss-soap-message-security-1.0 spec allows an
     * extensibility mechanism for wsse:SecurityTokenRefence and wsse:Reference. Hence we cannot limit to the
     * wsse:SecurityTokenReference\wsse:Reference case and only hold the URI and the ValueType values.
     */
    private OMElement unattachedReference;

    /**
     * A bag to hold any other properties
     */
    private Properties properties;

    /**
     * A flag to assist the TokenStorage
     */
    private boolean changed;

    /**
     * The secret associated with the Token
     */
    private byte[] secret;

    /**
     * Created time
     */
    private Date created;

    /**
     * Expiration time
     */
    private Date expires;

    /**
     * Issuer end point address
     */
    private String issuerAddress;

    private String encrKeySha1Value;

    public Token() {
    }

    public Token(String id, Date created, Date expires) {
        this.id = id;
        this.created = created;
        this.expires = expires;
    }

    public Token(String id, OMElement tokenElem, Date created, Date expires)
        throws TrustException {
        this.id = id;
        OMMetaFactory metaFactory = OMAbstractFactory.getMetaFactory(OMAbstractFactory.FEATURE_DOM);
        OMXMLStreamReaderConfiguration configuration = new OMXMLStreamReaderConfiguration();
        configuration.setNamespaceURIInterning(true);
        this.token = OMXMLBuilderFactory.createStAXOMBuilder(metaFactory.getOMFactory(),
                tokenElem.getXMLStreamReader(true, configuration)).getDocumentElement();
        this.created = created;
        this.expires = expires;
    }

    public Token(String id, OMElement tokenElem, OMElement lifetimeElem)
        throws TrustException {
        this.id = id;
        OMMetaFactory metaFactory = OMAbstractFactory.getMetaFactory(OMAbstractFactory.FEATURE_DOM);
        OMXMLStreamReaderConfiguration configuration = new OMXMLStreamReaderConfiguration();
        configuration.setNamespaceURIInterning(true);
        this.token = OMXMLBuilderFactory.createStAXOMBuilder(metaFactory.getOMFactory(),
                tokenElem.getXMLStreamReader(true, configuration)).getDocumentElement();
        this.processLifeTime(lifetimeElem);
    }

    /**
     * @param lifetimeElem
     * @throws TrustException
     */
    private void processLifeTime(OMElement lifetimeElem)
        throws TrustException {
        try {
            OMElement createdElem =
                lifetimeElem.getFirstChildWithName(new QName(WSConstants.WSU_NS, WSConstants.CREATED_LN));

            LocalDateTime localDateCreated = LocalDateTime.parse(createdElem.getText(), DateUtil.getDateTimeFormatter(true));
            this.created = Date.from(localDateCreated.atZone(ZoneOffset.UTC).toInstant());

            OMElement expiresElem =
                lifetimeElem.getFirstChildWithName(new QName(WSConstants.WSU_NS, WSConstants.EXPIRES_LN));

            LocalDateTime localDateExpires = LocalDateTime.parse(expiresElem.getText(), DateUtil.getDateTimeFormatter(true));
            this.expires = Date.from(localDateExpires.atZone(ZoneOffset.UTC).toInstant());
            
        } catch (OMException e) {
            throw new TrustException("lifeTimeProcessingError", new String[]{lifetimeElem.toString()}, e);
        } catch (Exception e) {
            throw new TrustException("lifeTimeProcessingError", new String[]{lifetimeElem.toString()}, e);
        }
    }

    /**
     * @return Returns the changed.
     */
    public boolean isChanged() {
        return changed;
    }

    /**
     * @param chnaged The changed to set.
     */
    public void setChanged(boolean chnaged) {
        this.changed = chnaged;
    }

    /**
     * @return Returns the properties.
     */
    public Properties getProperties() {
        return properties;
    }

    /**
     * @param properties The properties to set.
     */
    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    /**
     * @return Returns the state.
     */
    public int getState() {
        return state;
    }

    /**
     * @param state The state to set.
     */
    public void setState(int state) {
        this.state = state;
    }

    /**
     * @return Returns the token.
     */
    public OMElement getToken() {
        return token;
    }

    /**
     * @param token The token to set.
     */
    public void setToken(OMElement token) {
        this.token = token;
    }

    /**
     * @return Returns the id.
     */
    public String getId() {
        return id;
    }

    /**
     * @return Returns the presivousToken.
     */
    public OMElement getPreviousToken() {
        return previousToken;
    }

    /**
     * @param presivousToken The presivousToken to set.
     */
    public void setPreviousToken(OMElement presivousToken) {
    	OMMetaFactory metaFactory = OMAbstractFactory.getMetaFactory(OMAbstractFactory.FEATURE_DOM);
        this.previousToken = OMXMLBuilderFactory.createStAXOMBuilder(metaFactory.getOMFactory(),
                presivousToken.getXMLStreamReader()).getDocumentElement();
    }

    /**
     * @return Returns the secret.
     */
    public byte[] getSecret() {
        return secret;
    }

    /**
     * @param secret The secret to set.
     */
    public void setSecret(byte[] secret) {
        this.secret = secret;
    }

    /**
     * @return Returns the attachedReference.
     */
    public OMElement getAttachedReference() {
        return attachedReference;
    }

    /**
     * @param attachedReference The attachedReference to set.
     */
    public void setAttachedReference(OMElement attachedReference) {
        if (attachedReference != null) {
        	OMMetaFactory metaFactory = OMAbstractFactory.getMetaFactory(OMAbstractFactory.FEATURE_DOM);
            this.attachedReference =
                OMXMLBuilderFactory.createStAXOMBuilder(metaFactory.getOMFactory(),
                        attachedReference.getXMLStreamReader()).getDocumentElement();
        }
    }

    /**
     * @return Returns the unattachedReference.
     */
    public OMElement getUnattachedReference() {
        return unattachedReference;
    }

    /**
     * @param unattachedReference The unattachedReference to set.
     */
    public void setUnattachedReference(OMElement unattachedReference) {
        if (unattachedReference != null) {
        	OMMetaFactory metaFactory = OMAbstractFactory.getMetaFactory(OMAbstractFactory.FEATURE_DOM);
            this.unattachedReference =
                OMXMLBuilderFactory.createStAXOMBuilder(metaFactory.getOMFactory(),
                        unattachedReference.getXMLStreamReader()).getDocumentElement();
        }
    }

    /**
     * @return Returns the created.
     */
    public Date getCreated() {
        return created;
    }

    /**
     * @return Returns the expires.
     */
    public Date getExpires() {
        return expires;
    }

    /**
     * @param expires The expires to set.
     */
    public void setExpires(Date expires) {
        this.expires = expires;
    }

    public String getIssuerAddress() {
        return issuerAddress;
    }

    public void setIssuerAddress(String issuerAddress) {
        this.issuerAddress = issuerAddress;
    }

    /**
     * Implementing serialize logic according to our own protocol. We had to follow this, because
     * OMElement class is not serializable. Making OMElement serializable will have an huge impact
     * on other components. Therefore implementing serialization logic according to a manual
     * protocol.
     * @param out Stream which writes serialized bytes.
     * @throws IOException If unable to serialize particular member.
     */
    public void writeExternal(ObjectOutput out)
        throws IOException {

        out.writeObject(this.id);

        out.writeInt(this.state);
        
        String stringElement = convertOMElementToString(this.token);
        out.writeObject(stringElement);

        stringElement = convertOMElementToString(this.previousToken);
        out.writeObject(stringElement);

        stringElement = convertOMElementToString(this.attachedReference);
        out.writeObject(stringElement);

        stringElement = convertOMElementToString(this.unattachedReference);
        out.writeObject(stringElement);

        out.writeObject(this.properties);

        out.writeBoolean(this.changed);

        int secretLength = 0;
        if (null != this.secret) {
            secretLength = this.secret.length;
        }

        // First write the length of secret
        out.writeInt(secretLength);
        if (0 != secretLength) {
            out.write(this.secret);
        }

        out.writeObject(this.created);

        out.writeObject(this.expires);

        out.writeObject(this.issuerAddress);

        out.writeObject(this.encrKeySha1Value);
    }

    /**
     * Implementing de-serialization logic in accordance with the serialization logic.
     * @param in Stream which used to read data.
     * @throws IOException If unable to de-serialize particular data member.
     * @throws ClassNotFoundException 
     */
    public void readExternal(ObjectInput in)
        throws IOException, ClassNotFoundException {

        this.id = (String)in.readObject();

        this.state = in.readInt();

        String stringElement = (String)in.readObject();
        this.token = convertStringToOMElement(stringElement);

        stringElement = (String)in.readObject();
        this.previousToken = convertStringToOMElement(stringElement);

        stringElement = (String)in.readObject();
        this.attachedReference = convertStringToOMElement(stringElement);

        stringElement = (String)in.readObject();
        this.unattachedReference = convertStringToOMElement(stringElement);

        this.properties = (Properties)in.readObject();

        this.changed = in.readBoolean();

        // Read the length of the secret
        int secretLength = in.readInt();

        if (0 != secretLength) {
            byte[] buffer = new byte[secretLength];
            if (secretLength != in.read(buffer)) {
                throw new IllegalStateException("Bytes read from the secret key is not equal to serialized length");
            }
            this.secret = buffer;
        }else{
            this.secret = null;
        }

        this.created = (Date)in.readObject();

        this.expires = (Date)in.readObject();

        this.issuerAddress = (String)in.readObject();

        this.encrKeySha1Value = (String)in.readObject();
    }

    private String convertOMElementToString(OMElement element)
        throws IOException {
        String serializedToken = "";

        if (null == element) {
            return serializedToken;
        }

        try {
            serializedToken = element.toStringWithConsume();
        } catch (XMLStreamException e) {
            throw new IOException("Could not serialize token OM element");
        }

        return serializedToken;
    }

    private OMElement convertStringToOMElement(String stringElement)
        throws IOException {

        if (null == stringElement || stringElement.trim().equals("")) {
            return null;
        }

        try {
            Reader in = new StringReader(stringElement);
            XMLStreamReader parser = XMLInputFactory.newInstance().createXMLStreamReader(in);
            OMXMLParserWrapper builder = OMXMLBuilderFactory.createStAXOMBuilder(parser);
            OMElement documentElement = builder.getDocumentElement();

            XMLStreamReader llomReader = documentElement.getXMLStreamReader();
            OMMetaFactory metaFactory = OMAbstractFactory.getMetaFactory(OMAbstractFactory.FEATURE_DOM);
            OMFactory doomFactory = metaFactory.getOMFactory();
            OMXMLParserWrapper doomBuilder = OMXMLBuilderFactory.createStAXOMBuilder(doomFactory, llomReader);
            return doomBuilder.getDocumentElement();
            
        } catch (XMLStreamException e) {
            log.error("Cannot convert de-serialized string to OMElement. Could not create XML stream.", e);
            // IOException only has a constructor supporting exception chaining starting with Java 1.6
            IOException ex = new IOException("Cannot convert de-serialized string to OMElement. Could not create XML stream.");
            ex.initCause(e);
            throw ex;
        }
    }
}
