/*******************************************************************************
* Copyright (c) 2005, 2011 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.che.ide.ext.java.jdt.codeassistant;
import org.eclipse.che.ide.ext.java.jdt.core.Flags;
import org.eclipse.che.ide.ext.java.jdt.core.IType;
import org.eclipse.che.ide.ext.java.worker.Preferences;
import org.eclipse.che.ide.runtime.Assert;
import org.eclipse.che.ide.runtime.CoreException;
import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONString;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/** An LRU cache for code assist. */
public final class ContentAssistHistory {
/** Persistence implementation. */
private static final class ReaderWriter {
private static final String NODE_ROOT = "history"; //$NON-NLS-1$
// private static final String NODE_LHS = "lhs"; //$NON-NLS-1$
//
// private static final String NODE_RHS = "rhs"; //$NON-NLS-1$
//
// private static final String ATTRIBUTE_NAME = "name"; //$NON-NLS-1$
private static final String ATTRIBUTE_MAX_LHS = "maxLHS"; //$NON-NLS-1$
private static final String ATTRIBUTE_MAX_RHS = "maxRHS"; //$NON-NLS-1$
public void store(ContentAssistHistory history, StringBuilder result) {
JSONObject root = new JSONObject();
root.put(ATTRIBUTE_MAX_LHS, new JSONString(Integer.toString(history.fMaxLHS)));
root.put(ATTRIBUTE_MAX_RHS, new JSONString(Integer.toString(history.fMaxRHS)));
JSONObject historyJs = new JSONObject();
for (Iterator<String> leftHandSides = history.fLHSCache.keySet().iterator(); leftHandSides.hasNext(); ) {
String lhs = leftHandSides.next();
JSONArray arr = new JSONArray();
MRUSet<String> rightHandSides = history.fLHSCache.get(lhs);
int i = 0;
for (Iterator<String> rhsIterator = rightHandSides.iterator(); rhsIterator.hasNext(); ) {
String rhs = rhsIterator.next();
arr.set(i, new JSONString(rhs));
i++;
}
historyJs.put(lhs, arr);
}
root.put(NODE_ROOT, historyJs);
result.append(root.toString());
}
public ContentAssistHistory load(String source) {
JSONObject root = JSONParser.parseLenient(source).isObject();
int maxLHS = parseNaturalInt(root.get(ATTRIBUTE_MAX_LHS).isString().stringValue(), DEFAULT_TRACKED_LHS);
int maxRHS = parseNaturalInt(root.get(ATTRIBUTE_MAX_RHS).isString().stringValue(), DEFAULT_TRACKED_RHS);
ContentAssistHistory history = new ContentAssistHistory(maxLHS, maxRHS);
JSONObject list = root.get(NODE_ROOT).isObject();
for (String lhs : list.keySet()) {
Set<String> cache = history.getCache(lhs);
JSONArray arr = list.get(lhs).isArray();
for (int i = 0; i < arr.size(); i++) {
cache.add(arr.get(i).isString().stringValue());
}
}
return history;
}
private int parseNaturalInt(String attribute, int defaultValue) {
try {
int integer = Integer.parseInt(attribute);
if (integer > 0)
return integer;
return defaultValue;
} catch (NumberFormatException e) {
return defaultValue;
}
}
}
/**
* Most recently used variant with capped size that only counts {@linkplain #put(Object, Object) put} as access. This is
* implemented by always removing an element before it gets put back.
*
* @since 3.2
*/
private static final class MRUMap<K, V> extends LinkedHashMap<K, V> {
private static final long serialVersionUID = 1L;
private final int fMaxSize;
/**
* Creates a new <code>MRUMap</code> with the given size.
*
* @param maxSize
* the maximum size of the cache, must be > 0
*/
public MRUMap(int maxSize) {
Assert.isLegal(maxSize > 0);
fMaxSize = maxSize;
}
/*
* @see java.util.HashMap#put(java.lang.Object, java.lang.Object)
*/
@Override
public V put(K key, V value) {
V object = remove(key);
super.put(key, value);
return object;
}
/*
* @see java.util.LinkedHashMap#removeEldestEntry(java.util.Map.Entry)
*/
@Override
protected boolean removeEldestEntry(Entry<K, V> eldest) {
return size() > fMaxSize;
}
}
/**
* Most recently used variant with capped size that orders the elements by addition. This is implemented by always removing an
* element before it gets added back.
*
* @since 3.2
*/
private static final class MRUSet<E> extends LinkedHashSet<E> {
private static final long serialVersionUID = 1L;
private final int fMaxSize;
/**
* Creates a new <code>MRUSet</code> with the given size.
*
* @param maxSize
* the maximum size of the cache, must be > 0
*/
public MRUSet(int maxSize) {
Assert.isLegal(maxSize > 0);
fMaxSize = maxSize;
}
/*
* @see java.util.HashSet#add(java.lang.Object)
*/
@Override
public boolean add(E o) {
if (remove(o)) {
super.add(o);
return false;
}
if (size() >= fMaxSize)
remove(this.iterator().next());
super.add(o);
return true;
}
}
/**
* A ranking of the most recently selected types.
*
* @since 3.2
*/
public static final class RHSHistory {
private final LinkedHashMap<String, Integer> fHistory;
private List<String> fList;
RHSHistory(LinkedHashMap<String, Integer> history) {
fHistory = history;
}
/**
* Returns the rank of a type in the history in [0.0, 1.0]. The rank of the most recently selected type is 1.0, the
* rank of any type that is not remembered is zero.
*
* @param type
* the fully qualified type name to get the rank for
* @return the rank of <code>type</code>
*/
public float getRank(String type) {
if (fHistory == null)
return 0.0F;
Integer integer = fHistory.get(type);
return integer == null ? 0.0F : integer.floatValue() / fHistory.size();
}
/**
* Returns the size of the history.
*
* @return the size of the history
*/
public int size() {
return fHistory == null ? 0 : fHistory.size();
}
/**
* Returns the list of remembered types ordered by recency. The first element is the <i>least</i>, the last element the
* <i>most</i> recently remembered type.
*
* @return the list of remembered types as fully qualified type names
*/
public List<String> getTypes() {
if (fHistory == null)
return Collections.emptyList();
if (fList == null) {
fList = Collections.unmodifiableList(new ArrayList<String>(fHistory.keySet()));
}
return fList;
}
}
private static final RHSHistory EMPTY_HISTORY = new RHSHistory(null);
private static final int DEFAULT_TRACKED_LHS = 100;
private static final int DEFAULT_TRACKED_RHS = 10;
private static final Set<String> UNCACHEABLE;
static {
Set<String> uncacheable = new HashSet<String>();
uncacheable.add("java.lang.Object"); //$NON-NLS-1$
uncacheable.add("java.lang.Comparable"); //$NON-NLS-1$
uncacheable.add("java.io.Serializable"); //$NON-NLS-1$
uncacheable.add("java.io.Externalizable"); //$NON-NLS-1$
UNCACHEABLE = Collections.unmodifiableSet(uncacheable);
}
private final LinkedHashMap<String, MRUSet<String>> fLHSCache;
private final int fMaxLHS;
private final int fMaxRHS;
/**
* Creates a new history.
*
* @param maxLHS
* the maximum number of tracked left hand sides (> 0)
* @param maxRHS
* the maximum number of tracked right hand sides per left hand side(> 0)
*/
public ContentAssistHistory(int maxLHS, int maxRHS) {
Assert.isLegal(maxLHS > 0);
Assert.isLegal(maxRHS > 0);
fMaxLHS = maxLHS;
fMaxRHS = maxRHS;
fLHSCache = new MRUMap<String, MRUSet<String>>(fMaxLHS);
}
/** Creates a new history, equivalent to <code>ContentAssistHistory(DEFAULT_TRACKED_LHS, DEFAULT_TRACKED_RHS})</code>. */
public ContentAssistHistory() {
this(DEFAULT_TRACKED_LHS, DEFAULT_TRACKED_RHS);
}
/**
* Remembers the selection of a right hand side type (proposal type) for a certain left hand side (expected type) in content
* assist.
*
* @param lhs
* the left hand side / expected type
* @param rhs
* the selected right hand side
*/
public void remember(IType lhs, IType rhs) {
Assert.isLegal(lhs != null);
Assert.isLegal(rhs != null);
if (!isCacheableRHS(rhs))
return;
// ITypeHierarchy hierarchy = rhs.newSupertypeHierarchy(getProgressMonitor());
// if (hierarchy.contains(lhs))
// {
// // TODO remember for every member of the LHS hierarchy or not? Yes for now.
// IGenericType[] allLHSides = hierarchy.getAllSupertypes(lhs);
String rhsQualifiedName = rhs.getFullyQualifiedName();
// for (int i = 0; i < allLHSides.length; i++)
// rememberInternal(allLHSides[i], rhsQualifiedName);
rememberInternal(lhs, rhsQualifiedName);
// }
}
/**
* Returns the {@link RHSHistory history} of the types that have been selected most recently as right hand sides for the given
* type.
*
* @param lhs
* the fully qualified type name of an expected type for which right hand sides are requested, or <code>null</code>
* @return the right hand side history for the given type
*/
public RHSHistory getHistory(String lhs) {
MRUSet<String> rhsCache = fLHSCache.get(lhs);
if (rhsCache != null) {
int count = rhsCache.size();
LinkedHashMap<String, Integer> history = new LinkedHashMap<String, Integer>((int)(count / 0.75));
int rank = 1;
for (Iterator<String> it = rhsCache.iterator(); it.hasNext(); rank++) {
String type = it.next();
history.put(type, new Integer(rank));
}
return new RHSHistory(history);
}
return EMPTY_HISTORY;
}
/**
* Returns a read-only map from {@link IType} to {@link RHSHistory}, where each value is the history for the key type (see
* {@link #getHistory(String)}.
*
* @return the set of remembered right hand sides ordered by least recent selection
*/
public Map<String, RHSHistory> getEntireHistory() {
HashMap<String, RHSHistory> map = new HashMap<String, RHSHistory>((int)(fLHSCache.size() / 0.75));
for (Iterator<Entry<String, MRUSet<String>>> it = fLHSCache.entrySet().iterator(); it.hasNext(); ) {
Entry<String, MRUSet<String>> entry = it.next();
String lhs = entry.getKey();
map.put(lhs, getHistory(lhs));
}
return Collections.unmodifiableMap(map);
}
private void rememberInternal(IType lhs, String rhsQualifiedName) {
String lhsQualifiedName = lhs.getFullyQualifiedName();
if (isCacheableLHS(lhs, lhsQualifiedName))
getCache(lhsQualifiedName).add(rhsQualifiedName);
}
private boolean isCacheableLHS(IType type, String qualifiedName) {
return !Flags.isFinal(type.getFlags()) && !UNCACHEABLE.contains(qualifiedName);
}
private boolean isCacheableRHS(IType type) {
return !Flags.isInterface(type.getFlags()) && !Flags.isAbstract(type.getFlags());
}
private Set<String> getCache(String lhs) {
MRUSet<String> rhsCache = fLHSCache.get(lhs);
if (rhsCache == null) {
rhsCache = new MRUSet<String>(fMaxRHS);
fLHSCache.put(lhs, rhsCache);
}
return rhsCache;
}
/**
* Stores the history as JSON into the given preferences.
*
* @param history
* the history to store
* @param preferences
* the preferences to store the history into
* @param key
* the key under which to store the history
* @throws CoreException
* if serialization fails
* @see #load(Preferences, String) on how to restore a history stored by this method
*/
public static void store(ContentAssistHistory history, Preferences preferences, String key) {
StringBuilder result = new StringBuilder();
new ReaderWriter().store(history, result);
preferences.setValue(key, result.toString());
}
/**
* Loads a history from an JSON encoded preference value.
*
* @param preferences
* the preferences to retrieve the history from
* @param key
* the key under which the history is stored
* @return the deserialized history, or <code>null</code> if there is nothing stored under the given key
* @throws CoreException
* if deserialization fails
* @see #store(ContentAssistHistory, Preferences, String) on how to store a history such that it can be read by this method
*/
public static ContentAssistHistory load(Preferences preferences, String key) {
String value = preferences.getString(key);
if (value != null && value.length() > 0) {
return new ReaderWriter().load(value);
}
return null;
}
}