/**
 * 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.cxf.ws.security.tokenstore;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.io.StringReader;
import java.security.Key;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Date;
import java.util.Map;

import javax.xml.stream.XMLStreamException;

import org.w3c.dom.Element;
import org.apache.cxf.helpers.DOMUtils;
import org.apache.cxf.security.SecurityContext;
import org.apache.cxf.staxutils.StaxUtils;
import org.apache.cxf.staxutils.W3CDOMStreamWriter;
import org.apache.wss4j.common.crypto.Crypto;
import org.apache.wss4j.common.token.Reference;
import org.apache.wss4j.common.util.DOM2Writer;
import org.apache.wss4j.common.util.XMLUtils;
import org.apache.wss4j.dom.WSConstants;
import org.apache.wss4j.dom.util.XmlSchemaDateFormat;


/**
 * 
 */
public class SecurityToken implements Serializable {
    
    /**
     * This tag holds an ID of a Bootstrap SecurityToken stored in the TokenStore 
     */
    public static final String BOOTSTRAP_TOKEN_ID = "bootstrap_security_token_id";
    
    /**
     * 
     */
    private static final long serialVersionUID = -8220267049304000696L;


    /**
     * Token identifier
     */
    private String id;
    
    /**
     * WSU Identifier of the token
     */
    private String wsuId;
    
    /**
     * The actual token in its current state
     */
    private transient Element token;
    
    /**
     * The String representation of the token (The token can't be serialized as it's a DOM Element) 
     */
    private String tokenStr;
    
    /**
     * 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 transient Element attachedReference;
    
    /**
     * The RequestedUnattachedReference 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 transient Element unattachedReference;
    
    /**
     * A bag to hold any other properties
     */
    private Map<String, Object> properties;

    /**
     * The secret associated with the Token
     */
    private transient byte[] secret;
    
    /**
     * Some binary data associated with the token
     */
    private byte[] data;
    
    /**
     * A key associated with the token
     */
    private transient Key key;
    
    /**
     * Created time
     */
    private Date created;
    
    /**
     * Expiration time
     */
    private Date expires;
    
    /**
     * Issuer end point address
     */
    private String issuerAddress;
    
    /**
     * If an encrypted key, this contains the sha1 for the key
     */
    private String encrKeySha1Value;
    
    /**
     * A hash code associated with this token.
     */
    private int tokenHash;
    
    /**
     * This holds the identifier of another SecurityToken which represents a transformed
     * version of this token. 
     */
    private String transformedTokenIdentifier;
    
    /**
     * The tokenType
     */
    private String tokenType;

    private X509Certificate x509cert;

    private transient Crypto crypto;
    
    /**
     * The principal of this SecurityToken
     */
    private Principal principal;
    /**
     * The SecurityContext originally associated with this token
     */
    private transient SecurityContext securityContext;
    
    public SecurityToken() {
        
    }
    
    public SecurityToken(String id) {
        this.id = XMLUtils.getIDFromReference(id);
    }

    public SecurityToken(String id, Date created, Date expires) {
        this.id = XMLUtils.getIDFromReference(id);
        
        if (created != null) {
            this.created = new Date(created.getTime());
        }
        if (expires != null) {
            this.expires = new Date(expires.getTime());
        }
    }
    
    public SecurityToken(String id,
                 Element tokenElem,
                 Date created,
                 Date expires) {
        this.id = XMLUtils.getIDFromReference(id);
        
        this.token = cloneElement(tokenElem);
        if (created != null) {
            this.created = new Date(created.getTime());
        }
        if (expires != null) {
            this.expires = new Date(expires.getTime());
        }
    }

    public SecurityToken(String id,
                 Element tokenElem,
                 Element lifetimeElem) {
        this.id = XMLUtils.getIDFromReference(id);
        
        this.token = cloneElement(tokenElem);
        if (lifetimeElem != null) {
            processLifeTime(lifetimeElem);
        }
    }
    
    private static Element cloneElement(Element el) {
        try {
            W3CDOMStreamWriter writer = new W3CDOMStreamWriter();
            writer.setNsRepairing(true);
            StaxUtils.copy(el, writer);
            return writer.getDocument().getDocumentElement();
        } catch (Exception ex) {
            //ignore
        }
        return el;
    }
    /**
     * @param lifetimeElem
     * @throws TrustException 
     */
    private void processLifeTime(Element lifetimeElem) {
        try {
            Element createdElem = 
                DOMUtils.getFirstChildWithName(lifetimeElem,
                                                WSConstants.WSU_NS,
                                                WSConstants.CREATED_LN);
            DateFormat zulu = new XmlSchemaDateFormat();
            
            this.created = zulu.parse(DOMUtils.getContent(createdElem));

            Element expiresElem = 
                DOMUtils.getFirstChildWithName(lifetimeElem,
                                                WSConstants.WSU_NS,
                                                WSConstants.EXPIRES_LN);
            this.expires = zulu.parse(DOMUtils.getContent(expiresElem));
        } catch (ParseException e) {
            //shouldn't happen
        }
    }

    /**
     * @return Returns the properties.
     */
    public Map<String, Object> getProperties() {
        return properties;
    }

    /**
     * @param properties The properties to set.
     */
    public void setProperties(Map<String, Object> properties) {
        this.properties = properties;
    }

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

    /**
     * @param token The token to set.
     */
    public void setToken(Element token) {
        if (token != null) {
            this.token = cloneElement(token);
        }
    }

    /**
     * Get the identifier corresponding to a transformed version of this token
     */
    public String getTransformedTokenIdentifier() {
        return transformedTokenIdentifier;
    }

