/* * 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.Iterator; import java.util.LinkedHashMap; 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:lryc@redhat.com">Lukas Fryc</a> * @author <a href="mailto:kpiwko@redhat.com">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 final Map<ByteArray, LinkedList<ByteArray>> rawStore; public ReusedSessionStoreImpl() { this.rawStore = new LinkedHashMap<ByteArray, LinkedList<ByteArray>>(); } @Override public ReusedSession pull(InitializationParameter key) { synchronized (rawStore) { LinkedList<ByteArray> queue = null; log.log(Level.FINER, "Pulling key {0} from Session Store", key); // 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(); log.log(Level.FINE, "Reusing session {0} ", disposableSession.getSession().getSessionId()); 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.log(Level.SEVERE, "Unable to store browser initialization parameter in ReusedSessionStore for browser :{0}", 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.log(Level.SEVERE, "Unable to store browser initialization parameter in ReusedSessionStore for browser :{0}", 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)); log.log(Level.FINE, "Stored session {0} within {1}", new Object[] { timeStampedSession.getSession().getSessionId(), key}); } } private LinkedList<RawDisposableReusedSession> getValidSessions(LinkedList<ByteArray> rawQueue) { if (rawQueue == null || rawQueue.size() == 0) { return new LinkedList<RawDisposableReusedSession>(); } 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. * <p> * This implementation ignores invalid content, it simply returns null when a object cannot be deserialized * * @author <a href="mailto:kpiwko@redhat.com">Karel Piwko</a> */ 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. * <p> * This allows implementation to invalidate a session without actually trying to deserialize it. * * @author <a href="mailto:kpiwko@redhat.com">Karel Piwko</a> */ static class TimeStampedSession implements Serializable { private static final long serialVersionUID = 1L; private final Date timestamp; private final ByteArray rawSession; 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; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(timestamp).append(" ").append(getSession()); return sb.toString(); } } /** * Wrapper for reusable session with ability to dispose data from rawStore. * * @author <a href="mailto:kpiwko@redhat.com">Karel Piwko</a> */ private static class RawDisposableReusedSession { private final ByteArray key; private final ReusedSession session; private final LinkedList<ByteArray> parentList; 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; } } }