com.google.dart.tools.ui.internal.preferences.DartKeyBindingPersistence.java Source code

Java tutorial

Introduction

Here is the source code for com.google.dart.tools.ui.internal.preferences.DartKeyBindingPersistence.java

Source

/*
 * 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.preferences;

import com.google.dart.tools.core.utilities.io.FileUtilities;
import com.google.dart.tools.ui.DartToolsPlugin;
import com.google.dart.tools.ui.internal.DartUiException;
import com.google.dart.tools.ui.internal.DartUiStatus;

import org.eclipse.core.commands.Command;
import org.eclipse.core.commands.CommandManager;
import org.eclipse.core.commands.ParameterizedCommand;
import org.eclipse.core.commands.common.NotDefinedException;
import org.eclipse.core.commands.contexts.ContextManager;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.bindings.Binding;
import org.eclipse.jface.bindings.BindingManager;
import org.eclipse.jface.bindings.Scheme;
import org.eclipse.jface.bindings.keys.KeyBinding;
import org.eclipse.jface.bindings.keys.KeySequence;
import org.eclipse.jface.bindings.keys.ParseException;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.ui.activities.IActivityManager;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.keys.IBindingService;
import org.w3c.dom.Comment;
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;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
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.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;

/**
 * Defines persistence operations for Dart Editor key bindings.
 * <p>
 * The key binding file is an XML file with the following schema:
 * 
 * <pre>
 * dartKeyBindings - Root element containing a list of keyBinding
 * keyBinding - Key binding element with attributes:
 *   commandName - The user-readable name of the command (as it appears in the UI)
 *   customKeySequence - The user-customized key sequence (default to dartKeySequence)
 *   dartKeySequence - The standard key sequence defined by Dart Editor
 *   platform - The platform name for platform-specific bindings (optional)
 * </pre>
 * TODO allow empty command name to unbind a key sequence<br>
 * TODO allow empty keys to remove all bindings for a command
 */
public class DartKeyBindingPersistence {

    private class KeyBindingHandler extends DefaultHandler {

        private List<Map<String, String>> bindings;
        private int version;
        private Map<String, String> attribs;

        @Override
        public void endElement(String uri, String localName, String qName) {
            if (qName.equals(XML_NODE_BINDING)) {
                bindings.add(attribs);
                attribs = null;
            }
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes)
                throws SAXException {

            if (qName.equals(XML_NODE_BINDING)) {

                if (version != 1) {
                    // only check version if it has content
                    throw new SAXException();
                }
                attribs = new HashMap<String, String>();
                String commandName = attributes.getValue(XML_ATTRIBUTE_COMMANDID);
                attribs.put(XML_ATTRIBUTE_COMMANDID, commandName);
                String dartkeys = attributes.getValue(XML_ATTRIBUTE_KEYS);
                attribs.put(XML_ATTRIBUTE_KEYS, dartkeys);
                String platform = attributes.getValue(XML_ATTRIBUTE_PLATFORM);
                attribs.put(XML_ATTRIBUTE_PLATFORM, platform);

            } else if (qName.equals(XML_NODE_ROOT)) {

                bindings = new ArrayList<Map<String, String>>();
                try {
                    String vers = attributes.getValue(XML_ATTRIBUTE_VERSION);
                    if (vers != null) {
                        version = Integer.parseInt(vers);
                    }
                } catch (NumberFormatException ex) {
                    throw new SAXException(ex);
                }

            }
        }

        List<Map<String, String>> getBindings() {
            return bindings;
        }
    }

    public static final String CUSTOM_KEY_BINDING_STRING = DartToolsPlugin.PLUGIN_ID + ".keyBindings";

    private static final String SERIALIZATION_PROBLEM = "Problems serializing key bindings to XML."; //$NON-NLS-1$
    private static final String DESERIALIZATION_PROBLEM = "Problems reading key bindings from XML."; //$NON-NLS-1$
    private static final String DART_BINDING_SCHEME = "com.google.dart.tools.dartAcceleratorConfiguration"; //$NON-NLS-1$
    private static final String XML_NODE_ROOT = "dartKeyBindings"; //$NON-NLS-1$
    private static final String XML_NODE_BINDING = "keyBinding"; //$NON-NLS-1$
    private static final String XML_ATTRIBUTE_VERSION = "version"; //$NON-NLS-1$
    // the key sequence names are chosen to be adjacent after lexically sorting attribute names
    private static final String XML_ATTRIBUTE_KEYS = "keySequence"; //$NON-NLS-1$
    // the command name is first in a lexical sort of attribute names
    private static final String XML_ATTRIBUTE_COMMANDID = "commandName"; //$NON-NLS-1$
    private static final String XML_ATTRIBUTE_PLATFORM = "platform"; //$NON-NLS-1$// optional attribute
    private static final String XML_UNKNOWN = ""; //$NON-NLS-1$ // should never be used
    private static final String DESCR_FORMAT = "The format is straightforward, consisting of two attributes plus one that is optional.\n"
            + "The required attributes are the command name, which is the same as it appears in\n"
            + "menus, and the key sequence, which is all uppercase. The optional attribute is the\n"
            + "name of the platform to which the binding applies if it is not universal.";

