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    }