    /**
     * Set the identifier corresponding to a transformed version of this token
     */
    public void setTransformedTokenIdentifier(String transformedTokenIdentifier) {
        this.transformedTokenIdentifier = transformedTokenIdentifier;
    }
    
    /**
     * Set the id
     */
    public void setId(String id) {
        this.id = XMLUtils.getIDFromReference(id);
    }
    
    /**
     * @return Returns the id.
     */
    public String getId() {
        return id;
    }

    /**
     * @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 Element getAttachedReference() {
        return attachedReference;
    }

    /**
     * @param attachedReference The attachedReference to set.
     */
    public void setAttachedReference(Element attachedReference) {
        if (attachedReference != null) {
            this.attachedReference = cloneElement(attachedReference);
        }
    }

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

    /**
     * @param unattachedReference The unattachedReference to set.
     */
    public void setUnattachedReference(Element unattachedReference) {
        if (unattachedReference != null) {
            this.unattachedReference = cloneElement(unattachedReference);
        }
    }

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

    /**
     * @return Returns the expires.
     */
    public Date getExpires() {
        if (expires == null) {
            return null;
        }
        return (Date)expires.clone();
    }
    
    /**
     * Return whether this SecurityToken is expired or not
     */
    public boolean isExpired() {
        if (expires != null) {
            Date rightNow = new Date();
            if (expires.before(rightNow)) {
                return true;
            }
        }
        return false;
    }
    
    /**
     * Return whether this SecurityToken is about to expire or not
     */
    public boolean isAboutToExpire(long secondsToExpiry) {
        if (expires != null && secondsToExpiry > 0) {
            Date rightNow = new Date();
            rightNow.setTime(rightNow.getTime() + (secondsToExpiry * 1000L));
            if (expires.before(rightNow)) {
                return true;
            }
        }
        return false;
    }

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

    public String getIssuerAddress() {
        return issuerAddress;
    }

    public void setIssuerAddress(String issuerAddress) {
        this.issuerAddress = issuerAddress;
    }
    
    /**
     * @param sha SHA1 of the encrypted key
     */
    public void setSHA1(String sha) {
        this.encrKeySha1Value = sha;
    }
    
    /** 
     * @return SHA1 value of the encrypted key 
     */
    public String getSHA1() {
        return encrKeySha1Value;
    }
    
    public String getTokenType() {
        return tokenType;
    }
    
    public void setTokenType(String s) {
        tokenType = s;
    }
    
    public void setWsuId(String wsuId) {
        this.wsuId = wsuId;
    }
    
    public String getWsuId() {
        if (wsuId != null) {
            return wsuId;
        }
        Element elem = getAttachedReference();
        if (elem != null) {
            String t = getIdFromSTR(elem);
            if (t != null) {
                return t;
            }
        }
        elem = getUnattachedReference();
        if (elem != null) {
            String t = getIdFromSTR(elem);
            if (t != null) {
                return t;
            }
        }
        return null;
    }   
    
    public static String getIdFromSTR(Element str) {
        Element child = DOMUtils.getFirstElement(str);
        if (child == null) {
            return null;
        }
        
        if ("KeyInfo".equals(child.getLocalName())
            && WSConstants.SIG_NS.equals(child.getNamespaceURI())) {
            return DOMUtils.getContent(child);
        } else if (Reference.TOKEN.getLocalPart().equals(child.getLocalName())
            && Reference.TOKEN.getNamespaceURI().equals(child.getNamespaceURI())) {
            return child.getAttributeNS(null, "URI").substring(1);
        }
        return null;
    }
    
    public void setX509Certificate(X509Certificate cert, Crypto cpt) {
        x509cert = cert;
        crypto = cpt;
    }
    
    public X509Certificate getX509Certificate() {
        return x509cert;
    }
    
    public Crypto getCrypto() {
        return crypto;
    }
    
    /**
     * Set a hash code associated with this token.
     * @param hash a hash code associated with this token
     */
    public void setTokenHash(int hash) {
        tokenHash = hash;
    }
    
    /**
     * Get a hash code associated with this token.
     * @return a hash code associated with this token.
     */
    public int getTokenHash() {
        return tokenHash;
    }
    
    /**
     * Set the principal associated with this SecurityToken
     * @param principal the principal associated with this SecurityToken
     */
    public void setPrincipal(Principal principal) {
        this.principal = principal;
    }
    
    /**
     * Get the principal associated with this SecurityToken
     * @return the principal associated with this SecurityToken
     */
    public Principal getPrincipal() {
        return principal;
    }
    
    /**
     * Set the SecurityContext associated with this SecurityToken
     * @param securityContext the SecurityContext associated with this SecurityToken
     */
    public void setSecurityContext(SecurityContext securityContext) {
        this.securityContext = securityContext;
    }
    
    /**
     * Get the SecurityContext associated with this SecurityToken
     * @return the SecurityContext associated with this SecurityToken
     */
    public SecurityContext getSecurityContext() {
        return securityContext;
    }

    public Key getKey() {
        return key;
    }

    public void setKey(Key key) {
        this.key = key;
    }

    public byte[] getData() {
        return data;
    }

    public void setData(byte[] data) {
        this.data = data;
    }
    
    private void writeObject(ObjectOutputStream stream) throws IOException {
        if (token != null && tokenStr == null) {
            tokenStr = DOM2Writer.nodeToString(token);
        }
        stream.defaultWriteObject();
    }
    
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException, XMLStreamException {
        in.defaultReadObject();
        
        if (token == null && tokenStr != null) {
            token = StaxUtils.read(new StringReader(tokenStr)).getDocumentElement();
        }
    }
} 
