Project: arquillian-extension-drone
/*
 * JBoss, Home of Professional Open Source 
 * Copyright 2010, Red Hat Middleware LLC, and individual contributors 
 * by the @authors tag. See the copyright.txt in the distribution for a 
 * full listing of individual contributors. 
 * 
 * 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.jboss.arquillian.drone.webdriver.factory.remote.reusable; 
 
import java.io.IOException; 
import java.io.Serializable; 
import java.util.Arrays; 
import java.util.Calendar; 
import java.util.Date; 
import java.util.HashMap; 
import java.util.Iterator; 
import java.util.LinkedList; 
import java.util.Map; 
import java.util.Map.Entry; 
import java.util.logging.Level; 
import java.util.logging.Logger; 
 
/**
 * Storage for ReusedSession. It allows to work with sessions stored with different versions of Drones in a single place. 
 * 
 * 
 * 
 * @author <a href="mailto:[email protected]">Lukas Fryc</a> 
 * @author <a href="mailto:[email protected]">Karel Piwko</a> 
 */
 
public class ReusedSessionStoreImpl implements ReusedSessionStore { 
    private static final Logger log = Logger.getLogger(ReusedSessionStoreImpl.class.getName()); 
 
    private static final long serialVersionUID = 914857799370645455L
 
    // session is valid for two days 
    private static final int SESSION_VALID_IN_SECONDS = 3600 * 48
 
    // represents a "raw" list of reused sessions, storing sessions with timeout information 
    // we cannot use Deque, since it is 1.6+ 
    private Map<ByteArray, LinkedList<ByteArray>> rawStore; 
 
    public ReusedSessionStoreImpl() { 
        this.rawStore = new HashMap<ByteArray, LinkedList<ByteArray>>(); 
    } 
 
    @Override 
    public ReusedSession pull(InitializationParameter key) { 
        synchronized (rawStore) { 
 
            LinkedList<ByteArray> queue = null
            // find key 
            for (Entry<ByteArray, LinkedList<ByteArray>> entry : rawStore.entrySet()) { 
                InitializationParameter candidate = entry.getKey().as(InitializationParameter.class); 
                if (candidate != null && candidate.equals(key)) { 
                    queue = entry.getValue(); 
                    break
                } 
            } 
 
            // there is no such queue 
            if (queue == null || queue.isEmpty()) { 
                return null
            } 
 
            // map the view to available queues 
            LinkedList<RawDisposableReusedSession> sessions = getValidSessions(queue); 
            if (sessions == null || sessions.isEmpty()) { 
                return null
            } 
 
            // get session and dispose it 
            RawDisposableReusedSession disposableSession = sessions.getLast(); 
            disposableSession.dispose(); 
 
            return disposableSession.getSession(); 
        } 
    } 
 
    @Override 
    public void store(InitializationParameter key, ReusedSession session) { 
        synchronized (rawStore) { 
            // update map of raw data 
            ByteArray rawKey = ByteArray.fromObject(key); 
            if (rawKey == null) { 
                log.severe("Unable to store browser initialization parameter in ReusedSessionStore for browser: " 
                        + key.getDesiredCapabilities().getBrowserName()); 
                return
            } 
 
            LinkedList<ByteArray> rawList = rawStore.get(rawKey); 
            if (rawList == null) { 
                rawList = new LinkedList<ByteArray>(); 
                rawStore.put(rawKey, rawList); 
            } 
            ByteArray rawSession = ByteArray.fromObject(session); 
            if (rawSession == null) { 
                log.severe("Unable to store browser session in ReusedSessionStore for browser: " 
                        + key.getDesiredCapabilities().getBrowserName()); 
                return
            } 
            // add a timestamp to the session and store in the list 
            TimeStampedSession timeStampedSession = new TimeStampedSession(rawSession); 
            rawList.add(ByteArray.fromObject(timeStampedSession)); 
 
        } 
    } 
 
    private LinkedList<RawDisposableReusedSession> getValidSessions(LinkedList<ByteArray> rawQueue) { 
 
        if (rawQueue == null || rawQueue.size() == 0) { 
            return null
        } 
 
        LinkedList<RawDisposableReusedSession> sessions = new LinkedList<RawDisposableReusedSession>(); 
        Iterator<ByteArray> byteArrayIterator = rawQueue.iterator(); 
        while (byteArrayIterator.hasNext()) { 
            TimeStampedSession session = byteArrayIterator.next().as(TimeStampedSession.class); 
            // add session if valid and it can be deserialized 
            ReusedSession reusedSession = session.getSession(); 
            if (session.isValid(SESSION_VALID_IN_SECONDS) && reusedSession != null) { 
                sessions.add(new RawDisposableReusedSession(session.getRawSession(), rawQueue, reusedSession)); 
            } 
            // remove completely if session is not valid 
            else if (!session.isValid(SESSION_VALID_IN_SECONDS)) { 
                byteArrayIterator.remove(); 
            } 
        } 
        return sessions; 
    } 
 
    /**
     * Wrapper for array of bytes to act as a key/value in a map. This abstraction allows as to have stored sessions for Drones 
     * with incompatible serialVersionUID, for instance Drones based on different Selenium version. 
     * 
     * This implementation ignores invalid content, it simply returns null when a object cannot be deserialized 
     * 
     * @author <a href="mailto:[email protected]">Karel Piwko</a> 
     * 
     */
 
