content: /*******************************************************************************
   * Copyright (c) 2000, 2008 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.wst.jsdt.internal.corext.util;
  
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.io.OutputStream;
  import java.util.Collection;
  import java.util.Hashtable;
  import java.util.Iterator;
  import java.util.LinkedHashMap;
  import java.util.Map;
  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.TransformerFactoryConfigurationError;
  import javax.xml.transform.dom.DOMSource;
  import javax.xml.transform.stream.StreamResult;
  
  import org.eclipse.core.runtime.CoreException;
  import org.eclipse.core.runtime.IPath;
  import org.eclipse.core.runtime.IStatus;
  import org.eclipse.wst.jsdt.internal.corext.CorextMessages;
  import org.eclipse.wst.jsdt.internal.ui.JavaScriptPlugin;
  import org.eclipse.wst.jsdt.internal.ui.JavaUIException;
  import org.eclipse.wst.jsdt.internal.ui.JavaUIStatus;
  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;
  
  /**
   * History stores a list of key, object pairs. The list is bounded at size
   * MAX_HISTORY_SIZE. If the list exceeds this size the eldest element is removed
   * from the list. An element can be added/renewed with a call to <code>accessed(Object)</code>. 
   * 
   * The history can be stored to/loaded from an xml file.
   */
  public abstract class History {
  
  	private static final String DEFAULT_ROOT_NODE_NAME= "histroyRootNode"; //$NON-NLS-1$
  	private static final String DEFAULT_INFO_NODE_NAME= "infoNode"; //$NON-NLS-1$
  	private static final int MAX_HISTORY_SIZE= 60;
  
  	private static JavaUIException createException(Throwable t, String message) {
  		return new JavaUIException(JavaUIStatus.createError(IStatus.ERROR, message, t));
  	}
  
  	private final Map fHistory;
  	private final Hashtable fPositions;
  	private final String fFileName;
  	private final String fRootNodeName;
  	private final String fInfoNodeName;
  		
  	public History(String fileName, String rootNodeName, String infoNodeName) {
  		fHistory= new LinkedHashMap(80, 0.75f, true) {
  			private static final long serialVersionUID= 1L;
  			protected boolean removeEldestEntry(Map.Entry eldest) {
  				return size() > MAX_HISTORY_SIZE;
  			}
  		};
  		fFileName= fileName;
  		fRootNodeName= rootNodeName;
  		fInfoNodeName= infoNodeName;
  		fPositions= new Hashtable(MAX_HISTORY_SIZE);
  	}
  	
  	public History(String fileName) {
  		this(fileName, DEFAULT_ROOT_NODE_NAME, DEFAULT_INFO_NODE_NAME);
  	}
  	
  	public synchronized void accessed(Object object) {
  		fHistory.put(getKey(object), object);
  		rebuildPositions();
  	}
  	
  	public synchronized boolean contains(Object object) {
  		return fHistory.containsKey(getKey(object));
  	}
  	
  	public synchronized boolean containsKey(Object key) {
  		return fHistory.containsKey(key);
  	}
  	
  	public synchronized boolean isEmpty() {
  		return fHistory.isEmpty();
  	}
  	
  	public synchronized Object remove(Object object) {
  		Object removed= fHistory.remove(getKey(object));
  		rebuildPositions();
  		return removed;
  	}
  	
  	public synchronized Object removeKey(Object key) {
  		Object removed= fHistory.remove(key);
  		rebuildPositions();
  		return removed;
  	}
  	
  	/**
  	 * Normalized position in history of object denoted by key.
  	 * The position is a value between zero and one where zero
  	 * means not contained in history and one means newest element
  	 * in history. The lower the value the older the element.
  	 * 
  	 * @param key The key of the object to inspect
  	 * @return value in [0.0, 1.0] the lower the older the element
  	 */
  	public synchronized float getNormalizedPosition(Object key) {
  		if (!containsKey(key)) 
  			return 0.0f;
  
  		int pos= ((Integer)fPositions.get(key)).intValue() + 1;
  		
  		//containsKey(key) implies fHistory.size()>0	
  		return (float)pos / (float)fHistory.size();
  	}
  	
  	/**
  	 * Absolute position of object denoted by key in the
  	 * history or -1 if !containsKey(key). The higher the
  	 * newer.
  	 * 
  	 * @param key The key of the object to inspect
  	 * @return value between 0 and MAX_HISTORY_SIZE - 1, or -1
  	 */
  	public synchronized int getPosition(Object key) {
  		if (!containsKey(key))
  			return -1;
  		
  		return ((Integer)fPositions.get(key)).intValue();
  	}
  
  	public synchronized void load() {
  		IPath stateLocation= JavaScriptPlugin.getDefault().getStateLocation().append(fFileName);
  		File file= new File(stateLocation.toOSString());
  		if (file.exists()) {
  			InputStreamReader reader= null;
  	        try {
  				reader = new InputStreamReader(new FileInputStream(file), "utf-8");//$NON-NLS-1$
  				load(new InputSource(reader));
  			} catch (IOException e) {
  				JavaScriptPlugin.log(e);
  			} catch (CoreException e) {
  				JavaScriptPlugin.log(e);
  			} finally {
  				try {
  					if (reader != null)
  						reader.close();
  				} catch (IOException e) {
  					JavaScriptPlugin.log(e);
  				}
  			}
  		}
  	}
  	
  	public synchronized void save() {
  		IPath stateLocation= JavaScriptPlugin.getDefault().getStateLocation().append(fFileName);
  		File file= new File(stateLocation.toOSString());
  		OutputStream out= null;
  		try {
  			out= new FileOutputStream(file); 
  			save(out);
  		} catch (IOException e) {
  			JavaScriptPlugin.log(e);
  		} catch (CoreException e) {
  			JavaScriptPlugin.log(e);
  		} catch (TransformerFactoryConfigurationError e) {
  			// The XML library can be misconficgured (e.g. via 
  			// -Djava.endorsed.dirs=C:\notExisting\xerces-2_7_1)
  			JavaScriptPlugin.log(e);
  		} finally {
  			try {
  				if (out != null) {
  					out.close();
  				}
  			} catch (IOException e) {
  				JavaScriptPlugin.log(e);
  			}
  		}
  	}
  	
  	protected Set getKeys() {
  		return fHistory.keySet();
  	}
  	
  	protected Collection getValues() {
  		return fHistory.values();
  	}
  	
  	/**
  	 * Store <code>Object</code> in <code>Element</code>
  	 * 
  	 * @param object The object to store
  	 * @param element The Element to store to
  	 */
  	protected abstract void setAttributes(Object object, Element element);
  	
  	/**
  	 * Return a new instance of an Object given <code>element</code>
  	 * 
  	 * @param element The element containing required information to create the Object
  	 */
  	protected abstract Object createFromElement(Element element);
  	
  	/**
  	 * Get key for object
  	 * 
  	 * @param object The object to calculate a key for, not null
  	 * @return The key for object, not null
  	 */
  	protected abstract Object getKey(Object object);
  	
  	private void rebuildPositions() {
  		fPositions.clear();
  		Collection values= fHistory.values();
  		int pos=0;
  		for (Iterator iter= values.iterator(); iter.hasNext();) {
  			Object element= iter.next();
  			fPositions.put(getKey(element), Integer.valueOf(pos));
  			pos++;
  		}
  	}
  
  	private void load(InputSource inputSource) throws CoreException {
  		Element root;
  		try {
  			DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
  			root = parser.parse(inputSource).getDocumentElement();
  		} catch (SAXException e) {
  			throw createException(e, Messages.format(CorextMessages.History_error_read, fFileName));  
  		} catch (ParserConfigurationException e) {
  			throw createException(e, Messages.format(CorextMessages.History_error_read, fFileName)); 
  		} catch (IOException e) {
  			throw createException(e, Messages.format(CorextMessages.History_error_read, fFileName)); 
  		}
  		
  		if (root == null) return;
  		if (!root.getNodeName().equalsIgnoreCase(fRootNodeName)) {
  			return;
  		}
  		NodeList list= root.getChildNodes();
  		int length= list.getLength();
  		for (int i= 0; i < length; ++i) {
  			Node node= list.item(i);
  			if (node.getNodeType() == Node.ELEMENT_NODE) {
  				Element type= (Element) node;
  				if (type.getNodeName().equalsIgnoreCase(fInfoNodeName)) {
  					Object object= createFromElement(type);
  					if (object != null) {
  						fHistory.put(getKey(object), object);
  					}
  				}
  			}
  		}
  		rebuildPositions();
  	}
  	
  	private void save(OutputStream stream) throws CoreException {
  		try {
  			DocumentBuilderFactory factory= DocumentBuilderFactory.newInstance();
  			DocumentBuilder builder= factory.newDocumentBuilder();		
  			Document document= builder.newDocument();
  			
  			Element rootElement = document.createElement(fRootNodeName);
  			document.appendChild(rootElement);
  	
  			Iterator values= getValues().iterator();
  			while (values.hasNext()) {
  				Object object= values.next();
  				Element element= document.createElement(fInfoNodeName);
  				setAttributes(object, element);
  				rootElement.appendChild(element);
  			}
  			
  			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, "yes"); //$NON-NLS-1$
  			DOMSource source = new DOMSource(document);
  			StreamResult result = new StreamResult(stream);
  
  			transformer.transform(source, result);
  		} catch (TransformerException e) {
  			throw createException(e, Messages.format(CorextMessages.History_error_serialize, fFileName));
  		} catch (ParserConfigurationException e) {
  			throw createException(e, Messages.format(CorextMessages.History_error_serialize, fFileName));
  		}
  	}
  
  }
  