aboutsummaryrefslogtreecommitdiffstats
path: root/src/net/java/sip/communicator/service/keybindings/Persistence.java
blob: 51c72453205de99803eaaadc776f42023afe389f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Copyright @ 2015 Atlassian Pty Ltd
 *
 * Licensed 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 net.java.sip.communicator.service.keybindings;

import java.io.*;
import java.text.*;
import java.util.*;

import javax.swing.*;

/**
 * Convenience methods providing a quick means of loading and saving
 * keybindings. None preserve disabled mappings. The formats provided are as
 * follows:<br>
 * SERIAL_HASH- Serialized hash map of bindings. Ordering is preserved if
 * available.<br>
 * SERIAL_INPUT- Serialized input map of bindings.<br>
 * PROPERTIES_PAIR- Persistence provided by java.util.Properties using plain
 * text key/value pairs.<br>
 * PROPERTIES_XML- Persistence provided by java.util.Properties using its XML
 * format.
 *
 * @author Damian Johnson (atagar1@gmail.com)
 * @version September 21, 2007
 */
public enum Persistence
{
    SERIAL_HASH, SERIAL_INPUT, PROPERTIES_PAIRS, PROPERTIES_XML;

    private static final String PROPERTIES_COMMENT =
        "Keybindings (mapping of KeyStrokes to string representations of actions)";

    /**
     * Returns the enum representation of a string. This is case sensitive.
     *
     * @param str toString representation of this enum
     * @return enum associated with a string
     * @throws IllegalArgumentException if argument is not represented by this
     *             enum.
     */
    public static Persistence fromString(String str)
    {
        for (Persistence type : Persistence.values())
        {
            if (str.equals(type.toString()))
                return type;
        }
        throw new IllegalArgumentException();
    }

    /**
     * Attempts to load this type of persistent keystroke map from a given path.
     * This is unable to parse any null content.
     *
     * @param path absolute path to resource to be loaded
     * @return keybinding map reflecting file contents
     * @throws IOException if unable to load resource
     * @throws ParseException if unable to parse content
     */
    public LinkedHashMap<KeyStroke, String> load(String path)
        throws IOException,
        ParseException
    {
        return load(new FileInputStream(path));
    }

    /**
     * Attempts to load this type of persistent keystroke map from a given
     * stream. This is unable to parse any null content.
     *
     * @param input source of keybindings to be parsed
     * @return keybinding map reflecting file contents
     * @throws IOException if unable to load resource
     * @throws ParseException if unable to parse content
     */
    public LinkedHashMap<KeyStroke, String> load(InputStream input)
        throws IOException,
        ParseException
    {
        LinkedHashMap<KeyStroke, String> output =
            new LinkedHashMap<KeyStroke, String>();

        if (this == SERIAL_HASH || this == SERIAL_INPUT)
        {
            Object instance = null; // Loaded serialized object

            try
            {
                ObjectInputStream objectInput = new ObjectInputStream(input);
                instance = objectInput.readObject();
                objectInput.close();
            }
            catch (ClassNotFoundException exc)
            {
                throw new ParseException("Unable to load serialized content", 0);
            }

            if (this == SERIAL_HASH)
            {
                if (!(instance instanceof HashMap<?, ?>))
                {
                    throw new ParseException(
                        "Serialized resource doesn't represent a HashMap", 0);
                }

                HashMap<?, ?> mapping = (HashMap<?, ?>)instance;
                for (Object key : mapping.keySet())
                {
                    Object value = mapping.get(key);

                    if (key instanceof KeyStroke && value instanceof String)
                    {
                        output.put((KeyStroke) key, (String) value);
                    }
                    else
                    {
                        if (key == null || value == null)
                        {
                            throw new ParseException(
                                "Unable to load null content", 0);
                        }
                        else
                        {
                            StringBuilder message = new StringBuilder();
                            message
                                .append("Entry doesn't represent a keybinding: ");
                            message.append(key.getClass().getName());
                            message.append(" -> ");
                            message.append(value.getClass().getName());
                            message
                                .append("\nMust match KeyStroke -> String mapping");
                            throw new ParseException(message.toString(), 0);
                        }
                    }
                }
            }
            else
            {
                if (!(instance instanceof InputMap))
                {
                    throw new ParseException(
                        "Serialized resource doesn't represent an InputMap", 0);
                }

                InputMap mapping = (InputMap) instance;
                if (mapping.keys() != null)
                {
                    for (KeyStroke shortcut : mapping.keys())
                    {
                        if (shortcut == null || mapping.get(shortcut) == null)
                        {
                            throw new ParseException(
                                "Unable to load null content", 0);
                        }
                        else
                        {
                            output.put(shortcut, mapping.get(shortcut)
                                .toString());
                        }
                    }
                }
            }
        }
        else if (this == PROPERTIES_PAIRS || this == PROPERTIES_XML)
        {
            Properties properties = new Properties();
            if (this == PROPERTIES_PAIRS)
                properties.load(input);
            else if (this == PROPERTIES_XML)
                properties.loadFromXML(input);

            for (Object key : properties.keySet())
            {
                Object value = properties.get(key);

                if (key instanceof String && value instanceof String)
                {
                    KeyStroke keystroke = KeyStroke.getKeyStroke((String) key);
                    if (keystroke == null)
                    {
                        StringBuilder message = new StringBuilder();
                        message
                            .append("Unable to parse keystroke, see the getKeyStroke(String) method of ");
                        message.append(KeyStroke.class.getName());
                        message.append(" for proper format");
                        throw new ParseException(message.toString(), 0);
                    }
                    else
                    {
                        output.put(keystroke, (String) value);
                    }
                }
                else
                {
                    if (key == null || value == null)
                    {
                        throw new ParseException("Unable to load null content",
                            0);
                    }
                    else
                    {
                        StringBuilder message = new StringBuilder();
                        message
                            .append("Entry doesn't represent a keybinding: ");
                        message.append(key.getClass().getName());
                        message.append(" -> ");
                        message.append(value.getClass().getName());
                        message
                            .append("\nMust match String -> String mapping where the first string represents a keystroke");
                        throw new ParseException(message.toString(), 0);
                    }
                }
            }
        }

        input.close();
        return output;
    }