    private static class ByteArray implements Serializable { 
        private static final long serialVersionUID = 1L
 
        private byte[] raw = new byte[0]; 
 
        static ByteArray fromObject(Serializable object) { 
            ByteArray bytes = new ByteArray(); 
            try { 
                bytes.raw = SerializationUtils.serializeToBytes(object); 
                return bytes; 
            } catch (IOException e) { 
                log.log(Level.FINE, "Unable to deserialize object of " + object.getClass().getName(), e); 
            } 
            return null
        } 
 
        <T extends Serializable> T as(Class<T> classType) { 
            try { 
                return SerializationUtils.deserializeFromBytes(classType, raw); 
            } catch (ClassNotFoundException e) { 
                log.log(Level.FINE, "Unable to deserialize object of " + classType.getName(), e); 
            } catch (IOException e) { 
                log.log(Level.FINE, "Unable to deserialize object of " + classType.getName(), e); 
            } catch (ClassCastException e) { 
                log.log(Level.FINE, "Unable to deserialize object of " + classType.getName(), e); 
            } 
 
            return null
        } 
 
        @Override 
        public int hashCode() { 
            final int prime = 31
            int result = 1
            result = prime * result + Arrays.hashCode(raw); 
            return result; 
        } 
 
        @Override 
        public boolean equals(Object obj) { 
            if (this == obj) 
                return true
            if (obj == null
                return false
            if (getClass() != obj.getClass()) 
                return false
            ByteArray other = (ByteArray) obj; 
            if (!Arrays.equals(raw, other.raw)) 
                return false
            return true
        } 
 
    } 
 
    /**
     * Wrapper for ReusedSession. This session is stored in binary format including a timestamp. 
     * 
     * This allows implementation to invalidate a session without actually trying to deserialize it. 
     * 
     * @author <a href="mailto:[email protected]">Karel Piwko</a> 
     * 
     */
 
    private static class TimeStampedSession implements Serializable { 
        private static final long serialVersionUID = 1L
 
        private Date timestamp; 
 
        private ByteArray rawSession; 
 
        public TimeStampedSession(ByteArray rawSession) { 
            this.timestamp = new Date(); 
            this.rawSession = rawSession; 
        } 
 
        public boolean isValid(int timeoutInSeconds) { 
 
            Date now = new Date(); 
            Calendar calendar = Calendar.getInstance(); 
            calendar.setTime(timestamp); 
            calendar.add(Calendar.SECOND, timeoutInSeconds); 
 
            return calendar.getTime().after(now); 
        } 
 
        public ReusedSession getSession() { 
            return rawSession.as(ReusedSession.class); 
        } 
 
        public ByteArray getRawSession() { 
            return rawSession; 
        } 
 
        @Override 
        public int hashCode() { 
            final int prime = 31
            int result = 1
            result = prime * result + ((rawSession == null) ? 0 : rawSession.hashCode()); 
            result = prime * result + ((timestamp == null) ? 0 : timestamp.hashCode()); 
            return result; 
        } 
 
        @Override 
        public boolean equals(Object obj) { 
            if (this == obj) 
                return true
            if (obj == null
                return false
            if (getClass() != obj.getClass()) 
                return false
            TimeStampedSession other = (TimeStampedSession) obj; 
            if (rawSession == null) { 
                if (other.rawSession != null
                    return false
            } else if (!rawSession.equals(other.rawSession)) 
                return false
            if (timestamp == null) { 
                if (other.timestamp != null
                    return false
            } else if (!timestamp.equals(other.timestamp)) 
                return false
            return true
        } 
    } 
 
    /**
     * Wrapper for reusable session with ability to dispose data from rawStore. 
     * 
     * @author <a href="mailto:[email protected]">Karel Piwko</a> 
     * 
     */
 
    private static class RawDisposableReusedSession { 
        private final ByteArray key; 
        private final ReusedSession session; 
        private final LinkedList<ByteArray> parentList; 
 
        public RawDisposableReusedSession(ByteArray key, LinkedList<ByteArray> parentList, ReusedSession session) { 
            this.key = key; 
            this.parentList = parentList; 
            this.session = session; 
        } 
 
        /**
         * Removes current session from queue of relevant raw data 
         */
 
        public void dispose() { 
            synchronized (parentList) { 
                Iterator<ByteArray> iterator = parentList.iterator(); 
                ReusedSession wrappedKey = key.as(ReusedSession.class); 
                if (wrappedKey == null) { 
                    throw new IllegalStateException( 
                            "Could not dispose a session from the storage, current session cannot be deserialized."); 
                } 
 
                // find appropriate object from parentList to be removed 
                while (iterator.hasNext()) { 
                    TimeStampedSession candidate = iterator.next().as(TimeStampedSession.class); 
                    // check if both point to the same session 
                    if (candidate != null && candidate.getSession() != null && candidate.getSession().equals(wrappedKey)) { 
                        iterator.remove(); 
                    } 
                } 
            } 
        } 
 
        public ReusedSession getSession() { 
            return session; 
        } 
    } 
}