com.borhan.client.BorhanClientBase.java Source code

Java tutorial

Introduction

Here is the source code for com.borhan.client.BorhanClientBase.java

Source

// ===================================================================================================
//                     _  __    _ _
//                    | |/ /__ _| | |_ _  _ _ _ __ _
//                    | ' </ _` | |  _| || | '_/ _` |
//                    |_|\_\__,_|_|\__|\_,_|_| \__,_|
//
// This file is part of the Borhan Collaborative Media Suite which allows users
// to do with audio, video, and animation what Wiki platfroms allow them to do with
// text.
//
// Copyright (C) 2006-2011  Borhan Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// @ignore
// ===================================================================================================
package com.borhan.client;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.SocketTimeoutException;
import java.net.URLEncoder;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.zip.GZIPInputStream;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.xpath.XPathExpressionException;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.httpclient.ConnectTimeoutException;
import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpConnectionManager;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.ProxyHost;
import org.apache.commons.httpclient.SimpleHttpConnectionManager;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.httpclient.methods.multipart.FilePart;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;
import org.apache.commons.httpclient.methods.multipart.PartSource;
import org.apache.commons.httpclient.methods.multipart.StringPart;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.w3c.dom.Element;

import com.borhan.client.enums.BorhanSessionType;
import com.borhan.client.utils.ParseUtils;
import com.borhan.client.utils.XmlUtils;

/**
 * Contains non-generated client logic. Includes the doQueue method which is responsible for
 * making HTTP calls to the Borhan server.
 * 
 * @author jpotts
 *
 */
@SuppressWarnings("serial")
abstract public class BorhanClientBase implements Serializable {

    private static final String UTF8_CHARSET = "UTF-8";

    // KS v2 constants
    private static final int BLOCK_SIZE = 16;
    private static final String FIELD_EXPIRY = "_e";
    private static final String FIELD_USER = "_u";
    private static final String FIELD_TYPE = "_t";
    private static final int RANDOM_SIZE = 16;

    private static final int MAX_DEBUG_RESPONSE_STRING_LENGTH = 1024;
    protected BorhanConfiguration borhanConfiguration;
    protected List<BorhanServiceActionCall> callsQueue;
    protected List<Class<?>> requestReturnType;
    protected BorhanParams multiRequestParamsMap;
    protected Map<String, Object> clientConfiguration = new HashMap<String, Object>();
    protected Map<String, Object> requestConfiguration = new HashMap<String, Object>();

    private static IBorhanLogger logger = BorhanLogger.getLogger(BorhanClientBase.class);

    private Header[] responseHeaders = null;

    private boolean acceptGzipEncoding = true;

    protected static final String HTTP_HEADER_ACCEPT_ENCODING = "Accept-Encoding";

    protected static final String HTTP_HEADER_CONTENT_ENCODING = "Content-Encoding";

    protected static final String ENCODING_GZIP = "gzip";

    /**
    * Set whether to accept GZIP encoding, that is, whether to
    * send the HTTP "Accept-Encoding" header with "gzip" as value.
    * <p>Default is "true". Turn this flag off if you do not want
    * GZIP response compression even if enabled on the HTTP server.
    */
    public void setAcceptGzipEncoding(boolean acceptGzipEncoding) {
        this.acceptGzipEncoding = acceptGzipEncoding;
    }

    /**
    * Return whether to accept GZIP encoding, that is, whether to
    * send the HTTP "Accept-Encoding" header with "gzip" as value.
    */
    public boolean isAcceptGzipEncoding() {
        return acceptGzipEncoding;
    }

    /**
    * Determine whether the given response is a GZIP response.
    * <p>Default implementation checks whether the HTTP "Content-Encoding"
    * header contains "gzip" (in any casing).
    * @param postMethod the PostMethod to check
    */
    protected boolean isGzipResponse(PostMethod postMethod) {
        Header encodingHeader = postMethod.getResponseHeader(HTTP_HEADER_CONTENT_ENCODING);
        if (encodingHeader == null || encodingHeader.getValue() == null) {
            return false;
        }
        return (encodingHeader.getValue().toLowerCase().indexOf(ENCODING_GZIP) != -1);
    }