    private static DartUiException createException(Throwable ex, String message) {
        return new DartUiException(DartUiStatus.createError(IStatus.ERROR, message, ex));
    }

    /**
     * The workbench's activity manager. This activity manager is used to see if certain commands
     * should be filtered from the user interface.
     */
    private IActivityManager activityManager;

    /**
     * The workbench's binding service. This binding service is used to access the current set of
     * bindings, and to persist changes.
     */
    private IBindingService bindingService;

    /**
     * A local copy of the workbench's binding manager. Changes are made locally while processing the
     * new bindings. When everything is complete and error-free the changes are persisted.
     */
    private BindingManager bindingManager;

    /**
     * A map of binding elements that are being written.
     */
    private Map<String, Element> knownBindings;

    private ICommandService commandService;

    public DartKeyBindingPersistence(IActivityManager activityManager, IBindingService bindingService,
            ICommandService commandService) {
        this.activityManager = activityManager;
        this.bindingService = bindingService;
        this.commandService = commandService;
    }

    public Binding findBinding(String commandName, String platform) throws NotDefinedException {
        Binding[] bindings = bindingService.getBindings();
        if (bindings != null) {
            for (Binding binding : bindings) {
                if (binding.getSchemeId().equals(DART_BINDING_SCHEME)) {
                    if ((platform != null && platform.equals(binding.getPlatform()))
                            || binding.getPlatform() == null) {
                        ParameterizedCommand pc = binding.getParameterizedCommand();
                        if (pc != null) {
                            Command cmd = pc.getCommand();
                            if (cmd != null) {
                                if (commandName.equals(pc.getName())) {
                                    return binding;
                                }
                            }
                        }
                    }
                }
            }
        }
        return null;
    }

