1 /*******************************************************************************
2  * Copyright (c) 2000, 2016 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *******************************************************************************/
14 package org.eclipse.jdt.internal.corext.util;
15 
16 import java.io.File;
17 import java.io.FileInputStream;
18 import java.io.FileOutputStream;
19 import java.io.IOException;
20 import java.io.InputStreamReader;
21 import java.io.OutputStream;
22 import java.util.Collection;
23 import java.util.Hashtable;
24 import java.util.Iterator;
25 import java.util.LinkedHashMap;
26 import java.util.Map;
27 import java.util.Set;
28 
29 import javax.xml.parsers.DocumentBuilder;
30 import javax.xml.parsers.DocumentBuilderFactory;
31 import javax.xml.parsers.ParserConfigurationException;
32 import javax.xml.transform.OutputKeys;
33 import javax.xml.transform.Transformer;
34 import javax.xml.transform.TransformerException;
35 import javax.xml.transform.TransformerFactory;
36 import javax.xml.transform.TransformerFactoryConfigurationError;
37 import javax.xml.transform.dom.DOMSource;
38 import javax.xml.transform.stream.StreamResult;
39 
40 import org.xml.sax.InputSource;
41 import org.xml.sax.SAXException;
42 import org.xml.sax.helpers.DefaultHandler;
43 
44 import org.w3c.dom.Document;
45 import org.w3c.dom.Element;
46 import org.w3c.dom.Node;
47 import org.w3c.dom.NodeList;
48 
49 import org.eclipse.core.runtime.CoreException;
50 import org.eclipse.core.runtime.IPath;
51 import org.eclipse.core.runtime.IStatus;
52 
53 import org.eclipse.jdt.internal.corext.CorextMessages;
54 
55 import org.eclipse.jdt.internal.ui.JavaPlugin;
56 import org.eclipse.jdt.internal.ui.JavaUIException;
57 import org.eclipse.jdt.internal.ui.JavaUIStatus;
58 import org.eclipse.jdt.internal.core.manipulation.util.BasicElementLabels;
59 
60 /**
61  * History stores a list of key, object pairs. The list is bounded at size
62  * MAX_HISTORY_SIZE. If the list exceeds this size the eldest element is removed
63  * from the list. An element can be added/renewed with a call to <code>accessed(Object)</code>.
64  *
65  * The history can be stored to/loaded from an xml file.
66  *
67  * @param <K> key type
68  * @param <V> value type
69  */
70 public abstract class History<K, V> {
71 
72 	private static final String DEFAULT_ROOT_NODE_NAME= "histroyRootNode"; //$NON-NLS-1$
73 	private static final String DEFAULT_INFO_NODE_NAME= "infoNode"; //$NON-NLS-1$
74 	private static final int MAX_HISTORY_SIZE= 60;
75 
createException(Throwable t, String message)76 	private static JavaUIException createException(Throwable t, String message) {
77 		return new JavaUIException(JavaUIStatus.createError(IStatus.ERROR, message, t));
78 	}
79 
80 	private final Map<K, V> fHistory;
81 	private final Hashtable<K, Integer> fPositions;
82 	private final String fFileName;
83 	private final String fRootNodeName;
84 	private final String fInfoNodeName;
85 
History(String fileName, String rootNodeName, String infoNodeName)86 	public History(String fileName, String rootNodeName, String infoNodeName) {
87 		fHistory= new LinkedHashMap<K, V>(80, 0.75f, true) {
88 			private static final long serialVersionUID= 1L;
89 			@Override
90 			protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
91 				return size() > MAX_HISTORY_SIZE;
92 			}
93 		};
94 		fFileName= fileName;
95 		fRootNodeName= rootNodeName;
96 		fInfoNodeName= infoNodeName;
97 		fPositions= new Hashtable<>(MAX_HISTORY_SIZE);
98 	}
99 
History(String fileName)100 	public History(String fileName) {
101 		this(fileName, DEFAULT_ROOT_NODE_NAME, DEFAULT_INFO_NODE_NAME);
102 	}
103 
accessed(V object)104 	public synchronized void accessed(V object) {
105 		fHistory.put(getKey(object), object);
106 		rebuildPositions();
107 	}
108 
contains(V object)109 	public synchronized boolean contains(V object) {
110 		return fHistory.containsKey(getKey(object));
111 	}
112 
containsKey(K key)113 	public synchronized boolean containsKey(K key) {
114 		return fHistory.containsKey(key);
115 	}
116 
isEmpty()117 	public synchronized boolean isEmpty() {
118 		return fHistory.isEmpty();
119 	}
120 
remove(V object)121 	public synchronized Object remove(V object) {
122 		Object removed= fHistory.remove(getKey(object));
123 		rebuildPositions();
124 		return removed;
125 	}
126 
removeKey(Object key)127 	public synchronized Object removeKey(Object key) {
128 		Object removed= fHistory.remove(key);
129 		rebuildPositions();
130 		return removed;
131 	}
132 
133 	/**
134 	 * Normalized position in history of object denoted by key.
135 	 * The position is a value between zero and one where zero
136 	 * means not contained in history and one means newest element
137 	 * in history. The lower the value the older the element.
138 	 *
139 	 * @param key The key of the object to inspect
140 	 * @return value in [0.0, 1.0] the lower the older the element
141 	 */
getNormalizedPosition(K key)142 	public synchronized float getNormalizedPosition(K key) {
143 		if (!containsKey(key))
144 			return 0.0f;
145 
146 		int pos= fPositions.get(key).intValue() + 1;
147 
148 		//containsKey(key) implies fHistory.size()>0
149 		return (float)pos / (float)fHistory.size();
150 	}
151 
152 	/**
153 	 * Absolute position of object denoted by key in the
154 	 * history or -1 if !containsKey(key). The higher the
155 	 * newer.
156 	 *
157 	 * @param key The key of the object to inspect
158 	 * @return value between 0 and MAX_HISTORY_SIZE - 1, or -1
159 	 */
getPosition(K key)160 	public synchronized int getPosition(K key) {
161 		if (!containsKey(key))
162 			return -1;
163 
164 		return fPositions.get(key).intValue();
165 	}
166 
load()167 	public synchronized void load() {
168 		IPath stateLocation= JavaPlugin.getDefault().getStateLocation().append(fFileName);
169 		File file= stateLocation.toFile();
170 		if (file.exists()) {
171 			InputStreamReader reader= null;
172 	        try {
173 				reader = new InputStreamReader(new FileInputStream(file), "utf-8");//$NON-NLS-1$
174 				load(new InputSource(reader));
175 			} catch (IOException e) {
176 				JavaPlugin.log(e);
177 			} catch (CoreException e) {
178 				JavaPlugin.log(e);
179 			} finally {
180 				try {
181 					if (reader != null)
182 						reader.close();
183 				} catch (IOException e) {
184 					JavaPlugin.log(e);
185 				}
186 			}
187 		}
188 	}
189 
save()190 	public synchronized void save() {
191 		IPath stateLocation= JavaPlugin.getDefault().getStateLocation().append(fFileName);
192 		File file= stateLocation.toFile();
193 		OutputStream out= null;
194 		try {
195 			out= new FileOutputStream(file);
196 			save(out);
197 		} catch (IOException e) {
198 			JavaPlugin.log(e);
199 		} catch (CoreException e) {
200 			JavaPlugin.log(e);
201 		} catch (TransformerFactoryConfigurationError e) {
202 			// The XML library can be misconficgured (e.g. via
203 			// -Djava.endorsed.dirs=C:\notExisting\xerces-2_7_1)
204 			JavaPlugin.log(e);
205 		} finally {
206 			try {
207 				if (out != null) {
208 					out.close();
209 				}
210 			} catch (IOException e) {
211 				JavaPlugin.log(e);
212 			}
213 		}
214 	}
215 
getKeys()216 	protected Set<K> getKeys() {
217 		return fHistory.keySet();
218 	}
219 
getValues()220 	protected Collection<V> getValues() {
221 		return fHistory.values();
222 	}
223 
224 	/**
225 	 * Store <code>Object</code> in <code>Element</code>
226 	 *
227 	 * @param object The object to store
228 	 * @param element The Element to store to
229 	 */
setAttributes(Object object, Element element)230 	protected abstract void setAttributes(Object object, Element element);
231 
232 	/**
233 	 * Return a new instance of an Object given <code>element</code>
234 	 *
235 	 * @param element The element containing required information to create the Object
236 	 * @return return a new instance of an Object given <code>element</code>
237 	 */
createFromElement(Element element)238 	protected abstract V createFromElement(Element element);
239 
240 	/**
241 	 * Get key for object
242 	 *
243 	 * @param object The object to calculate a key for, not null
244 	 * @return The key for object, not null
245 	 */
getKey(V object)246 	protected abstract K getKey(V object);
247 
rebuildPositions()248 	private void rebuildPositions() {
249 		fPositions.clear();
250 		int pos=0;
251 		for (V element : fHistory.values()) {
252 			fPositions.put(getKey(element), Integer.valueOf(pos));
253 			pos++;
254 		}
255 	}
256 
load(InputSource inputSource)257 	private void load(InputSource inputSource) throws CoreException {
258 		Element root;
259 		try {
260 			DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
261 			parser.setErrorHandler(new DefaultHandler());
262 			root = parser.parse(inputSource).getDocumentElement();
263 		} catch (SAXException e) {
264 			throw createException(e, Messages.format(CorextMessages.History_error_read, BasicElementLabels.getResourceName(fFileName)));
265 		} catch (ParserConfigurationException e) {
266 			throw createException(e, Messages.format(CorextMessages.History_error_read, BasicElementLabels.getResourceName(fFileName)));
267 		} catch (IOException e) {
268 			throw createException(e, Messages.format(CorextMessages.History_error_read, BasicElementLabels.getResourceName(fFileName)));
269 		}
270 
271 		if (root == null) return;
272 		if (!root.getNodeName().equalsIgnoreCase(fRootNodeName)) {
273 			return;
274 		}
275 		NodeList list= root.getChildNodes();
276 		int length= list.getLength();
277 		for (int i= 0; i < length; ++i) {
278 			Node node= list.item(i);
279 			if (node.getNodeType() == Node.ELEMENT_NODE) {
280 				Element type= (Element) node;
281 				if (type.getNodeName().equalsIgnoreCase(fInfoNodeName)) {
282 					V object= createFromElement(type);
283 					if (object != null) {
284 						fHistory.put(getKey(object), object);
285 					}
286 				}
287 			}
288 		}
289 		rebuildPositions();
290 	}
291 
save(OutputStream stream)292 	private void save(OutputStream stream) throws CoreException {
293 		try {
294 			DocumentBuilderFactory factory= DocumentBuilderFactory.newInstance();
295 			DocumentBuilder builder= factory.newDocumentBuilder();
296 			Document document= builder.newDocument();
297 
298 			Element rootElement = document.createElement(fRootNodeName);
299 			document.appendChild(rootElement);
300 
301 			Iterator<V> values= getValues().iterator();
302 			while (values.hasNext()) {
303 				Object object= values.next();
304 				Element element= document.createElement(fInfoNodeName);
305 				setAttributes(object, element);
306 				rootElement.appendChild(element);
307 			}
308 
309 			Transformer transformer=TransformerFactory.newInstance().newTransformer();
310 			transformer.setOutputProperty(OutputKeys.METHOD, "xml"); //$NON-NLS-1$
311 			transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); //$NON-NLS-1$
312 			transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$
313 			DOMSource source = new DOMSource(document);
314 			StreamResult result = new StreamResult(stream);
315 
316 			transformer.transform(source, result);
317 		} catch (TransformerException e) {
318 			throw createException(e, Messages.format(CorextMessages.History_error_serialize, BasicElementLabels.getResourceName(fFileName)));
319 		} catch (ParserConfigurationException e) {
320 			throw createException(e, Messages.format(CorextMessages.History_error_serialize, BasicElementLabels.getResourceName(fFileName)));
321 		}
322 	}
323 
324 }
325