    /**
    * Extract the response body from the given executed remote invocation
    * request.
    * <p>The default implementation simply fetches the PostMethod's response
    * body stream. If the response is recognized as GZIP response, the
    * InputStream will get wrapped in a GZIPInputStream.
    * @param config the HTTP invoker configuration that specifies the target service
    * @param postMethod the PostMethod to read the response body from
    * @return an InputStream for the response body
    * @throws IOException if thrown by I/O methods
    * @see #isGzipResponse
    * @see java.util.zip.GZIPInputStream
    * @see org.apache.commons.httpclient.methods.PostMethod#getResponseBodyAsStream()
    * @see org.apache.commons.httpclient.methods.PostMethod#getResponseHeader(String)
    */
    protected InputStream getResponseBody(PostMethod postMethod) throws IOException {

        if (isGzipResponse(postMethod)) {
            return new GZIPInputStream(postMethod.getResponseBodyAsStream());
        } else {
            return postMethod.getResponseBodyAsStream();
        }
    }

    public Header[] getResponseHeaders() {
        return responseHeaders;
    }

    public BorhanClientBase() {
    }

    public BorhanClientBase(BorhanConfiguration config) {
        this.borhanConfiguration = config;
        this.callsQueue = new ArrayList<BorhanServiceActionCall>();
        this.multiRequestParamsMap = new BorhanParams();
    }

    public boolean isMultiRequest() {
        return (requestReturnType != null);
    }

    public void setBorhanConfiguration(BorhanConfiguration borhanConfiguration) {
        this.borhanConfiguration = borhanConfiguration;
    }

    public BorhanConfiguration getBorhanConfiguration() {
        return this.borhanConfiguration;
    }

    public void queueServiceCall(String service, String action, BorhanParams kparams) throws BorhanApiException {
        this.queueServiceCall(service, action, kparams, new BorhanFiles(), null);
    }

    public void queueServiceCall(String service, String action, BorhanParams kparams, Class<?> expectedClass)
            throws BorhanApiException {
        this.queueServiceCall(service, action, kparams, new BorhanFiles(), expectedClass);
    }

    public void queueServiceCall(String service, String action, BorhanParams kparams, BorhanFiles kfiles)
            throws BorhanApiException {
        this.queueServiceCall(service, action, kparams, kfiles, null);
    }

    public void queueServiceCall(String service, String action, BorhanParams kparams, BorhanFiles kfiles,
            Class<?> expectedClass) throws BorhanApiException {
        Object value;
        for (Entry<String, Object> itr : this.requestConfiguration.entrySet()) {
            value = itr.getValue();
            if (value instanceof BorhanObjectBase) {
                kparams.add(itr.getKey(), (BorhanObjectBase) value);
            } else {
                kparams.add(itr.getKey(), String.valueOf(value));
            }
        }

        BorhanServiceActionCall call = new BorhanServiceActionCall(service, action, kparams, kfiles);
        if (requestReturnType != null)
            requestReturnType.add(expectedClass);
        this.callsQueue.add(call);
    }

    public String serve() throws BorhanApiException {

        BorhanParams kParams = new BorhanParams();
        String url = extractParamsFromCallQueue(kParams, new BorhanFiles());
        String kParamsString = kParams.toQueryString();
        url += "?" + kParamsString;

        return url;
    }

    abstract protected void resetRequest();

    public Element doQueue() throws BorhanApiException {
        if (this.callsQueue.isEmpty())
            return null;

        if (logger.isEnabled())
            logger.debug("service url: [" + this.borhanConfiguration.getEndpoint() + "]");

        BorhanParams kparams = new BorhanParams();
        BorhanFiles kfiles = new BorhanFiles();

        String url = extractParamsFromCallQueue(kparams, kfiles);

        if (logger.isEnabled()) {
            logger.debug("JSON: [" + kparams + "]");
        }

        PostMethod method;
        try {
            method = createPostMethod(kparams, kfiles, url);
        } catch (UnsupportedEncodingException e) {
            resetRequest();
            throw new BorhanApiException("Unsupported encoding: " + e.getMessage());
        }

        HttpClient client = createHttpClient();
        String responseString = null;
        try {
            responseString = executeMethod(client, method);
        } finally {
            resetRequest();
        }

        Element responseXml = XmlUtils.parseXml(responseString);
        Element resultXml = this.validateXmlResult(responseXml);
        this.throwExceptionOnAPIError(resultXml);

        return resultXml;
    }