    /**
     * Read a key binding file in the format created by {@code writeFile()}.
     * 
     * @param file The File of key bindings
     * @throws CoreException if there is a problem reading or parsing the file
     */
    public void readFile(File file, String encoding) throws CoreException {
        Reader reader = null;
        try {
            reader = new FileReader(file);
            readFrom(reader);
        } catch (IOException ex) {
            throw createException(ex, ex.getMessage());
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException ex) {
                    DartToolsPlugin.log(ex);
                }
            }
        }
        try {
            String bindString = FileUtilities.getContents(file, encoding);
            IPreferenceStore prefs = DartToolsPlugin.getDefault().getPreferenceStore();
            prefs.setValue(CUSTOM_KEY_BINDING_STRING, bindString);
        } catch (IOException ex) {
            DartToolsPlugin.log(ex);
        }
    }

    /**
     * Read key bindings in the format created by {@code writeFile()}.
     * 
     * @param reader The Reader used to read the input
     * @throws CoreException if there is a problem reading or parsing the file
     */
    public void readFrom(Reader reader) throws CoreException {
        initBindingManager();
        bindingManager.setBindings(new Binding[0]);

        List<Map<String, String>> newBindings;
        newBindings = readKeyBindingsFromStream(new InputSource(reader));
        for (Map<String, String> map : newBindings) {
            updateKeyBinding(map);
        }

        try {
            bindingService.savePreferences(bindingManager.getActiveScheme(), bindingManager.getBindings());
        } catch (IOException e) {
            throw createException(e, DESERIALIZATION_PROBLEM);
        }
    }

    /**
     * Remove all custom key bindings and restore default bindings.
     */
    public void resetBindings() throws CoreException {
        IPreferenceStore prefs = DartToolsPlugin.getDefault().getPreferenceStore();
        prefs.setValue(CUSTOM_KEY_BINDING_STRING, "");
        bindingService.readRegistryAndPreferences(commandService);
        initBindingManager(); // deletes all USER bindings
        try {
            bindingService.savePreferences(bindingManager.getActiveScheme(), bindingManager.getBindings());
        } catch (IOException e) {
            throw createException(e, DESERIALIZATION_PROBLEM);
        }
    }

    /**
     * Called at start up; restore custom bindings saved in a previous session, if any.
     */
    public void restoreBindingPreferences() {
        IPreferenceStore prefs = DartToolsPlugin.getDefault().getPreferenceStore();
        String prefString = prefs.getString(CUSTOM_KEY_BINDING_STRING);
        if (prefString == null || prefString.isEmpty()) {
            return;
        }
        Reader reader = new StringReader(prefString);
        try {
            readFrom(reader);
        } catch (CoreException ex) {
            DartToolsPlugin.log(ex);
        }
    }

    /**
     * Write the currently-defined key bindings to a file. This file is intended to be edited by users
     * to create custom key bindings. For usability, ID strings are not written. For readability, an
     * XML format is used so that each value has a label indicating its purpose. The current key
     * binding is written twice, once as a reference that is used to identify the binding when the
     * file is read, and another time as a template for the user to edit. The command name is written
     * to help the user identify which command is being modified. A platform identifier is included if
     * it exists; this is used to define platform-specific bindings, which are common on Mac OSX.
     * 
     * @param file The File to write
     * @param encoding The file encoding to use
     * @throws CoreException if there are any problem writing the file
     */
    public void writeFile(File file, String encoding) throws CoreException {
        try {
            OutputStream stream = new FileOutputStream(file);
            try {
                writeKeyBindingsToStream(stream, encoding);
            } finally {
                try {
                    stream.close();
                } catch (IOException ex) {
                    DartToolsPlugin.log(ex);
                }
            }
        } catch (IOException e) {
            throw createException(e, SERIALIZATION_PROBLEM);
        }
    }

    private Element createBindingElement(Binding binding, Document document) {
        // binding is known to have a ParameterizedCommand whose command ID matches a registered Command
        String keys = binding.getTriggerSequence().toString();
        String platform = binding.getPlatform();
        String commandName;
        try {
            commandName = binding.getParameterizedCommand().getName();
        } catch (NotDefinedException ex) {
            return null;
        }
        String id = keys + commandName + (platform == null ? "" : platform);
        if (knownBindings.containsKey(id)) {
            if (binding.getType() == Binding.USER) {
                // A SYSTEM binding has already been created
                return null; // do not add it again
            } else {
                // A USER binding has already been created; update its standard key binding
                Element element = knownBindings.get(id);
                element.setAttribute(XML_ATTRIBUTE_KEYS, binding.getTriggerSequence().toString());
                return null;
            }
        }
        Element element = document.createElement(XML_NODE_BINDING);
        element.setAttribute(XML_ATTRIBUTE_KEYS, keys);
        element.setAttribute(XML_ATTRIBUTE_COMMANDID, commandName);
        if (platform != null) {
            element.setAttribute(XML_ATTRIBUTE_PLATFORM, platform);
        }
        knownBindings.put(id, element);
        return element;
    }

    private void initBindingManager() {
        bindingManager = new BindingManager(new ContextManager(), new CommandManager());
        Scheme[] definedSchemes = bindingService.getDefinedSchemes();
        try {
            for (int i = 0; i < definedSchemes.length; i++) {
                Scheme scheme = definedSchemes[i];
                Scheme copy = bindingManager.getScheme(scheme.getId());
                copy.define(scheme.getName(), scheme.getDescription(), scheme.getParentId());
            }
            bindingManager.setActiveScheme(bindingService.getActiveScheme());
        } catch (NotDefinedException e) {
            throw new Error("Internal error in DartKeyBindingPersistence"); //$NON-NLS-1$
        }
        bindingManager.setLocale(bindingService.getLocale());
        bindingManager.setPlatform(bindingService.getPlatform());

        Binding[] currentBindings = bindingService.getBindings();
        Set<Binding> trimmedBindings = new HashSet<Binding>();
        if (currentBindings != null) {
            for (Binding binding : currentBindings) {
                if (binding.getType() != Binding.USER) {
                    trimmedBindings.add(binding);
                }
            }
        }
        Binding[] trimmedBindingArray = trimmedBindings.toArray(new Binding[trimmedBindings.size()]);
        bindingManager.setBindings(trimmedBindingArray);
    }

    private boolean isActive(Command command) {
        return activityManager.getIdentifier(command.getId()).isEnabled();
    }

    private List<Map<String, String>> readKeyBindingsFromStream(InputSource inputSource) throws CoreException {

        KeyBindingHandler handler = new KeyBindingHandler();
        try {
            SAXParserFactory factory = SAXParserFactory.newInstance();
            SAXParser parser = factory.newSAXParser();
            parser.parse(inputSource, handler);
        } catch (SAXException e) {
            throw createException(e, DESERIALIZATION_PROBLEM);
        } catch (IOException e) {
            throw createException(e, DESERIALIZATION_PROBLEM);
        } catch (ParserConfigurationException e) {
            throw createException(e, DESERIALIZATION_PROBLEM);
        }
        return handler.getBindings();

    }

    private Binding[] sort(Binding[] bindings) {
        Comparator<Binding> comp = new Comparator<Binding>() {
            @Override
            public int compare(Binding b0, Binding b1) {
                ParameterizedCommand c0 = b0.getParameterizedCommand();
                ParameterizedCommand c1 = b1.getParameterizedCommand();
                int k;
                if (c0 == null || c1 == null) {
                    if (c0 != c1) {
                        k = c0 == null ? -1 : 1;
                    } else {
                        k = 0;
                    }
                } else {
                    try {
                        k = c0.getCommand().getName().compareTo(c1.getCommand().getName());
                    } catch (NotDefinedException ex) {
                        k = 0;
                    }
                }
                if (k == 0) {
                    String p0 = b0.getPlatform();
                    if (p0 == null) {
                        p0 = XML_UNKNOWN;
                    }
                    String p1 = b1.getPlatform();
                    if (p1 == null) {
                        p1 = XML_UNKNOWN;
                    }
                    k = p0.compareTo(p1);
                }
                if (k == 0) {
                    k = b0.getTriggerSequence().toString().compareTo(b1.getTriggerSequence().toString());
                }
                return k;
            }
        };
        Arrays.sort(bindings, comp);
        return bindings;
    }

    private void updateKeyBinding(Map<String, String> map) throws CoreException {
        try {
            String platform = map.get(XML_ATTRIBUTE_PLATFORM);
            String commandName = map.get(XML_ATTRIBUTE_COMMANDID);
            String stdKeys = map.get(XML_ATTRIBUTE_KEYS);
            Binding binding = findBinding(commandName, platform);
            if (binding == null) {
                return;
            }
            Command command = binding.getParameterizedCommand().getCommand();
            ParameterizedCommand cmd = new ParameterizedCommand(command, null);
            String schemeId = binding.getSchemeId();
            String contextId = binding.getContextId();
            String locale = binding.getLocale();
            String wm = null;
            int type = Binding.USER;
            KeySequence stdSeq = KeySequence.getInstance(stdKeys);
            Binding newBind = new KeyBinding(stdSeq, cmd, schemeId, contextId, locale, platform, wm, type);
            bindingManager.removeBindings(stdSeq, schemeId, contextId, null, null, null, type);
            bindingManager.addBinding(newBind);
        } catch (NotDefinedException ex) {
            throw createException(ex, ex.getMessage());
        } catch (ParseException ex) {
            throw createException(ex, ex.getMessage());
        }
    }

    private void writeKeyBindingsToStream(OutputStream stream, String encoding) throws CoreException {
        try {
            knownBindings = new HashMap<String, Element>();
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document document = builder.newDocument();
            Element rootElement = document.createElement(XML_NODE_ROOT);
            rootElement.setAttribute(XML_ATTRIBUTE_VERSION, Integer.toString(1));
            document.appendChild(rootElement);
            Comment comment = document.createComment(DESCR_FORMAT);
            document.getElementsByTagName(XML_NODE_ROOT).item(0).appendChild(comment);

            Binding[] bindings = bindingService.getBindings();
            if (bindings != null) {
                bindings = sort(bindings);
                for (Binding binding : bindings) {
                    if (binding.getSchemeId().equals(DART_BINDING_SCHEME)) {
                        ParameterizedCommand pc = binding.getParameterizedCommand();
                        if (pc != null) {
                            Command cmd = pc.getCommand();
                            if (cmd != null && isActive(cmd)) {
                                Element bindingElement = createBindingElement(binding, document);
                                if (bindingElement != null) {
                                    rootElement.appendChild(bindingElement);
                                }
                            }
                        }
                    }
                }
            }

            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, SERIALIZATION_PROBLEM);
        } catch (ParserConfigurationException e) {
            throw createException(e, SERIALIZATION_PROBLEM);
        }
    }

}