package com.vaadin.util;
import java.io.Serializable;
+import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.logging.Level;
+import java.util.logging.Logger;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinResponse;
* when {@link VaadinSession#access(Runnable)} or {@link UI#access(Runnable)} is
* used.
* <p>
+ * Please note that the instances are stored using {@link WeakReference}. This
+ * means that the a current instance value may suddenly disappear if there a no
+ * other references to the object.
+ * <p>
* Currently the framework uses the following instances:
* </p>
* <p>
* @since 7.0.0
*/
public class CurrentInstance implements Serializable {
- private final Object instance;
+ private final WeakReference<Object> instance;
private final boolean inheritable;
private static InheritableThreadLocal<Map<Class<?>, CurrentInstance>> instances = new InheritableThreadLocal<Map<Class<?>, CurrentInstance>>() {
return null;
}
- Map<Class<?>, CurrentInstance> value = new WeakValueMap<Class<?>, CurrentInstance>();
+ Map<Class<?>, CurrentInstance> value = new HashMap<Class<?>, CurrentInstance>();
// Copy all inheritable values to child map
for (Entry<Class<?>, CurrentInstance> e : parentValue.entrySet()) {
};
private CurrentInstance(Object instance, boolean inheritable) {
- this.instance = instance;
+ this.instance = new WeakReference<Object>(instance);
this.inheritable = inheritable;
}
}
CurrentInstance currentInstance = map.get(type);
if (currentInstance != null) {
- return type.cast(currentInstance.instance);
+ Object value = currentInstance.instance.get();
+ if (value == null) {
+ /*
+ * This is believed to never actually happen since the
+ * ThreadLocal should only outlive the referenced object on
+ * threads that are not doing anything related to Vaadin, which
+ * should thus never invoke CurrentInstance.get().
+ *
+ * At this point, there might also be other values that have
+ * been collected, so we'll scan the entire map and remove stale
+ * CurrentInstance objects. Using a ReferenceQueue could make
+ * this assumingly rare case slightly more efficient, but would
+ * significantly increase the complexity of the code for
+ * maintaining a separate ReferenceQueue for each Thread.
+ */
+ removeStaleInstances(map);
+
+ if (map.isEmpty()) {
+ instances.remove();
+ }
+
+ return null;
+ }
+ return type.cast(value);
} else {
return null;
}
}
+ private static void removeStaleInstances(Map<Class<?>, CurrentInstance> map) {
+ for (Iterator<Entry<Class<?>, CurrentInstance>> iterator = map
+ .entrySet().iterator(); iterator.hasNext();) {
+ Entry<Class<?>, CurrentInstance> entry = iterator.next();
+ Object instance = entry.getValue().instance.get();
+ if (instance == null) {
+ iterator.remove();
+ getLogger().log(Level.WARNING,
+ "CurrentInstance for {0} has been garbage collected.",
+ entry.getKey());
+ }
+ }
+ }
+
/**
* Sets the current instance of the given type.
*
* A Class -> CurrentInstance map to set as current instances
*/
public static void restoreInstances(Map<Class<?>, CurrentInstance> old) {
+ boolean removeStale = false;
for (Class c : old.keySet()) {
CurrentInstance ci = old.get(c);
- set(c, ci.instance, ci.inheritable);
+ Object v = ci.instance.get();
+ if (v == null) {
+ removeStale = true;
+ } else {
+ set(c, v, ci.inheritable);
+ }
+ }
+
+ if (removeStale) {
+ removeStaleInstances(old);
}
}
return Collections.emptyMap();
} else {
Map<Class<?>, CurrentInstance> copy = new HashMap<Class<?>, CurrentInstance>();
+ boolean removeStale = false;
for (Class<?> c : map.keySet()) {
CurrentInstance ci = map.get(c);
- if (ci.inheritable || !onlyInheritable) {
+ if (ci.instance.isEnqueued()) {
+ removeStale = true;
+ } else if (ci.inheritable || !onlyInheritable) {
copy.put(c, ci);
}
}
+ if (removeStale) {
+ removeStaleInstances(map);
+ if (map.isEmpty()) {
+ instances.remove();
+ }
+ }
return copy;
}
}
return old;
}
+
+ private static Logger getLogger() {
+ return Logger.getLogger(CurrentInstance.class.getName());
+ }
}
+++ /dev/null
-/*
- * Copyright 2000-2013 Vaadin Ltd.
- *
- * 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 com.vaadin.util;
-
-import java.lang.ref.Reference;
-import java.lang.ref.ReferenceQueue;
-import java.lang.ref.WeakReference;
-import java.util.AbstractMap;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * A Map holding weak references to its values. It is internally backed by a
- * normal HashMap and all values are stored as WeakReferences. Garbage collected
- * entries are removed when touched.
- * <p>
- * <em>Note</em> this class is not serializable.
- *
- * @author Vaadin Ltd
- * @since 7.1.4
- */
-public class WeakValueMap<K, V> implements Map<K, V> {
-
- /**
- * This class holds a weak reference to the value and a strong reference to
- * the key for efficient removal of stale values.
- */
- private static class WeakValueReference<K, V> extends WeakReference<V> {
- private final K key;
-
- WeakValueReference(K key, V value, ReferenceQueue<V> refQueue) {
- super(value, refQueue);
- this.key = key;
- }
-
- K getKey() {
- return key;
- }
- }
-
- private final HashMap<K, WeakValueReference<K, V>> backingMap;
- private final ReferenceQueue<V> refQueue;
-
- /**
- * Constructs a new WeakValueMap, where all values are stored as weak
- * references.
- */
- public WeakValueMap() {
- backingMap = new HashMap<K, WeakValueReference<K, V>>();
- refQueue = new ReferenceQueue<V>();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public V put(K key, V value) {
- if (key == null) {
- throw new NullPointerException("key cannot be null");
- }
- if (value == null) {
- throw new NullPointerException("value cannot be null");
- }
- removeStaleEntries();
- backingMap.put(key, new WeakValueReference<K, V>(key, value, refQueue));
- return value;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public V remove(Object o) {
- removeStaleEntries();
- WeakReference<V> value = backingMap.remove(o);
- return value == null ? null : value.get();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void putAll(Map<? extends K, ? extends V> map) {
- if (map != null) {
- for (Map.Entry<? extends K, ? extends V> entry : map.entrySet()) {
- put(entry.getKey(), entry.getValue());
- }
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void clear() {
- backingMap.clear();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public Set<K> keySet() {
- removeStaleEntries();
- return backingMap.keySet();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public V get(Object o) {
- removeStaleEntries();
- WeakReference<V> weakValue = backingMap.get(o);
- if (weakValue != null) {
- return weakValue.get();
- }
- return null;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int size() {
- removeStaleEntries();
- return backingMap.size();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean isEmpty() {
- removeStaleEntries();
- return backingMap.isEmpty();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean containsKey(Object o) {
- removeStaleEntries();
- return backingMap.containsKey(o);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean containsValue(Object o) {
- removeStaleEntries();
- for (V value : values()) {
- if (o.equals(value)) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public Collection<V> values() {
- removeStaleEntries();
- Collection<V> values = new HashSet<V>();
- for (WeakReference<V> weakValue : backingMap.values()) {
- V value = weakValue.get();
- if (value != null) {
- // null values have been GC'd, which may happen long before
- // anything is enqueued in the ReferenceQueue.
- values.add(value);
- }
- }
- return values;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public Set<Entry<K, V>> entrySet() {
- removeStaleEntries();
- Set<Entry<K, V>> entrySet = new HashSet<Entry<K, V>>();
- for (Entry<K, WeakValueReference<K, V>> entry : backingMap.entrySet()) {
- V value = entry.getValue().get();
- if (value != null) {
- // null values have been GC'd, which may happen long before
- // anything is enqueued in the ReferenceQueue.
- entrySet.add(new AbstractMap.SimpleEntry<K, V>(entry.getKey(),
- value));
- }
- }
- return entrySet;
- }
-
- /**
- * Cleans up stale entries by polling the ReferenceQueue.
- * <p>
- * Depending on the GC implementation and strategy, the ReferenceQueue is
- * not necessarily notified immediately when a reference is garbage
- * collected, but it will eventually be.
- */
- private void removeStaleEntries() {
- Reference<? extends V> ref;
- while ((ref = refQueue.poll()) != null) {
- Object key = ((WeakValueReference<?, ?>) ref).getKey();
- backingMap.remove(key);
- }
- }
-}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<link rel="selenium.base" href="" />
+<title>New Test</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr><td rowspan="1" colspan="3">New Test</td></tr>
+</thead><tbody>
+<tr>
+ <td>open</td>
+ <td>/run/com.vaadin.tests.components.ui.CurrentUiRetained?restartApplication</td>
+ <td></td>
+</tr>
+<tr>
+ <td>pause</td>
+ <td>2000</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>vaadin=runcomvaadintestscomponentsuiCurrentUiRetained::/VVerticalLayout[0]/Slot[2]/VVerticalLayout[0]/Slot[0]/VButton[0]/domChild[0]/domChild[0]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>vaadin=runcomvaadintestscomponentsuiCurrentUiRetained::PID_SLog_row_3</td>
+ <td>1. Correct UI.getCurrent before GC: true</td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>vaadin=runcomvaadintestscomponentsuiCurrentUiRetained::PID_SLog_row_2</td>
+ <td>2. Correct UI.getCurrent after GC: true</td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>vaadin=runcomvaadintestscomponentsuiCurrentUiRetained::PID_SLog_row_1</td>
+ <td>3. GC probe available before GC: true</td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>vaadin=runcomvaadintestscomponentsuiCurrentUiRetained::PID_SLog_row_0</td>
+ <td>4. GC probe available after GC: false</td>
+</tr>
+
+</tbody></table>
+</body>
+</html>
--- /dev/null
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * 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 com.vaadin.tests.components.ui;
+
+import java.util.ArrayList;
+
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUIWithLog;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.UI;
+import com.vaadin.util.CurrentInstance;
+
+public class CurrentUiRetained extends AbstractTestUIWithLog {
+ public static class GcProbe {
+
+ }
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ final ArrayList<UI> uiLog = new ArrayList<UI>();
+ final ArrayList<Boolean> probeLog = new ArrayList<Boolean>();
+
+ final Thread thread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ uiLog.add(UI.getCurrent());
+
+ GcProbe gcProbe = new GcProbe();
+ CurrentInstance.set(GcProbe.class, gcProbe);
+ probeLog.add(CurrentInstance.get(GcProbe.class) != null);
+ gcProbe = null;
+
+ Thread.sleep(500l);
+ System.gc();
+ Thread.sleep(500l);
+
+ probeLog.add(CurrentInstance.get(GcProbe.class) != null);
+ uiLog.add(UI.getCurrent());
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ thread.start();
+
+ addComponent(new Button("Show result", new Button.ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ try {
+ thread.join();
+
+ log("Correct UI.getCurrent before GC: "
+ + (uiLog.get(0) == CurrentUiRetained.this));
+ log("Correct UI.getCurrent after GC: "
+ + (uiLog.get(1) == CurrentUiRetained.this));
+
+ log("GC probe available before GC: " + probeLog.get(0));
+ log("GC probe available after GC: " + probeLog.get(1));
+
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }));
+ }
+
+ @Override
+ protected String getTestDescription() {
+ return "Tests that garbage collection removes stale CurrentInstance values while retaining values not collected.";
+ }
+
+ @Override
+ protected Integer getTicketNumber() {
+ return Integer.valueOf(12509);
+ }
+
+}