    protected String readRemoteInvocationResult(InputStream is) throws IOException {

        try {
            return doReadRemoteInvocationResult(is);
        } finally {
            is.close();
        }
    }

    protected String doReadRemoteInvocationResult(InputStream is) throws IOException {

        byte[] buf = new byte[1024];
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int len;
        while ((len = is.read(buf)) > 0) {
            out.write(buf, 0, len);
        }
        return new String(out.toByteArray(), UTF8_CHARSET);
    }

    protected String executeMethod(HttpClient client, PostMethod method) throws BorhanApiException {
        String responseString = "";
        try {
            // Execute the method.
            int statusCode = client.executeMethod(method);

            if (logger.isEnabled()) {
                Header[] headers = method.getRequestHeaders();
                for (Header header : headers)
                    logger.debug("Header [" + header.getName() + " value [" + header.getValue() + "]");
            }

            if (logger.isEnabled() && statusCode != HttpStatus.SC_OK) {
                logger.error("Method failed: " + method.getStatusLine());
                throw new BorhanApiException("Unexpected Http return code: " + statusCode);
            }

            // Read the response body
            InputStream responseBodyIS = null;
            if (isGzipResponse(method)) {
                responseBodyIS = new GZIPInputStream(method.getResponseBodyAsStream());
                if (logger.isEnabled())
                    logger.debug("Using gzip compression to handle response for: " + method.getName() + " "
                            + method.getPath() + "?" + method.getQueryString());
            } else {
                responseBodyIS = method.getResponseBodyAsStream();
                if (logger.isEnabled())
                    logger.debug("No gzip compression for this response");
            }
            String responseBody = readRemoteInvocationResult(responseBodyIS);
            responseHeaders = method.getResponseHeaders();

            // print server debug info
            String serverName = null;
            String serverSession = null;
            for (Header header : responseHeaders) {
                if (header.getName().compareTo("X-Me") == 0)
                    serverName = header.getValue();
                else if (header.getName().compareTo("X-Borhan-Session") == 0)
                    serverSession = header.getValue();
            }
            if (serverName != null || serverSession != null)
                logger.debug("Server: [" + serverName + "], Session: [" + serverSession + "]");

            // Deal with the response.
            // Use caution: ensure correct character encoding and is not binary data
            responseString = new String(responseBody.getBytes(UTF8_CHARSET), UTF8_CHARSET); // Unicon: this MUST be set to UTF-8 charset -AZ
            if (logger.isEnabled()) {
                if (responseString.length() < MAX_DEBUG_RESPONSE_STRING_LENGTH) {
                    logger.debug(responseString);
                } else {
                    logger.debug("Received long response. (length : " + responseString.length() + ")");
                }
            }

            return responseString;

        } catch (HttpException e) {
            if (logger.isEnabled())
                logger.error("Fatal protocol violation: " + e.getMessage(), e);
            throw new BorhanApiException("Protocol exception occured while executing request");
        } catch (SocketTimeoutException e) {
            if (logger.isEnabled())
                logger.error("Fatal transport error: " + e.getMessage(), e);
            throw new BorhanApiException("Request was timed out");
        } catch (ConnectTimeoutException e) {
            if (logger.isEnabled())
                logger.error("Fatal transport error: " + e.getMessage(), e);
            throw new BorhanApiException("Connection to server was timed out");
        } catch (IOException e) {
            if (logger.isEnabled())
                logger.error("Fatal transport error: " + e.getMessage(), e);
            throw new BorhanApiException("I/O exception occured while reading request response");
        } finally {
            // Release the connection.
            method.releaseConnection();
        }
    }

