1 /*
2  * Copyright (c) 1998, 2012, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package sun.awt.im;
27 
28 import java.awt.AWTException;
29 import java.awt.CheckboxMenuItem;
30 import java.awt.Component;
31 import java.awt.Dialog;
32 import java.awt.EventQueue;
33 import java.awt.Frame;
34 import java.awt.PopupMenu;
35 import java.awt.Menu;
36 import java.awt.MenuItem;
37 import java.awt.Toolkit;
38 import sun.awt.AppContext;
39 import java.awt.event.ActionEvent;
40 import java.awt.event.ActionListener;
41 import java.awt.event.InvocationEvent;
42 import java.awt.im.spi.InputMethodDescriptor;
43 import java.lang.reflect.InvocationTargetException;
44 import java.security.AccessController;
45 import java.security.PrivilegedAction;
46 import java.security.PrivilegedActionException;
47 import java.security.PrivilegedExceptionAction;
48 import java.util.Hashtable;
49 import java.util.Iterator;
50 import java.util.Locale;
51 import java.util.ServiceLoader;
52 import java.util.Vector;
53 import java.util.Set;
54 import java.util.prefs.BackingStoreException;
55 import java.util.prefs.Preferences;
56 import sun.awt.InputMethodSupport;
57 import sun.awt.SunToolkit;
58 
59 /**
60  * {@code ExecutableInputMethodManager} is the implementation of the
61  * {@code InputMethodManager} class. It is runnable as a separate
62  * thread in the AWT environment. 
63  * {@code InputMethodManager.getInstance()} creates an instance of
64  * {@code ExecutableInputMethodManager} and executes it as a deamon
65  * thread.
66  *
67  * @see InputMethodManager
68  */
69 class ExecutableInputMethodManager extends InputMethodManager
70                                    implements Runnable
71 {
72     // the input context that's informed about selections from the user interface
73     private InputContext currentInputContext;
74 
75     // Menu item string for the trigger menu.
76     private String triggerMenuString;
77 
78     // popup menu for selecting an input method
79     private InputMethodPopupMenu selectionMenu;
80     private static String selectInputMethodMenuTitle;
81 
82     // locator and name of host adapter
83     private InputMethodLocator hostAdapterLocator;
84 
85     // locators for Java input methods
86     private int javaInputMethodCount;         // number of Java input methods found
87     private Vector<InputMethodLocator> javaInputMethodLocatorList;
88 
89     // component that is requesting input method switch
90     // must be Frame or Dialog
91     private Component requestComponent;
92 
93     // input context that is requesting input method switch
94     private InputContext requestInputContext;
95 
96     // IM preference stuff
97     private static final String preferredIMNode = "/sun/awt/im/preferredInputMethod";
98     private static final String descriptorKey = "descriptor";
99     private Hashtable<String, InputMethodLocator> preferredLocatorCache = new Hashtable<>();
100     private Preferences userRoot;
101 
ExecutableInputMethodManager()102     ExecutableInputMethodManager() {
103 
104         // set up host adapter locator
105         Toolkit toolkit = Toolkit.getDefaultToolkit();
106         try {
107             if (toolkit instanceof InputMethodSupport) {
108                 InputMethodDescriptor hostAdapterDescriptor =
109                     ((InputMethodSupport)toolkit)
110                     .getInputMethodAdapterDescriptor();
111                 if (hostAdapterDescriptor != null) {
112                     hostAdapterLocator = new InputMethodLocator(hostAdapterDescriptor, null, null);
113                 }
114             }
115         } catch (AWTException e) {
116             // if we can't get a descriptor, we'll just have to do without native input methods
117         }
118 
119         javaInputMethodLocatorList = new Vector<InputMethodLocator>();
120         initializeInputMethodLocatorList();
121     }
122 
initialize()123     synchronized void initialize() {
124         selectInputMethodMenuTitle = Toolkit.getProperty("AWT.InputMethodSelectionMenu", "Select Input Method");
125 
126         triggerMenuString = selectInputMethodMenuTitle;
127     }
128 
run()129     public void run() {
130         // If there are no multiple input methods to choose from, wait forever
131         while (!hasMultipleInputMethods()) {
132             try {
133                 synchronized (this) {
134                     wait();
135                 }
136             } catch (InterruptedException e) {
137             }
138         }
139 
140         // Loop for processing input method change requests
141         while (true) {
142             waitForChangeRequest();
143             initializeInputMethodLocatorList();
144             try {
145                 if (requestComponent != null) {
146                     showInputMethodMenuOnRequesterEDT(requestComponent);
147                 } else {
148                     // show the popup menu within the event thread
149                     EventQueue.invokeAndWait(new Runnable() {
150                         public void run() {
151                             showInputMethodMenu();
152                         }
153                     });
154                 }
155             } catch (InterruptedException ie) {
156             } catch (InvocationTargetException ite) {
157                 // should we do anything under these exceptions?
158             }
159         }
160     }
161 
162     // Shows Input Method Menu on the EDT of requester component
163     // to avoid side effects. See 6544309.
showInputMethodMenuOnRequesterEDT(Component requester)164     private void showInputMethodMenuOnRequesterEDT(Component requester)
165         throws InterruptedException, InvocationTargetException {
166 
167         if (requester == null){
168             return;
169         }
170 
171         class AWTInvocationLock {}
172         Object lock = new AWTInvocationLock();
173 
174         InvocationEvent event =
175                 new InvocationEvent(requester,
176                                     new Runnable() {
177                                         public void run() {
178                                             showInputMethodMenu();
179                                         }
180                                     },
181                                     lock,
182                                     true);
183 
184         AppContext requesterAppContext = SunToolkit.targetToAppContext(requester);
185         synchronized (lock) {
186             SunToolkit.postEvent(requesterAppContext, event);
187             while (!event.isDispatched()) {
188                 lock.wait();
189             }
190         }
191 
192         Throwable eventThrowable = event.getThrowable();
193         if (eventThrowable != null) {
194             throw new InvocationTargetException(eventThrowable);
195         }
196     }
197 
setInputContext(InputContext inputContext)198     void setInputContext(InputContext inputContext) {
199         if (currentInputContext != null && inputContext != null) {
200             // don't throw this exception until 4237852 is fixed
201             // throw new IllegalStateException("Can't have two active InputContext at the same time");
202         }
203         currentInputContext = inputContext;
204     }
205 
notifyChangeRequest(Component comp)206     public synchronized void notifyChangeRequest(Component comp) {
207         if (!(comp instanceof Frame || comp instanceof Dialog))
208             return;
209 
210         // if busy with the current request, ignore this request.
211         if (requestComponent != null)
212             return;
213 
214         requestComponent = comp;
215         notify();
216     }
217 
notifyChangeRequestByHotKey(Component comp)218     public synchronized void notifyChangeRequestByHotKey(Component comp) {
219         while (!(comp instanceof Frame || comp instanceof Dialog)) {
220             if (comp == null) {
221                 // no Frame or Dialog found in containment hierarchy.
222                 return;
223             }
224             comp = comp.getParent();
225         }
226 
227         notifyChangeRequest(comp);
228     }
229 
getTriggerMenuString()230     public String getTriggerMenuString() {
231         return triggerMenuString;
232     }
233 
234     /*
235      * Returns true if the environment indicates there are multiple input methods
236      */
hasMultipleInputMethods()237     boolean hasMultipleInputMethods() {
238         return ((hostAdapterLocator != null) && (javaInputMethodCount > 0)
239                 || (javaInputMethodCount > 1));
240     }
241 
waitForChangeRequest()242     private synchronized void waitForChangeRequest() {
243         try {
244             while (requestComponent == null) {
245                 wait();
246             }
247         } catch (InterruptedException e) {
248         }
249     }
250 
251     /*
252      * initializes the input method locator list for all
253      * installed input method descriptors.
254      */
initializeInputMethodLocatorList()255     private void initializeInputMethodLocatorList() {
256         synchronized (javaInputMethodLocatorList) {
257             javaInputMethodLocatorList.clear();
258             try {
259                 AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
260                     public Object run() {
261                         for (InputMethodDescriptor descriptor :
262                             ServiceLoader.load(InputMethodDescriptor.class,
263                                                ClassLoader.getSystemClassLoader())) {
264                             ClassLoader cl = descriptor.getClass().getClassLoader();
265                             javaInputMethodLocatorList.add(new InputMethodLocator(descriptor, cl, null));
266                         }
267                         return null;
268                     }
269                 });
270             }  catch (PrivilegedActionException e) {
271                 e.printStackTrace();
272             }
273             javaInputMethodCount = javaInputMethodLocatorList.size();
274         }
275 
276         if (hasMultipleInputMethods()) {
277             // initialize preferences
278             if (userRoot == null) {
279                 userRoot = getUserRoot();
280             }
281         } else {
282             // indicate to clients not to offer the menu
283             triggerMenuString = null;
284         }
285     }
286 
showInputMethodMenu()287     private void showInputMethodMenu() {
288 
289         if (!hasMultipleInputMethods()) {
290             requestComponent = null;
291             return;
292         }
293 
294         // initialize pop-up menu
295         selectionMenu = InputMethodPopupMenu.getInstance(requestComponent, selectInputMethodMenuTitle);
296 
297         // we have to rebuild the menu each time because
298         // some input methods (such as IIIMP) may change
299         // their list of supported locales dynamically
300         selectionMenu.removeAll();
301 
302         // get information about the currently selected input method
303         // ??? if there's no current input context, what's the point
304         // of showing the menu?
305         String currentSelection = getCurrentSelection();
306 
307         // Add menu item for host adapter
308         if (hostAdapterLocator != null) {
309             selectionMenu.addOneInputMethodToMenu(hostAdapterLocator, currentSelection);
310             selectionMenu.addSeparator();
311         }
312 
313         // Add menu items for other input methods
314         for (int i = 0; i < javaInputMethodLocatorList.size(); i++) {
315             InputMethodLocator locator = javaInputMethodLocatorList.get(i);
316             selectionMenu.addOneInputMethodToMenu(locator, currentSelection);
317         }
318 
319         synchronized (this) {
320             selectionMenu.addToComponent(requestComponent);
321             requestInputContext = currentInputContext;
322             selectionMenu.show(requestComponent, 60, 80); // TODO: get proper x, y...
323             requestComponent = null;
324         }
325     }
326 
getCurrentSelection()327     private String getCurrentSelection() {
328         InputContext inputContext = currentInputContext;
329         if (inputContext != null) {
330             InputMethodLocator locator = inputContext.getInputMethodLocator();
331             if (locator != null) {
332                 return locator.getActionCommandString();
333             }
334         }
335         return null;
336     }
337 
changeInputMethod(String choice)338     synchronized void changeInputMethod(String choice) {
339         InputMethodLocator locator = null;
340 
341         String inputMethodName = choice;
342         String localeString = null;
343         int index = choice.indexOf('\n');
344         if (index != -1) {
345             localeString = choice.substring(index + 1);
346             inputMethodName = choice.substring(0, index);
347         }
348         if (hostAdapterLocator.getActionCommandString().equals(inputMethodName)) {
349             locator = hostAdapterLocator;
350         } else {
351             for (int i = 0; i < javaInputMethodLocatorList.size(); i++) {
352                 InputMethodLocator candidate = javaInputMethodLocatorList.get(i);
353                 String name = candidate.getActionCommandString();
354                 if (name.equals(inputMethodName)) {
355                     locator = candidate;
356                     break;
357                 }
358             }
359         }
360 
361         if (locator != null && localeString != null) {
362             String language = "", country = "", variant = "";
363             int postIndex = localeString.indexOf('_');
364             if (postIndex == -1) {
365                 language = localeString;
366             } else {
367                 language = localeString.substring(0, postIndex);
368                 int preIndex = postIndex + 1;
369                 postIndex = localeString.indexOf('_', preIndex);
370                 if (postIndex == -1) {
371                     country = localeString.substring(preIndex);
372                 } else {
373                     country = localeString.substring(preIndex, postIndex);
374                     variant = localeString.substring(postIndex + 1);
375                 }
376             }
377             Locale locale = new Locale(language, country, variant);
378             locator = locator.deriveLocator(locale);
379         }
380 
381         if (locator == null)
382             return;
383 
384         // tell the input context about the change
385         if (requestInputContext != null) {
386             requestInputContext.changeInputMethod(locator);
387             requestInputContext = null;
388 
389             // remember the selection
390             putPreferredInputMethod(locator);
391         }
392     }
393 
findInputMethod(Locale locale)394     InputMethodLocator findInputMethod(Locale locale) {
395         // look for preferred input method first
396         InputMethodLocator locator = getPreferredInputMethod(locale);
397         if (locator != null) {
398             return locator;
399         }
400 
401         if (hostAdapterLocator != null && hostAdapterLocator.isLocaleAvailable(locale)) {
402             return hostAdapterLocator.deriveLocator(locale);
403         }
404 
405         // Update the locator list
406         initializeInputMethodLocatorList();
407 
408         for (int i = 0; i < javaInputMethodLocatorList.size(); i++) {
409             InputMethodLocator candidate = javaInputMethodLocatorList.get(i);
410             if (candidate.isLocaleAvailable(locale)) {
411                 return candidate.deriveLocator(locale);
412             }
413         }
414         return null;
415     }
416 
getDefaultKeyboardLocale()417     Locale getDefaultKeyboardLocale() {
418         Toolkit toolkit = Toolkit.getDefaultToolkit();
419         if (toolkit instanceof InputMethodSupport) {
420             return ((InputMethodSupport)toolkit).getDefaultKeyboardLocale();
421         } else {
422             return Locale.getDefault();
423         }
424     }
425 
426     /**
427      * Returns a InputMethodLocator object that the
428      * user prefers for the given locale.
429      *
430      * @param locale Locale for which the user prefers the input method.
431      */
getPreferredInputMethod(Locale locale)432     private synchronized InputMethodLocator getPreferredInputMethod(Locale locale) {
433         InputMethodLocator preferredLocator = null;
434 
435         if (!hasMultipleInputMethods()) {
436             // No need to look for a preferred Java input method
437             return null;
438         }
439 
440         // look for the cached preference first.
441         preferredLocator = preferredLocatorCache.get(locale.toString().intern());
442         if (preferredLocator != null) {
443             return preferredLocator;
444         }
445 
446         // look for the preference in the user preference tree
447         String nodePath = findPreferredInputMethodNode(locale);
448         String descriptorName = readPreferredInputMethod(nodePath);
449         Locale advertised;
450 
451         // get the locator object
452         if (descriptorName != null) {
453             // check for the host adapter first
454             if (hostAdapterLocator != null &&
455                 hostAdapterLocator.getDescriptor().getClass().getName().equals(descriptorName)) {
456                 advertised = getAdvertisedLocale(hostAdapterLocator, locale);
457                 if (advertised != null) {
458                     preferredLocator = hostAdapterLocator.deriveLocator(advertised);
459                     preferredLocatorCache.put(locale.toString().intern(), preferredLocator);
460                 }
461                 return preferredLocator;
462             }
463             // look for Java input methods
464             for (int i = 0; i < javaInputMethodLocatorList.size(); i++) {
465                 InputMethodLocator locator = javaInputMethodLocatorList.get(i);
466                 InputMethodDescriptor descriptor = locator.getDescriptor();
467                 if (descriptor.getClass().getName().equals(descriptorName)) {
468                     advertised = getAdvertisedLocale(locator, locale);
469                     if (advertised != null) {
470                         preferredLocator = locator.deriveLocator(advertised);
471                         preferredLocatorCache.put(locale.toString().intern(), preferredLocator);
472                     }
473                     return preferredLocator;
474                 }
475             }
476 
477             // maybe preferred input method information is bogus.
478             writePreferredInputMethod(nodePath, null);
479         }
480 
481         return null;
482     }
483 
findPreferredInputMethodNode(Locale locale)484     private String findPreferredInputMethodNode(Locale locale) {
485         if (userRoot == null) {
486             return null;
487         }
488 
489         // create locale node relative path
490         String nodePath = preferredIMNode + "/" + createLocalePath(locale);
491 
492         // look for the descriptor
493         while (!nodePath.equals(preferredIMNode)) {
494             try {
495                 if (userRoot.nodeExists(nodePath)) {
496                     if (readPreferredInputMethod(nodePath) != null) {
497                         return nodePath;
498                     }
499                 }
500             } catch (BackingStoreException bse) {
501             }
502 
503             // search at parent's node
504             nodePath = nodePath.substring(0, nodePath.lastIndexOf('/'));
505         }
506 
507         return null;
508     }
509 
readPreferredInputMethod(String nodePath)510     private String readPreferredInputMethod(String nodePath) {
511         if ((userRoot == null) || (nodePath == null)) {
512             return null;
513         }
514 
515         return userRoot.node(nodePath).get(descriptorKey, null);
516     }
517 
518     /**
519      * Writes the preferred input method descriptor class name into
520      * the user's Preferences tree in accordance with the given locale.
521      *
522      * @param locator input method locator to remember.
523      */
putPreferredInputMethod(InputMethodLocator locator)524     private synchronized void putPreferredInputMethod(InputMethodLocator locator) {
525         InputMethodDescriptor descriptor = locator.getDescriptor();
526         Locale preferredLocale = locator.getLocale();
527 
528         if (preferredLocale == null) {
529             // check available locales of the input method
530             try {
531                 Locale[] availableLocales = descriptor.getAvailableLocales();
532                 if (availableLocales.length == 1) {
533                     preferredLocale = availableLocales[0];
534                 } else {
535                     // there is no way to know which locale is the preferred one, so do nothing.
536                     return;
537                 }
538             } catch (AWTException ae) {
539                 // do nothing here, either.
540                 return;
541             }
542         }
543 
544         // for regions that have only one language, we need to regard
545         // "xx_YY" as "xx" when putting the preference into tree
546         if (preferredLocale.equals(Locale.JAPAN)) {
547             preferredLocale = Locale.JAPANESE;
548         }
549         if (preferredLocale.equals(Locale.KOREA)) {
550             preferredLocale = Locale.KOREAN;
551         }
552         if (preferredLocale.equals(new Locale("th", "TH"))) {
553             preferredLocale = new Locale("th");
554         }
555 
556         // obtain node
557         String path = preferredIMNode + "/" + createLocalePath(preferredLocale);
558 
559         // write in the preference tree
560         writePreferredInputMethod(path, descriptor.getClass().getName());
561         preferredLocatorCache.put(preferredLocale.toString().intern(),
562             locator.deriveLocator(preferredLocale));
563 
564         return;
565     }
566 
createLocalePath(Locale locale)567     private String createLocalePath(Locale locale) {
568         String language = locale.getLanguage();
569         String country = locale.getCountry();
570         String variant = locale.getVariant();
571         String localePath = null;
572         if (!variant.equals("")) {
573             localePath = "_" + language + "/_" + country + "/_" + variant;
574         } else if (!country.equals("")) {
575             localePath = "_" + language + "/_" + country;
576         } else {
577             localePath = "_" + language;
578         }
579 
580         return localePath;
581     }
582 
writePreferredInputMethod(String path, String descriptorName)583     private void writePreferredInputMethod(String path, String descriptorName) {
584         if (userRoot != null) {
585             Preferences node = userRoot.node(path);
586 
587             // record it
588             if (descriptorName != null) {
589                 node.put(descriptorKey, descriptorName);
590             } else {
591                 node.remove(descriptorKey);
592             }
593         }
594     }
595 
getUserRoot()596     private Preferences getUserRoot() {
597         return AccessController.doPrivileged(new PrivilegedAction<Preferences>() {
598             public Preferences run() {
599                 return Preferences.userRoot();
600             }
601         });
602     }
603 
604     private Locale getAdvertisedLocale(InputMethodLocator locator, Locale locale) {
605         Locale advertised = null;
606 
607         if (locator.isLocaleAvailable(locale)) {
608             advertised = locale;
609         } else if (locale.getLanguage().equals("ja")) {
610             // for Japanese, Korean, and Thai, check whether the input method supports
611             // language or language_COUNTRY.
612             if (locator.isLocaleAvailable(Locale.JAPAN)) {
613                 advertised = Locale.JAPAN;
614             } else if (locator.isLocaleAvailable(Locale.JAPANESE)) {
615                 advertised = Locale.JAPANESE;
616             }
617         } else if (locale.getLanguage().equals("ko")) {
618             if (locator.isLocaleAvailable(Locale.KOREA)) {
619                 advertised = Locale.KOREA;
620             } else if (locator.isLocaleAvailable(Locale.KOREAN)) {
621                 advertised = Locale.KOREAN;
622             }
623         } else if (locale.getLanguage().equals("th")) {
624             if (locator.isLocaleAvailable(new Locale("th", "TH"))) {
625                 advertised = new Locale("th", "TH");
626             } else if (locator.isLocaleAvailable(new Locale("th"))) {
627                 advertised = new Locale("th");
628             }
629         }
630 
631         return advertised;
632     }
633 }
634