    /**
     * Writes the persistent state of the bindings to an output stream.
     *
     * @param output stream where persistent state should be written
     * @param bindings keybindings to be saved
     * @throws IOException if unable to save bindings
     * @throws UnsupportedOperationException if any keys or values of the
     *             binding are null
     */
    public void save(OutputStream output, Map<KeyStroke, String> bindings)
        throws IOException
    {
        for (KeyStroke key : bindings.keySet())
        {
            if (key == null || bindings.get(key) == null)
            {
                throw new UnsupportedOperationException(
                    "Invalid binding: Shortcuts and actions cannot be null");
            }
        }

        if (this == SERIAL_HASH || this == SERIAL_INPUT)
        {
            Object mapping; // Mapping to be serialized
            if (this == SERIAL_HASH)
                mapping = bindings;
            else
            {
                InputMap inputMap = new InputMap();
                for (KeyStroke shortcut : bindings.keySet())
                {
                    inputMap.put(shortcut, bindings.get(shortcut));
                }
                mapping = inputMap;
            }

            ObjectOutputStream objectOutput = new ObjectOutputStream(output);
            objectOutput.writeObject(mapping);
            objectOutput.flush();
            objectOutput.close();
        }
        else if (this == PROPERTIES_PAIRS || this == PROPERTIES_XML)
        {
            Properties properties = new Properties();
            for (KeyStroke shortcut : bindings.keySet())
            {
                properties.setProperty(shortcut.toString(), bindings
                    .get(shortcut));
            }

            if (this == PROPERTIES_PAIRS)
                properties.store(output, PROPERTIES_COMMENT);
            else
                properties.storeToXML(output, PROPERTIES_COMMENT);
        }
    }

    /**
     * Writes the persistent state of the bindings to a file.
     *
     * @param path absolute path to where bindings should be saved
     * @param bindings keybindings to be saved
     * @throws IOException if unable to save bindings
     * @throws UnsupportedOperationException if any keys or values of the
     *             binding are null
     */
    public void save(String path, Map<KeyStroke, String> bindings)
        throws IOException
    {
        FileOutputStream output = new FileOutputStream(path);
        try
        {
            save(output, bindings);
            output.flush();
            output.close();
        }
        catch (IOException exc)
        {
            output.flush();
            output.close();
            throw exc;
        }
        catch (UnsupportedOperationException exc)
        {
            output.flush();
            output.close();
            throw exc;
        }
    }

    /**
     * Provides the textual output of what this persistence format would save
     * given a set of bindings. This silently fails, returning null if unable to
     * generate output from bindings.
     *
     * @param bindings bindings for which to generate saved output
     * @return string reflecting what would be saved by this persistence format
     */
    public String getOutput(Map<KeyStroke, String> bindings)
    {
        /*-
         * This utilizes a rather lengthy chain of redirection to generate output as a string:
         * PipedOutputStream ->
         * PipedInputStream ->
         * Scanner ->
         * StringBuilder ->
         * String
         */

        PipedOutputStream pipeOut = new PipedOutputStream();
        PipedInputStream pipeIn = new PipedInputStream();
        Scanner scanner = new Scanner(pipeIn);

        try
        {
            pipeOut.connect(pipeIn);
            save(pipeOut, bindings);
            pipeOut.flush();
            pipeOut.close();

            StringBuilder builder = new StringBuilder();
            if (scanner.hasNextLine())
                builder.append(scanner.nextLine());
            while (scanner.hasNextLine())
            {
                builder.append("\n");
                builder.append(scanner.nextLine());
            }
            scanner.close();
            pipeIn.close();
            return builder.toString();
        }
        catch (IOException exc)
        {
            return null;
        }
        catch (UnsupportedOperationException exc)
        {
            return null;
        }
    }

    @Override
    public String toString()
    {
        if (this == SERIAL_HASH)
            return "Serialized Hash Map";
        else if (this == SERIAL_INPUT)
            return "Serialized Input Map";
        else if (this == PROPERTIES_XML)
            return "Properties XML";
        else
            return getReadableConstant(this.name());
    }

    /**
     * Provides a more readable version of constant names. Spaces replace
     * underscores and this changes the input to lowercase except the first
     * letter of each word. For instance, "RARE_CARDS" would become
     * "Rare Cards".
     *
     * @param input string to be converted
     * @return reader friendly variant of constant name
     */
    public static String getReadableConstant(String input)
    {
        char[] name = input.toCharArray();

        boolean isStartOfWord = true;
        for (int i = 0; i < name.length; ++i)
        {
            char chr = name[i];
            if (chr == '_')
                name[i] = ' ';
            else if (isStartOfWord)
                name[i] = Character.toUpperCase(chr);
            else
                name[i] = Character.toLowerCase(chr);
            isStartOfWord = chr == '_';
        }

        return new String(name);
    }
}