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.ui.preferences.formatter;
  
  import java.io.ByteArrayInputStream;
  import java.io.ByteArrayOutputStream;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.OutputStream;
  import java.io.UnsupportedEncodingException;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  
  import javax.xml.parsers.DocumentBuilder;
  import javax.xml.parsers.DocumentBuilderFactory;
  import javax.xml.parsers.ParserConfigurationException;
  import javax.xml.parsers.SAXParser;
  import javax.xml.parsers.SAXParserFactory;
  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;
  
  import org.eclipse.core.runtime.CoreException;
  import org.eclipse.core.runtime.IStatus;
  import org.eclipse.core.runtime.preferences.IEclipsePreferences;
  import org.eclipse.core.runtime.preferences.IScopeContext;
  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.eclipse.wst.jsdt.internal.ui.preferences.formatter.ProfileManager.CustomProfile;
  import org.eclipse.wst.jsdt.internal.ui.preferences.formatter.ProfileManager.Profile;
  import org.eclipse.wst.jsdt.ui.JavaScriptUI;
  import org.w3c.dom.Document;
  import org.w3c.dom.Element;
  import org.xml.sax.Attributes;
  import org.xml.sax.InputSource;
  import org.xml.sax.SAXException;
  import org.xml.sax.helpers.DefaultHandler;
  
  
  /**
   * Can load/store profiles from/to profilesKey
   */
  public class ProfileStore {
  
  	/** The default encoding to use */
  	public static final String ENCODING= "UTF-8"; //$NON-NLS-1$
  	
  	protected static final String VERSION_KEY_SUFFIX= ".version"; //$NON-NLS-1$
  
  	/**
  	 * A SAX event handler to parse the xml format for profiles. 
  	 */
  	private final static class ProfileDefaultHandler extends DefaultHandler {
  		
  		private List fProfiles;
  		private int fVersion;
  		
  		private String fName;
  		private Map fSettings;
  		private String fKind;
  
  		public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
  
  			if (qName.equals(XML_NODE_SETTING)) {
  
  				final String key= attributes.getValue(XML_ATTRIBUTE_ID);
  				final String value= attributes.getValue(XML_ATTRIBUTE_VALUE);
  				fSettings.put(key, value);
  
  			} else if (qName.equals(XML_NODE_PROFILE)) {
  
  				fName= attributes.getValue(XML_ATTRIBUTE_NAME);
  				fKind= attributes.getValue(XML_ATTRIBUTE_PROFILE_KIND);
  				if (fKind == null) //Can only be an CodeFormatterProfile created pre 3.3M2
  					fKind= ProfileVersioner.CODE_FORMATTER_PROFILE_KIND;
  				
  				fSettings= new HashMap(200);
  
  			}
  			else if (qName.equals(XML_NODE_ROOT)) {
  
  				fProfiles= new ArrayList();
  				try {
  					fVersion= Integer.parseInt(attributes.getValue(XML_ATTRIBUTE_VERSION));
  				} catch (NumberFormatException ex) {
  					throw new SAXException(ex);
  				}
  
  			}
  		}
  		
  		public void endElement(String uri, String localName, String qName) {
  			if (qName.equals(XML_NODE_PROFILE)) {
  				fProfiles.add(new CustomProfile(fName, fSettings, fVersion, fKind));
  				fName= null;
  				fSettings= null;
  				fKind= null;
  			}
  		}
  		
  		public List getProfiles() {
  			return fProfiles;
  		}
  		
  	}
  	
  	/**
  	 * Identifiers for the XML file.
  	 */
  	private final static String XML_NODE_ROOT= "profiles"; //$NON-NLS-1$
  	private final static String XML_NODE_PROFILE= "profile"; //$NON-NLS-1$
  	private final static String XML_NODE_SETTING= "setting"; //$NON-NLS-1$
  	
  	private final static String XML_ATTRIBUTE_VERSION= "version"; //$NON-NLS-1$
  	private final static String XML_ATTRIBUTE_ID= "id"; //$NON-NLS-1$
  	private final static String XML_ATTRIBUTE_NAME= "name"; //$NON-NLS-1$
  	private final static String XML_ATTRIBUTE_PROFILE_KIND= "kind"; //$NON-NLS-1$
  	private final static String XML_ATTRIBUTE_VALUE= "value"; //$NON-NLS-1$
  	
  	private final IProfileVersioner fProfileVersioner;
  	private final String fProfilesKey;
  	private final String fProfilesVersionKey;
  		
  	
  	public ProfileStore(String profilesKey, IProfileVersioner profileVersioner) {
  		fProfilesKey= profilesKey;
  		fProfileVersioner= profileVersioner;
  		fProfilesVersionKey= profilesKey + VERSION_KEY_SUFFIX;
  	}
  	
  	/**
  	 * @return Returns the collection of profiles currently stored in the preference store or
  	 * <code>null</code> if the loading failed. The elements are of type {@link ProfileManager.CustomProfile}
  	 * and are all updated to the latest version.
  	 * @throws CoreException
  	 */
  	public List readProfiles(IScopeContext scope) throws CoreException {
  		return readProfilesFromString(scope.getNode(JavaScriptUI.ID_PLUGIN).get(fProfilesKey, null));
  	}
  	
  	public void writeProfiles(Collection profiles, IScopeContext instanceScope) throws CoreException {
  		ByteArrayOutputStream stream= new ByteArrayOutputStream(2000);
  		try {
  			writeProfilesToStream(profiles, stream, ENCODING, fProfileVersioner);
  			String val;
  			try {
  				val= stream.toString(ENCODING);
  			} catch (UnsupportedEncodingException e) {
  				val= stream.toString(); 
  			}
  			IEclipsePreferences uiPreferences = instanceScope.getNode(JavaScriptUI.ID_PLUGIN);
  			uiPreferences.put(fProfilesKey, val);
  			uiPreferences.putInt(fProfilesVersionKey, fProfileVersioner.getCurrentVersion());
  		} finally {
  			try { stream.close(); } catch (IOException e) { /* ignore */ }
  		}
  	}
  	
  	public List readProfilesFromString(String profiles) throws CoreException {
  	    if (profiles != null && profiles.length() > 0) {
  			byte[] bytes;
  			try {
  				bytes= profiles.getBytes(ENCODING);
  			} catch (UnsupportedEncodingException e) {
  				bytes= profiles.getBytes();
  			}
  			InputStream is= new ByteArrayInputStream(bytes);
  			try {
  				List res= readProfilesFromStream(new InputSource(is));
  				if (res != null) {
  					for (int i= 0; i < res.size(); i++) {
  						fProfileVersioner.update((CustomProfile) res.get(i));
  					}
  				}
  				return res;
  			} finally {
  				try { is.close(); } catch (IOException e) { /* ignore */ }
  			}
  		}
  		return null;
      }	
  	
  	
  	/**
  	 * Read the available profiles from the internal XML file and return them
  	 * as collection or <code>null</code> if the file is not a profile file.
  	 * @param file The file to read from
  	 * @return returns a list of <code>CustomProfile</code> or <code>null</code>
  	 * @throws CoreException
  	 */
  	public List readProfilesFromFile(File file) throws CoreException {
  		try {
  			final FileInputStream reader= new FileInputStream(file); 
  			try {
  				return readProfilesFromStream(new InputSource(reader));
  			} finally {
  				try { reader.close(); } catch (IOException e) { /* ignore */ }
  			}
  		} catch (IOException e) {
  			throw createException(e, FormatterMessages.CodingStyleConfigurationBlock_error_reading_xml_message);  
  		}
  	}
  	
  	/**
  	 * Load profiles from a XML stream and add them to a map or <code>null</code> if the source is not a profile store.
  	 * @param inputSource The input stream
  	 * @return returns a list of <code>CustomProfile</code> or <code>null</code>
  	 * @throws CoreException
  	 */
  	public static List readProfilesFromStream(InputSource inputSource) throws CoreException {
  		
  		final ProfileDefaultHandler handler= new ProfileDefaultHandler();
  		try {
  		    final SAXParserFactory factory= SAXParserFactory.newInstance();
  			final SAXParser parser= factory.newSAXParser();
  			parser.parse(inputSource, handler);
  		} catch (SAXException e) {
  			throw createException(e, FormatterMessages.CodingStyleConfigurationBlock_error_reading_xml_message);  
  		} catch (IOException e) {
  			throw createException(e, FormatterMessages.CodingStyleConfigurationBlock_error_reading_xml_message);  
  		} catch (ParserConfigurationException e) {
  			throw createException(e, FormatterMessages.CodingStyleConfigurationBlock_error_reading_xml_message);  
  		}
  		return handler.getProfiles();
  	}
  	
  	/**
  	 * Write the available profiles to the internal XML file.
  	 * @param profiles List of <code>CustomProfile</code>
  	 * @param file File to write
  	 * @param encoding the encoding to use
  	 * @throws CoreException
  	 */
  	public void writeProfilesToFile(Collection profiles, File file, String encoding) throws CoreException {
  		final OutputStream stream;
  		try {
  			stream= new FileOutputStream(file);
  			try {
  				writeProfilesToStream(profiles, stream, encoding, fProfileVersioner);
  			} finally {
  				try { stream.close(); } catch (IOException e) { /* ignore */ }
  			}
  		} catch (IOException e) {
  			throw createException(e, FormatterMessages.CodingStyleConfigurationBlock_error_serializing_xml_message);  
  		}
  	}
  	
  	/**
  	 * Save profiles to an XML stream
  	 * @param profiles the list of <code>CustomProfile</code>
  	 * @param stream the stream to write to
  	 * @param encoding the encoding to use
  	 * @throws CoreException
  	 */
  	public static void writeProfilesToStream(Collection profiles, OutputStream stream, String encoding, IProfileVersioner profileVersioner) throws CoreException {
  
  		try {
  			final DocumentBuilderFactory factory= DocumentBuilderFactory.newInstance();
  			final DocumentBuilder builder= factory.newDocumentBuilder();		
  			final Document document= builder.newDocument();
  			
  			final Element rootElement = document.createElement(XML_NODE_ROOT);
  			rootElement.setAttribute(XML_ATTRIBUTE_VERSION, Integer.toString(profileVersioner.getCurrentVersion()));
  
  			document.appendChild(rootElement);
  			
  			for(final Iterator iter= profiles.iterator(); iter.hasNext();) {
  				final Profile profile= (Profile)iter.next();
  				if (profile.isProfileToSave()) {
  					final Element profileElement= createProfileElement(profile, document, profileVersioner);
  					rootElement.appendChild(profileElement);
  				}
  			}
  
  			Transformer transformer=TransformerFactory.newInstance().newTransformer();
  			transformer.setOutputProperty(OutputKeys.METHOD, "xml"); //$NON-NLS-1$
  			transformer.setOutputProperty(OutputKeys.ENCODING, encoding);
  			transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$
  			transformer.transform(new DOMSource(document), new StreamResult(stream));
  		} catch (TransformerException e) {
  			throw createException(e, FormatterMessages.CodingStyleConfigurationBlock_error_serializing_xml_message);  
  		} catch (ParserConfigurationException e) {
  			throw createException(e, FormatterMessages.CodingStyleConfigurationBlock_error_serializing_xml_message);  
  		}
  	}
  
  	
  	/*
  	 * Create a new profile element in the specified document. The profile is not added
  	 * to the document by this method. 
  	 */
  	private static Element createProfileElement(Profile profile, Document document, IProfileVersioner profileVersioner) {
  		final Element element= document.createElement(XML_NODE_PROFILE);
  		element.setAttribute(XML_ATTRIBUTE_NAME, profile.getName());
  		element.setAttribute(XML_ATTRIBUTE_VERSION, Integer.toString(profile.getVersion()));
  		element.setAttribute(XML_ATTRIBUTE_PROFILE_KIND, profileVersioner.getProfileKind());
  		
  		final Iterator keyIter= profile.getSettings().keySet().iterator();
  		
  		while (keyIter.hasNext()) {
  			final String key= (String)keyIter.next();
  			final String value= (String)profile.getSettings().get(key);
  			if (value != null) {
  				final Element setting= document.createElement(XML_NODE_SETTING);
  				setting.setAttribute(XML_ATTRIBUTE_ID, key);
  				setting.setAttribute(XML_ATTRIBUTE_VALUE, value);
  				element.appendChild(setting);
  			} else {
  				JavaScriptPlugin.logErrorMessage("ProfileStore: Profile does not contain value for key " + key); //$NON-NLS-1$
  			}
  		}
  		return element;
  	}
  	
  		
  	/*
  	 * Creates a UI exception for logging purposes
  	 */
  	private static JavaUIException createException(Throwable t, String message) {
  		return new JavaUIException(JavaUIStatus.createError(IStatus.ERROR, message, t));
  	}
  }
  