001 /*
002 * Created on Mar 12, 2010
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
005 * the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
010 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
011 * specific language governing permissions and limitations under the License.
012 *
013 * Copyright @2010 the original author or authors.
014 */
015 package org.fest.swing.keystroke;
016
017 import static java.lang.Thread.currentThread;
018 import static org.fest.reflect.core.Reflection.staticField;
019 import static org.fest.swing.keystroke.KeyStrokeMapping.mapping;
020 import static org.fest.swing.keystroke.KeyStrokeMappingProvider.NO_MASK;
021 import static org.fest.util.Closeables.close;
022 import static org.fest.util.Strings.*;
023
024 import java.awt.event.InputEvent;
025 import java.awt.event.KeyEvent;
026 import java.io.*;
027 import java.util.*;
028
029 import org.fest.reflect.exception.ReflectionError;
030 import org.fest.swing.exception.ParsingException;
031 import org.fest.util.VisibleForTesting;
032
033 /**
034 * Understands creation of <code>{@link KeyStrokeMapping}</code>s by parsing a text file.
035 * <p>
036 * Mappings for the following characters:
037 * <ul>
038 * <li>Backspace</li>
039 * <li>Delete</li>
040 * <li>Enter</li>
041 * <li>Escape</li>
042 * <li>Tab</li>
043 * </ul>
044 * will be automatically added and should <strong>not</strong> be included to the file to parse.
045 * </p>
046 * <p>
047 * The following is an example of a mapping file:
048 *
049 * <pre>
050 * a, A, NO_MASK
051 * A, A, SHIFT_MASK
052 * COMMA, COMMA, NO_MASK
053 * </pre>
054 *
055 * Each line represents a character-keystroke mapping where each value is separated by a comma.
056 * <p>
057 * The first value represents the character to map. For example 'a' or 'A'. Since each field is separated by a comma, to
058 * map the ',' character we need to specify the text "COMMA."
059 * </p>
060 * <p>
061 * The second value represents the key code, which should be the name of a key code from <code>{@link KeyEvent}</code>
062 * without the prefix "VK_". For example, if the key code is <code>{@link KeyEvent#VK_COMMA}</code> we just need to
063 * specify "COMMA".
064 * </p>
065 * <p>
066 * The third value represents any modifiers to use, which should be the name of a modifier from
067 * <code>{@link InputEvent}</code>. For example, if the modifier to use is <code>{@link InputEvent#SHIFT_MASK}</code> we
068 * need to specify "SHIFT_MASK". If no modifiers are necessary, we just specify "NO_MASK".
069 * </p>
070 *
071 * @author Olivier DOREMIEUX
072 * @author Alex Ruiz
073 *
074 * @since 1.2
075 */
076 public class KeyStrokeMappingsParser {
077
078 private static final Map<String, Character> SPECIAL_MAPPINGS = new HashMap<String, Character>();
079
080 static {
081 SPECIAL_MAPPINGS.put("COMMA", ',');
082 }
083
084 /**
085 * Creates a <code>{@link KeyStrokeMappingProvider}</code> containing all the character-keystroke mappings specified
086 * in the file with the given name.
087 * <p>
088 * <strong>Note:</strong> This attempts to read the file using
089 * <code>{@link ClassLoader#getResourceAsStream(String)}</code>.
090 * </p>
091 * @param file the name of the file to parse.
092 * @return the created {@code KeyStrokeMappingProvider}.
093 * @throws NullPointerException if the given name is <code>null</code>.
094 * @throws IllegalArgumentException if the given name is empty.
095 * @throws ParsingException if any error occurs during parsing.
096 * @see #parse(File)
097 */
098 public KeyStrokeMappingProvider parse(String file) {
099 validate(file);
100 try {
101 return parse(fileAsStream(file));
102 } catch (IOException e) {
103 throw new ParsingException(concat("An I/O error ocurred while parsing file ", file), e);
104 }
105 }
106
107 private void validate(String file) {
108 if (file == null)
109 throw new NullPointerException("The name of the file to parse should not be null");
110 if (isEmpty(file))
111 throw new IllegalArgumentException("The name of the file to parse should not be an empty string");
112 }
113
114 private InputStream fileAsStream(String file) {
115 InputStream stream = currentThread().getContextClassLoader().getResourceAsStream(file);
116 if (stream == null) throw new ParsingException(concat("Unable to open file ", file));
117 return stream;
118 }
119
120 /**
121 * Creates a <code>{@link KeyStrokeMappingProvider}</code> containing all the character-keystroke mappings specified
122 * in the given file.
123 * @param file the file to parse.
124 * @return the created {@code KeyStrokeMappingProvider}.
125 * @throws NullPointerException if the given file is <code>null</code>.
126 * @throws IllegalArgumentException if the given file does not represent an existing file.
127 * @throws ParsingException if any error occurs during parsing.
128 */
129 public KeyStrokeMappingProvider parse(File file) {
130 validate(file);
131 try {
132 return parse(fileAsStream(file));
133 } catch (IOException e) {
134 throw new ParsingException(concat("An I/O error ocurred while parsing file ", file), e);
135 }
136 }
137
138 private void validate(File file) {
139 if (file == null)
140 throw new NullPointerException("The file to parse should not be null");
141 if (!file.isFile())
142 throw new IllegalArgumentException(concat("The file ", file.getPath(), " is not an existing file"));
143 }
144
145 private InputStream fileAsStream(File file) {
146 try {
147 return new FileInputStream(file);
148 } catch (FileNotFoundException e) {
149 throw new ParsingException(concat("The file ", file.getPath(), " was not found"), e);
150 }
151 }
152
153 private KeyStrokeMappingProvider parse(InputStream input) throws IOException {
154 List<KeyStrokeMapping> mappings = new ArrayList<KeyStrokeMapping>();
155 BufferedReader reader = new BufferedReader(new InputStreamReader(input));
156 try {
157 String line = reader.readLine();
158 while(line != null) {
159 mappings.add(mappingFrom(line));
160 line = reader.readLine();
161 }
162 return new ParsedKeyStrokeMappingProvider(mappings);
163 } finally {
164 close(reader);
165 }
166 }
167
168 @VisibleForTesting
169 KeyStrokeMapping mappingFrom(String line) {
170 String[] parts = split(line);
171 if (parts.length != 3) throw notConformingWithPatternError(line);
172 char character = characterFrom(parts[0].trim());
173 int keyCode = keyCodeFrom(parts[1].trim());
174 int modifiers = modifiersFrom(parts[2].trim());
175 return mapping(character, keyCode, modifiers);
176 }
177
178 private static String[] split(String line) {
179 return line.trim().split(",");
180 }
181
182 private static ParsingException notConformingWithPatternError(String line) {
183 return new ParsingException(concat(
184 "Line ", quote(line), " does not conform with pattern '{char}, {keycode}, {modifiers}'"));
185 }
186
187 private static char characterFrom(String s) {
188 if (SPECIAL_MAPPINGS.containsKey(s)) return SPECIAL_MAPPINGS.get(s);
189 if (s.length() == 1) return s.charAt(0);
190 throw new ParsingException(concat("The text ", quote(s) , " should have a single character"));
191 }
192
193 private static int keyCodeFrom(String s) {
194 try {
195 return staticField(keyCodeNameFrom(s)).ofType(int.class).in(KeyEvent.class).get();
196 } catch (ReflectionError e) {
197 throw new ParsingException(concat("Unable to retrieve key code from text ", quote(s)), e.getCause());
198 }
199 }
200
201 private static String keyCodeNameFrom(String s) {
202 return concat("VK_", s);
203 }
204
205 private static int modifiersFrom(String s) {
206 if ("NO_MASK".equals(s)) return NO_MASK;
207 try {
208 return staticField(s).ofType(int.class).in(InputEvent.class).get();
209 } catch (ReflectionError e) {
210 throw new ParsingException(concat("Unable to retrieve modifiers from text ", quote(s)), e.getCause());
211 }
212 }
213 }