blob: 1f8a0dda40779f8909d690b27d162575ba53f345 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* 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 org.netbeans.modules.editor.macros.storage;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import javax.swing.KeyStroke;
import org.netbeans.api.editor.settings.MultiKeyBinding;
import org.netbeans.lib.editor.util.CharacterConversions;
import org.netbeans.modules.editor.macros.MacroDialogSupport;
import org.netbeans.modules.editor.settings.storage.spi.StorageDescription;
import org.netbeans.modules.editor.settings.storage.spi.StorageReader;
import org.netbeans.modules.editor.settings.storage.spi.StorageWriter;
import org.netbeans.modules.editor.settings.storage.spi.support.StorageSupport;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.MIMEResolver;
import org.openide.xml.XMLUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
/**
*
* @author Vita Stejskal
*/
@MIMEResolver.NamespaceRegistration(
displayName="org.netbeans.modules.editor.macros.Bundle#MacrosResolver",
position=530,
doctypePublicId="-//NetBeans//DTD Editor Macros settings 1.1//EN",
mimeType="text/x-nbeditor-macrosettings",
elementName="editor-macros"
)
@org.openide.util.lookup.ServiceProvider(service=org.netbeans.modules.editor.settings.storage.spi.StorageDescription.class)
public final class MacrosStorage implements StorageDescription<String, MacroDescription> {
private static final Logger LOG = Logger.getLogger(MacrosStorage.class.getName());
public static final String ID = "Macros"; //NOI18N
// ---------------------------------------------------------
// StorageDescription implementation
// ---------------------------------------------------------
public MacrosStorage() {
}
public String getId() {
return ID;
}
public boolean isUsingProfiles() {
return false;
}
public String getMimeType() {
return MIME_TYPE;
}
public String getLegacyFileName() {
return "macros.xml"; //NOI18N
}
public StorageReader<String, MacroDescription> createReader(FileObject f, String mimePath) {
if (MIME_TYPE.equals(f.getMIMEType())) {
return new Reader(f, mimePath);
} else {
// assume legacy file
return new LegacyReader(f, mimePath);
}
}
public StorageWriter<String, MacroDescription> createWriter(FileObject f, String mimePath) {
return new Writer();
}
// ---------------------------------------------------------
// Private implementation
// ---------------------------------------------------------
private static final String E_ROOT = "editor-macros"; //NOI18N
private static final String E_MACRO = "macro"; //NOI18N
private static final String E_DESCRIPTION = "description"; //NOI18N
private static final String E_CODE = "code"; //NOI18N
private static final String E_SHORTCUT = "shortcut"; //NOI18N
private static final String A_NAME = "name"; //NOI18N
private static final String A_DESCRIPTION_ID = "descriptionId"; //NOI18N
private static final String A_KEYSTROKES = "keystrokes"; //NOI18N
private static final String A_REMOVE = "remove"; //NOI18N
private static final String A_XML_SPACE = "xml:space"; //NOI18N
private static final String V_PRESERVE = "preserve"; //NOI18N
private static final String PUBLIC_ID = "-//NetBeans//DTD Editor Macros settings 1.1//EN"; //NOI18N
private static final String SYSTEM_ID = "http://www.netbeans.org/dtds/EditorMacros-1_1.dtd"; //NOI18N
private static final String MIME_TYPE = "text/x-nbeditor-macrosettings"; //NOI18N
private static abstract class MacrosReader extends StorageReader<String, MacroDescription> {
protected MacrosReader(FileObject f, String mimePath) {
super(f, mimePath);
}
public abstract Map<String, MacroDescription> getAdded();
public abstract Set<String> getRemoved();
}
private static final class Reader extends MacrosReader {
private Map<String, MacroDescription> macrosMap = new HashMap<String, MacroDescription>();
private Set<String> removedMacros = new HashSet<String>();
// The macro being processed
private String name = null;
private String description = null;
private String code = null;
private List<MultiKeyBinding> shortcuts = null;
private StringBuilder text = null;
private StringBuilder cdataText = null;
private boolean insideCdata = false;
public Reader(FileObject f, String mimePath) {
super(f, mimePath);
}
public Map<String, MacroDescription> getAdded() {
return macrosMap;
}
public Set<String> getRemoved() {
return removedMacros;
}
public @Override void characters(char[] ch, int start, int length) throws SAXException {
if (text != null) {
text.append(ch, start, length);
if (insideCdata) {
cdataText.append(ch, start, length);
}
}
}
public @Override void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if (qName.equals(E_ROOT)) {
// We don't read anything from the root
} else if (qName.equals(E_MACRO)) {
boolean removed = Boolean.valueOf(attributes.getValue(A_REMOVE));
name = null;
description = null;
shortcuts = null;
text = null;
cdataText = null;
if (removed) {
String macroName = attributes.getValue(A_NAME);
removedMacros.add(macroName);
} else {
// Read the name
name = attributes.getValue(A_NAME);
// Read the description and localize it
description = attributes.getValue(A_DESCRIPTION_ID);
if (description != null) {
String localizedDescription = StorageSupport.getLocalizingBundleMessage(
getProcessedFile(), description, null);
if (localizedDescription != null) {
description = localizedDescription;
}
}
shortcuts = new ArrayList<MultiKeyBinding>();
}
} else if (name != null && qName.equals(E_CODE)) {
// Initiate the new builder for the code template parametrized text
text = new StringBuilder();
cdataText = new StringBuilder();
insideCdata = false;
} else if (name != null && qName.equals(E_DESCRIPTION)) {
// Initiate the new builder for the code template description
text = new StringBuilder();
cdataText = new StringBuilder();
insideCdata = false;
} else if (name != null && qName.equals(E_SHORTCUT)) {
String keystrokes = attributes.getValue(A_KEYSTROKES);
if (isModuleFile() && isDefaultProfile() && keystrokes != null && keystrokes.length() > 0) {
// check the key, it should never start with 'A' or 'C', because
// these characters do not work on MAC, Alt should be coded as 'O'
// and Ctrl as 'D'
int idx = keystrokes.indexOf('-'); //NOI18N
if (idx != -1 && (keystrokes.charAt(0) == 'A' || keystrokes.charAt(0) == 'C')) { //NOI18N
LOG.warning("The keybinding '" + keystrokes + //NOI18N
"' in " + getProcessedFile().getPath() + " may not work correctly on Mac. " + //NOI18N
"Keybindings starting with Alt or Ctrl should " + //NOI18N
"be coded with latin capital letters 'O' " + //NOI18N
"or 'D' respectively. For details see org.openide.util.Utilities.stringToKey()."); //NOI18N
}
}
KeyStroke[] arr = StorageSupport.stringToKeyStrokes(keystrokes, true);
if (arr == null) {
LOG.warning("Cannot decode key bindings: " + keystrokes);
} else {
shortcuts.add(new MultiKeyBinding(arr, MacroDialogSupport.RunMacroAction.runMacroAction)); //NOI18N
}
}
}
public @Override void endElement(String uri, String localName, String qName) throws SAXException {
if (qName.equals(E_ROOT)) {
// We don't read anything from the root
} else if (qName.equals(E_MACRO)) {
if (name != null) {
MacroDescription macro = new MacroDescription(
name,
code == null ? "" : CharacterConversions.lineSeparatorToLineFeed(code), //NOI18N
description == null ? null : CharacterConversions.lineSeparatorToLineFeed(description),
Collections.unmodifiableList(shortcuts)
);
macrosMap.put(name, macro);
}
} else if (qName.equals(E_CODE)) {
if (text != null) {
code = cdataText.length() > 0 ? cdataText.toString() : text.toString();
}
} else if (qName.equals(E_DESCRIPTION)) {
if (text != null) {
if (cdataText.length() > 0) {
description = cdataText.toString();
} else if (text.length() > 0) {
description = text.toString();
}
}
}
}
public @Override void startCDATA() throws SAXException {
if (cdataText != null) {
insideCdata = true;
}
}
public @Override void endCDATA() throws SAXException {
if (cdataText != null) {
insideCdata = false;
}
}
} // End of Reader class
private static final class LegacyReader extends MacrosReader {
private static final String EL_ROOT = "macros"; //NOI18N
private static final String EL_MACRO = "macro"; //NOI18N
private static final String AL_NAME = "name"; //NOI18N
private static final String AL_REMOVE = "remove"; //NOI18N
private Map<String, MacroDescription> macrosMap = new HashMap<String, MacroDescription>();
private Set<String> removedMacros = new HashSet<String>();
// The macro being processed
private String name = null;
private StringBuilder text = null;
public LegacyReader(FileObject f, String mimePath) {
super(f, mimePath);
}
public Map<String, MacroDescription> getAdded() {
return macrosMap;
}
public Set<String> getRemoved() {
return removedMacros;
}
public @Override void characters(char[] ch, int start, int length) throws SAXException {
if (text != null) {
text.append(ch, start, length);
}
}
public @Override void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if (qName.equals(EL_ROOT)) {
// We don't read anything from the root
} else if (qName.equals(EL_MACRO)) {
boolean removed = Boolean.valueOf(attributes.getValue(AL_REMOVE));
if (removed) {
String abbrev = attributes.getValue(AL_NAME);
removedMacros.add(abbrev);
name = null;
text = null;
} else {
name = attributes.getValue(AL_NAME);
text = new StringBuilder();
}
}
}
public @Override void endElement(String uri, String localName, String qName) throws SAXException {
if (qName.equals(EL_ROOT)) {
// We don't read anything from the root
} else if (qName.equals(EL_MACRO)) {
if (name != null) {
MacroDescription macro = new MacroDescription(
name,
CharacterConversions.lineSeparatorToLineFeed(text),
null,
Collections.<MultiKeyBinding>emptyList()
);
macrosMap.put(name, macro);
}
}
}
} // End of LegacyReader class
private static final class Writer extends StorageWriter<String, MacroDescription> {
public Writer() {
}
public Document getDocument() {
Document doc = XMLUtil.createDocument(E_ROOT, null, PUBLIC_ID, SYSTEM_ID);
Node root = doc.getElementsByTagName(E_ROOT).item(0);
for(MacroDescription macro : getAdded().values()) {
Element element = doc.createElement(E_MACRO);
root.appendChild(element);
// Store macro's name
element.setAttribute(A_NAME, macro.getName());
// Store macro's shortcuts
List<? extends MultiKeyBinding> shortcuts = macro.getShortcuts();
if (shortcuts != null && shortcuts.size() > 0) {
for(MultiKeyBinding mkb : shortcuts) {
Element shortcutElement = doc.createElement(E_SHORTCUT);
shortcutElement.setAttribute(A_KEYSTROKES, StorageSupport.keyStrokesToString(mkb.getKeyStrokeList(), true));
element.appendChild(shortcutElement);
}
}
// Just some XML crap to preserve whitespace
element.setAttribute(A_XML_SPACE, V_PRESERVE);
// Store macro's code
String code = macro.getCode();
if (code.length() > 0) {
Element codeElement = doc.createElement(E_CODE);
codeElement.appendChild(doc.createCDATASection(
CharacterConversions.lineFeedToLineSeparator(code)));
element.appendChild(codeElement);
}
// Store macro's description
String description = macro.getDescription();
if (description != null && description.length() > 0) {
Element descriptionElement = doc.createElement(E_DESCRIPTION);
descriptionElement.appendChild(doc.createCDATASection(
CharacterConversions.lineFeedToLineSeparator(description)));
element.appendChild(descriptionElement);
}
}
for(String name : getRemoved()) {
Element element = doc.createElement(E_MACRO);
root.appendChild(element);
element.setAttribute(A_NAME, name);
element.setAttribute(A_REMOVE, Boolean.TRUE.toString());
}
return doc;
}
} // End of Writer class
}