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 }