    private PostMethod createPostMethod(BorhanParams kparams, BorhanFiles kfiles, String url)
            throws UnsupportedEncodingException {
        PostMethod method = new PostMethod(url);
        method.setRequestHeader("Accept", "text/xml,application/xml,*/*");
        method.setRequestHeader("Accept-Charset", "utf-8,ISO-8859-1;q=0.7,*;q=0.5");

        if (!kfiles.isEmpty()) {
            method = this.getPostMultiPartWithFiles(method, kparams, kfiles);
        } else {
            method = this.addParams(method, kparams);
        }

        if (isAcceptGzipEncoding()) {
            method.addRequestHeader(HTTP_HEADER_ACCEPT_ENCODING, ENCODING_GZIP);
        }

        // Provide custom retry handler is necessary
        method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
                new DefaultHttpMethodRetryHandler(3, false));
        return method;
    }

    protected HttpClient createHttpClient() {
        HttpClient client = new HttpClient();

        // added by Unicon to handle proxy hosts
        String proxyHost = System.getProperty("http.proxyHost");
        if (proxyHost != null) {
            int proxyPort = -1;
            String proxyPortStr = System.getProperty("http.proxyPort");
            if (proxyPortStr != null) {
                try {
                    proxyPort = Integer.parseInt(proxyPortStr);
                } catch (NumberFormatException e) {
                    if (logger.isEnabled())
                        logger.warn("Invalid number for system property http.proxyPort (" + proxyPortStr
                                + "), using default port instead");
                }
            }
            ProxyHost proxy = new ProxyHost(proxyHost, proxyPort);
            client.getHostConfiguration().setProxyHost(proxy);
        }
        // added by Unicon to force encoding to UTF-8
        client.getParams().setParameter(HttpMethodParams.HTTP_CONTENT_CHARSET, UTF8_CHARSET);
        client.getParams().setParameter(HttpMethodParams.HTTP_ELEMENT_CHARSET, UTF8_CHARSET);
        client.getParams().setParameter(HttpMethodParams.HTTP_URI_CHARSET, UTF8_CHARSET);

        HttpConnectionManagerParams connParams = client.getHttpConnectionManager().getParams();
        if (this.borhanConfiguration.getTimeout() != 0) {
            connParams.setSoTimeout(this.borhanConfiguration.getTimeout());
            connParams.setConnectionTimeout(this.borhanConfiguration.getTimeout());
        }
        client.getHttpConnectionManager().setParams(connParams);
        return client;
    }

    /**
     * We need to make sure that we shut down the connection.
     * The possible connection manager types are taken from here:
     * http://hc.apache.org/httpclient-legacy/apidocs/org/apache/commons/httpclient/HttpConnectionManager.html
     * 
     * The issue details is described here:
     * http://fuyun.org/2009/09/connection-close-in-httpclient/
     * 
     * @param client The client we wish to close
     */
    protected void closeHttpClient(HttpClient client) {
        HttpConnectionManager mgr = client.getHttpConnectionManager();

        if (mgr instanceof SimpleHttpConnectionManager) {
            ((SimpleHttpConnectionManager) mgr).shutdown();
        }

        if (mgr instanceof MultiThreadedHttpConnectionManager) {
            ((MultiThreadedHttpConnectionManager) mgr).shutdown();
        }
    }

    private String extractParamsFromCallQueue(BorhanParams kparams, BorhanFiles kfiles) throws BorhanApiException {

        String url = this.borhanConfiguration.getEndpoint() + "/api_v3";

        // append the basic params
        kparams.add("format", this.borhanConfiguration.getServiceFormat());
        kparams.add("ignoreNull", true);

        Object value;
        for (Entry<String, Object> itr : this.clientConfiguration.entrySet()) {
            value = itr.getValue();
            if (value instanceof BorhanObjectBase) {
                kparams.add(itr.getKey(), (BorhanObjectBase) value);
            } else {
                kparams.add(itr.getKey(), String.valueOf(value));
            }
        }

        if (requestReturnType != null) {
            url += "/service/multirequest";
            int i = 1;
            for (BorhanServiceActionCall call : this.callsQueue) {
                BorhanParams callParams = call.getParamsForMultiRequest(i);
                kparams.add(callParams);
                BorhanFiles callFiles = call.getFilesForMultiRequest(i);
                kfiles.add(callFiles);
                i++;
            }

            // map params
            for (Object key : this.multiRequestParamsMap.keySet()) {
                String requestParam = (String) key;
                BorhanParams resultParam = this.multiRequestParamsMap.getParams(requestParam);

                if (kparams.containsKey(requestParam)) {
                    kparams.add(requestParam, resultParam);
                }
            }

            // Clean
            this.multiRequestParamsMap.clear();

        } else {
            BorhanServiceActionCall call = this.callsQueue.get(0);
            url += "/service/" + call.getService() + "/action/" + call.getAction();
            kparams.add(call.getParams());
            kfiles.add(call.getFiles());
        }

        // cleanup
        this.callsQueue.clear();

        kparams.add("kalsig", this.signature(kparams));
        return url;
    }

    public void startMultiRequest() {
        requestReturnType = new ArrayList<Class<?>>();
    }

    public Element getElementByXPath(Element element, String xPath) throws BorhanApiException {
        try {
            return XmlUtils.getElementByXPath(element, xPath);
        } catch (XPathExpressionException xee) {
            throw new BorhanApiException("XPath expression exception evaluating result");
        }
    }

    public BorhanMultiResponse doMultiRequest() throws BorhanApiException {
        Element multiRequestResult = doQueue();

        BorhanMultiResponse multiResponse = new BorhanMultiResponse();

        for (int i = 0; i < multiRequestResult.getChildNodes().getLength(); i++) {
            Element arrayNode = (Element) multiRequestResult.getChildNodes().item(i);

            try {
                BorhanApiException exception = getExceptionOnAPIError(arrayNode);
                if (exception != null) {
                    multiResponse.add(exception);
                } else if (getElementByXPath(arrayNode, "objectType") != null) {
                    multiResponse.add(BorhanObjectFactory.create(arrayNode, requestReturnType.get(i)));
                } else if (getElementByXPath(arrayNode, "item/objectType") != null) {
                    multiResponse.add(ParseUtils.parseArray(requestReturnType.get(i), arrayNode));
                } else {
                    multiResponse.add(arrayNode.getTextContent());
                }
            } catch (BorhanApiException e) {
                multiResponse.add(e);
            }
        }

        // Cleanup
        this.requestReturnType = null;
        return multiResponse;
    }

    public void mapMultiRequestParam(int resultNumber, int requestNumber, String requestParamName)
            throws BorhanApiException {
        this.mapMultiRequestParam(resultNumber, null, requestNumber, requestParamName);
    }

    public void mapMultiRequestParam(int resultNumber, String resultParamName, int requestNumber,
            String requestParamName) throws BorhanApiException {
        String resultParam = "{" + resultNumber + ":result";
        if (resultParamName != null && resultParamName != "") {
            resultParam += resultParamName;
        }
        resultParam += "}";

        String requestNumberString = Integer.toString(requestNumber);
        BorhanParams params = new BorhanParams();
        params.add(requestParamName, resultParam);
        this.multiRequestParamsMap.add(requestNumberString, params);
    }

    private String signature(BorhanParams kparams) throws BorhanApiException {
        String md5 = new String(Hex.encodeHex(DigestUtils.md5(kparams.toString())));
        ;
        return md5;
    }

    private Element validateXmlResult(Element resultXml) throws BorhanApiException {

        Element resultElement = null;
        resultElement = getElementByXPath(resultXml, "/xml/result");

        if (resultElement != null) {
            return resultElement;
        } else {
            throw new BorhanApiException("Invalid result");
        }
    }

    private BorhanApiException getExceptionOnAPIError(Element result) throws BorhanApiException {
        Element errorElement = getElementByXPath(result, "error");
        if (errorElement == null) {
            return null;
        }

        Element messageElement = getElementByXPath(errorElement, "message");
        Element codeElement = getElementByXPath(errorElement, "code");
        if (messageElement == null || codeElement == null) {
            return null;
        }

        return new BorhanApiException(messageElement.getTextContent(), codeElement.getTextContent());
    }

    private void throwExceptionOnAPIError(Element result) throws BorhanApiException {
        BorhanApiException exception = getExceptionOnAPIError(result);
        if (exception != null) {
            throw exception;
        }
    }

    private PostMethod getPostMultiPartWithFiles(PostMethod method, BorhanParams kparams, BorhanFiles kfiles) {

        String boundary = "---------------------------" + System.currentTimeMillis();
        List<Part> parts = new ArrayList<Part>();
        parts.add(new StringPart(HttpMethodParams.MULTIPART_BOUNDARY, boundary));

        parts.add(new StringPart("json", kparams.toString()));

        for (String key : kfiles.keySet()) {
            final BorhanFile kFile = kfiles.get(key);
            parts.add(new StringPart(key, "filename=" + kFile.getName()));
            if (kFile.getFile() != null) {
                // use the file
                File file = kFile.getFile();
                try {
                    parts.add(new FilePart(key, file));
                } catch (FileNotFoundException e) {
                    // TODO this sort of leaves the submission in a weird
                    // state... -AZ
                    if (logger.isEnabled())
                        logger.error("Exception while iterating over kfiles", e);
                }
            } else {
                // use the input stream
                PartSource fisPS = new PartSource() {
                    public long getLength() {
                        return kFile.getSize();
                    }

                    public String getFileName() {
                        return kFile.getName();
                    }

                    public InputStream createInputStream() throws IOException {
                        return kFile.getInputStream();
                    }
                };
                parts.add(new FilePart(key, fisPS));
            }
        }

        Part allParts[] = new Part[parts.size()];
        allParts = parts.toArray(allParts);

        method.setRequestEntity(new MultipartRequestEntity(allParts, method.getParams()));

        return method;
    }

    private PostMethod addParams(PostMethod method, BorhanParams kparams) throws UnsupportedEncodingException {
        String content = kparams.toString();
        String contentType = "application/json";
        StringRequestEntity requestEntity = new StringRequestEntity(content, contentType, null);

        method.setRequestEntity(requestEntity);
        return method;
    }

    public String generateSession(String adminSecretForSigning, String userId, BorhanSessionType type,
            int partnerId) throws Exception {
        return this.generateSession(adminSecretForSigning, userId, type, partnerId, 86400);
    }

    public String generateSession(String adminSecretForSigning, String userId, BorhanSessionType type,
            int partnerId, int expiry) throws Exception {
        return this.generateSession(adminSecretForSigning, userId, type, partnerId, expiry, "");
    }

    public String generateSession(String adminSecretForSigning, String userId, BorhanSessionType type,
            int partnerId, int expiry, String privileges) throws Exception {
        try {
            // initialize required values
            int rand = (int) (Math.random() * 32000);
            expiry += (int) (System.currentTimeMillis() / 1000);

            // build info string
            StringBuilder sbInfo = new StringBuilder();
            sbInfo.append(partnerId).append(";"); // index 0 - partner ID
            sbInfo.append(partnerId).append(";"); // index 1 - partner pattern - using partner ID
            sbInfo.append(expiry).append(";"); // index 2 - expiration timestamp
            sbInfo.append(type.getHashCode()).append(";"); // index 3 - session type
            sbInfo.append(rand).append(";"); // index 4 - random number
            sbInfo.append(userId).append(";"); // index 5 - user ID
            sbInfo.append(privileges); // index 6 - privileges

            byte[] infoSignature = signInfoWithSHA1(adminSecretForSigning + (sbInfo.toString()));

            // convert signature to hex:
            String signature = this.convertToHex(infoSignature);

            // build final string to base64 encode
            StringBuilder sbToEncode = new StringBuilder();
            sbToEncode.append(signature.toString()).append("|").append(sbInfo.toString());

            // encode the signature and info with base64
            String hashedString = new String(Base64.encodeBase64(sbToEncode.toString().getBytes()));

            // remove line breaks in the session string
            String ks = hashedString.replace("\n", "");
            ks = hashedString.replace("\r", "");

            // return the generated session key (KS)
            return ks;
        } catch (NoSuchAlgorithmException ex) {
            throw new Exception(ex);
        }
    }

    public String generateSessionV2(String adminSecretForSigning, String userId, BorhanSessionType type,
            int partnerId, int expiry, String privileges) throws Exception {
        try {
            // build fields array
            BorhanParams fields = new BorhanParams();
            String[] privilegesArr = privileges.split(",");
            for (String curPriv : privilegesArr) {
                String privilege = curPriv.trim();
                if (privilege.length() == 0)
                    continue;
                if (privilege.equals("*"))
                    privilege = "all:*";

                String[] splittedPriv = privilege.split(":");
                if (splittedPriv.length > 1) {
                    fields.add(splittedPriv[0], URLEncoder.encode(splittedPriv[1], UTF8_CHARSET));
                } else {
                    fields.add(splittedPriv[0], "");
                }
            }

            Integer expiryInt = (int) (System.currentTimeMillis() / 1000) + expiry;
            String expStr = expiryInt.toString();
            fields.add(FIELD_EXPIRY, expStr);
            fields.add(FIELD_TYPE, Integer.toString(type.getHashCode()));
            fields.add(FIELD_USER, userId);

            // build fields string
            byte[] randomBytes = createRandomByteArray(RANDOM_SIZE);
            byte[] fieldsByteArray = fields.toQueryString().getBytes();
            int totalLength = randomBytes.length + fieldsByteArray.length;
            byte[] fieldsAndRandomBytes = new byte[totalLength];
            System.arraycopy(randomBytes, 0, fieldsAndRandomBytes, 0, randomBytes.length);
            System.arraycopy(fieldsByteArray, 0, fieldsAndRandomBytes, randomBytes.length, fieldsByteArray.length);

            byte[] infoSignature = signInfoWithSHA1(fieldsAndRandomBytes);
            byte[] input = new byte[infoSignature.length + fieldsAndRandomBytes.length];
            System.arraycopy(infoSignature, 0, input, 0, infoSignature.length);
            System.arraycopy(fieldsAndRandomBytes, 0, input, infoSignature.length, fieldsAndRandomBytes.length);

            // encrypt and encode
            byte[] encryptedFields = aesEncrypt(adminSecretForSigning, input);
            String prefix = "v2|" + partnerId + "|";

            byte[] output = new byte[encryptedFields.length + prefix.length()];
            System.arraycopy(prefix.getBytes(), 0, output, 0, prefix.length());
            System.arraycopy(encryptedFields, 0, output, prefix.length(), encryptedFields.length);

            String encodedKs = new String(Base64.encodeBase64(output));
            encodedKs = encodedKs.replaceAll("\\+", "-");
            encodedKs = encodedKs.replaceAll("/", "_");
            encodedKs = encodedKs.replace("\n", "");
            encodedKs = encodedKs.replace("\r", "");

            return encodedKs;
        } catch (GeneralSecurityException ex) {
            logger.error("Failed to generate v2 session.");
            throw new Exception(ex);
        }
    }

    private byte[] signInfoWithSHA1(String text) throws GeneralSecurityException {
        return signInfoWithSHA1(text.getBytes());
    }

    private byte[] signInfoWithSHA1(byte[] data) throws GeneralSecurityException {
        MessageDigest algorithm = MessageDigest.getInstance("SHA1");
        algorithm.reset();
        algorithm.update(data);
        byte infoSignature[] = algorithm.digest();
        return infoSignature;
    }

    private byte[] aesEncrypt(String secretForSigning, byte[] text)
            throws GeneralSecurityException, UnsupportedEncodingException {
        // Key
        byte[] hashedKey = signInfoWithSHA1(secretForSigning);
        byte[] keyBytes = new byte[BLOCK_SIZE];
        System.arraycopy(hashedKey, 0, keyBytes, 0, BLOCK_SIZE);
        SecretKeySpec key = new SecretKeySpec(keyBytes, "AES");

        // IV
        byte[] ivBytes = new byte[BLOCK_SIZE];
        IvParameterSpec iv = new IvParameterSpec(ivBytes);

        // Text
        int textSize = ((text.length + BLOCK_SIZE - 1) / BLOCK_SIZE) * BLOCK_SIZE;
        byte[] textAsBytes = new byte[textSize];
        Arrays.fill(textAsBytes, (byte) 0);
        System.arraycopy(text, 0, textAsBytes, 0, text.length);

        // Encrypt
        Cipher cipher = Cipher.getInstance("AES/CBC/NOPADDING");
        cipher.init(Cipher.ENCRYPT_MODE, key, iv);
        return cipher.doFinal(textAsBytes);
    }

    private byte[] createRandomByteArray(int size) {
        byte[] b = new byte[size];
        new Random().nextBytes(b);
        return b;
    }

    // new function to convert byte array to Hex
    private String convertToHex(byte[] data) {
        StringBuffer buf = new StringBuffer();
        for (int i = 0; i < data.length; i++) {
            int halfbyte = (data[i] >>> 4) & 0x0F;
            int two_halfs = 0;
            do {
                if ((0 <= halfbyte) && (halfbyte <= 9))
                    buf.append((char) ('0' + halfbyte));
                else
                    buf.append((char) ('a' + (halfbyte - 10)));
                halfbyte = data[i] & 0x0F;
            } while (two_halfs++ < 1);
        }
        return buf.toString();
    }

}