/*
* Copyright (c) 2012, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.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.eclipse.org/legal/epl-v10.html
*
* 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.google.dart.tools.ui.internal.text.dart;
import com.google.dart.tools.ui.internal.DartUiException;
import com.google.dart.tools.ui.internal.DartUiStatus;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Preferences;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
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;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
/**
* An LRU cache for code assist.
*/
@SuppressWarnings({"rawtypes", "unchecked", "deprecation"})
public final class ContentAssistHistory {
/**
* A ranking of the most recently selected types.
*/
public static final class RHSHistory {
private final LinkedHashMap fHistory;
private List fList;
RHSHistory(LinkedHashMap 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 = (Integer) fHistory.get(type);
return integer == null ? 0.0F : integer.floatValue() / 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 (element type:
* {@link String})
*/
public List getTypes() {
if (fHistory == null) {
return Collections.EMPTY_LIST;
}
if (fList == null) {
fList = Collections.unmodifiableList(new ArrayList(fHistory.keySet()));
}
return fList;
}
/**
* Returns the size of the history.
*
* @return the size of the history
*/
public int size() {
return fHistory == null ? 0 : fHistory.size();
}
}
/**
* 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.
*/
private static final class MRUMap extends LinkedHashMap {
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 Object put(Object key, Object value) {
Object object = remove(key);
super.put(key, value);
return object;
}
/*
* @see java.util.LinkedHashMap#removeEldestEntry(java.util.Map.Entry)
*/
@Override
protected boolean removeEldestEntry(Entry 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.
*/
private static final class MRUSet extends LinkedHashSet {
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(Object o) {
if (remove(o)) {
super.add(o);
return false;
}
if (size() >= fMaxSize) {
remove(this.iterator().next());
}
super.add(o);
return true;
}
}
/**
* 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 ContentAssistHistory load(InputSource source) throws CoreException {
Element root;
try {
DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
root = parser.parse(source).getDocumentElement();
} catch (SAXException e) {
throw createException(e, DartTextMessages.ContentAssistHistory_deserialize_error);
} catch (ParserConfigurationException e) {
throw createException(e, DartTextMessages.ContentAssistHistory_deserialize_error);
} catch (IOException e) {
throw createException(e, DartTextMessages.ContentAssistHistory_deserialize_error);
}
if (root == null || !root.getNodeName().equalsIgnoreCase(NODE_ROOT)) {
return null;
}
int maxLHS = parseNaturalInt(root.getAttribute(ATTRIBUTE_MAX_LHS), DEFAULT_TRACKED_LHS);
int maxRHS = parseNaturalInt(root.getAttribute(ATTRIBUTE_MAX_RHS), DEFAULT_TRACKED_RHS);
ContentAssistHistory history = new ContentAssistHistory(maxLHS, maxRHS);
NodeList list = root.getChildNodes();
int length = list.getLength();
for (int i = 0; i < length; ++i) {
Node lhsNode = list.item(i);
if (lhsNode.getNodeType() == Node.ELEMENT_NODE) {
Element lhsElement = (Element) lhsNode;
if (lhsElement.getNodeName().equalsIgnoreCase(NODE_LHS)) {
String lhs = lhsElement.getAttribute(ATTRIBUTE_NAME);
if (lhs != null) {
Set cache = history.getCache(lhs);
NodeList children = lhsElement.getChildNodes();
int nRHS = children.getLength();
for (int j = 0; j < nRHS; j++) {
Node rhsNode = children.item(j);
if (rhsNode.getNodeType() == Node.ELEMENT_NODE) {
Element rhsElement = (Element) rhsNode;
if (rhsElement.getNodeName().equalsIgnoreCase(NODE_RHS)) {
String rhs = rhsElement.getAttribute(ATTRIBUTE_NAME);
if (rhs != null) {
cache.add(rhs);
}
}
}
}
}
}
}
}
return history;
}
public void store(ContentAssistHistory history, StreamResult result) throws CoreException {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.newDocument();
Element rootElement = document.createElement(NODE_ROOT);
rootElement.setAttribute(ATTRIBUTE_MAX_LHS, Integer.toString(history.fMaxLHS));
rootElement.setAttribute(ATTRIBUTE_MAX_RHS, Integer.toString(history.fMaxRHS));
document.appendChild(rootElement);
for (Iterator leftHandSides = history.fLHSCache.keySet().iterator(); leftHandSides.hasNext();) {
String lhs = (String) leftHandSides.next();
Element lhsElement = document.createElement(NODE_LHS);
lhsElement.setAttribute(ATTRIBUTE_NAME, lhs);
rootElement.appendChild(lhsElement);
Set rightHandSides = (Set) history.fLHSCache.get(lhs);
for (Iterator rhsIterator = rightHandSides.iterator(); rhsIterator.hasNext();) {
String rhs = (String) rhsIterator.next();
Element rhsElement = document.createElement(NODE_RHS);
rhsElement.setAttribute(ATTRIBUTE_NAME, rhs);
lhsElement.appendChild(rhsElement);
}
}
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.METHOD, "xml"); //$NON-NLS-1$
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); //$NON-NLS-1$
transformer.setOutputProperty(OutputKeys.INDENT, "no"); //$NON-NLS-1$
DOMSource source = new DOMSource(document);
transformer.transform(source, result);
} catch (TransformerException e) {
throw createException(e, DartTextMessages.ContentAssistHistory_serialize_error);
} catch (ParserConfigurationException e) {
throw createException(e, DartTextMessages.ContentAssistHistory_serialize_error);
}
}
private DartUiException createException(Exception e, String message) {
return new DartUiException(DartUiStatus.createError(IStatus.ERROR, message, e));
}
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;
}
}
}
public static final RHSHistory EMPTY_HISTORY = new RHSHistory(null);
private static final int DEFAULT_TRACKED_LHS = 100;
private static final int DEFAULT_TRACKED_RHS = 10;
/**
* Loads a history from an XML 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) throws CoreException {
String value = preferences.getString(key);
if (value != null && value.length() > 0) {
return new ReaderWriter().load(new InputSource(new StringReader(value)));
}
return null;
}
/**
* Stores the history as XML document 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)
throws CoreException {
StringWriter writer = new StringWriter();
new ReaderWriter().store(history, new StreamResult(writer));
preferences.setValue(key, writer.toString());
}
private final LinkedHashMap/* <Type, MRUSet<Type>> */fLHSCache;
private final int fMaxLHS;
private final int fMaxRHS;
/**
* Creates a new history, equivalent to
* <code>ContentAssistHistory(DEFAULT_TRACKED_LHS, DEFAULT_TRACKED_RHS})</code> .
*/
public ContentAssistHistory() {
this(DEFAULT_TRACKED_LHS, DEFAULT_TRACKED_RHS);
}
/**
* 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(fMaxLHS);
}
/**
* Returns a read-only map from {@link Type} 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 getEntireHistory() {
HashMap map = new HashMap((int) (fLHSCache.size() / 0.75));
for (Iterator it = fLHSCache.entrySet().iterator(); it.hasNext();) {
Entry entry = (Entry) it.next();
String lhs = (String) entry.getKey();
map.put(lhs, getHistory(lhs));
}
return Collections.unmodifiableMap(map);
}
/**
* 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 rhsCache = (MRUSet) fLHSCache.get(lhs);
if (rhsCache != null) {
int count = rhsCache.size();
LinkedHashMap history = new LinkedHashMap((int) (count / 0.75));
int rank = 1;
for (Iterator it = rhsCache.iterator(); it.hasNext(); rank++) {
String type = (String) it.next();
history.put(type, new Integer(rank));
}
return new RHSHistory(history);
}
return EMPTY_HISTORY;
}
private Set getCache(String lhs) {
MRUSet rhsCache = (MRUSet) fLHSCache.get(lhs);
if (rhsCache == null) {
rhsCache = new MRUSet(fMaxRHS);
fLHSCache.put(lhs, rhsCache);
}
return rhsCache;
}
}