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</code> is the implementation of the
  61  * <code>InputMethodManager</code> class. It is runnable as a separate
  62  * thread in the AWT environment.&nbsp;
  63  * <code>InputMethodManager.getInstance()</code> creates an instance of
  64  * <code>ExecutableInputMethodManager</code> 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 
 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 
 123     synchronized void initialize() {
 124         selectInputMethodMenuTitle = Toolkit.getProperty("AWT.InputMethodSelectionMenu", "Select Input Method");
 125 
 126         triggerMenuString = selectInputMethodMenuTitle;
 127     }
 128 
 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.
 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 
 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 
 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 
 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 
 230     public String getTriggerMenuString() {
 231         return triggerMenuString;
 232     }
 233 
 234     /*
 235      * Returns true if the environment indicates there are multiple input methods
 236      */
 237     boolean hasMultipleInputMethods() {
 238         return ((hostAdapterLocator != null) && (javaInputMethodCount > 0)
 239                 || (javaInputMethodCount > 1));
 240     }
 241 
 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      */
 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.loadInstalled(InputMethodDescriptor.class)) {
 263                             ClassLoader cl = descriptor.getClass().getClassLoader();
 264                             javaInputMethodLocatorList.add(new InputMethodLocator(descriptor, cl, null));
 265                         }
 266                         return null;
 267                     }
 268                 });
 269             }  catch (PrivilegedActionException e) {
 270                 e.printStackTrace();
 271             }
 272             javaInputMethodCount = javaInputMethodLocatorList.size();
 273         }
 274 
 275         if (hasMultipleInputMethods()) {
 276             // initialize preferences
 277             if (userRoot == null) {
 278                 userRoot = getUserRoot();
 279             }
 280         } else {
 281             // indicate to clients not to offer the menu
 282             triggerMenuString = null;
 283         }
 284     }
 285 
 286     private void showInputMethodMenu() {
 287 
 288         if (!hasMultipleInputMethods()) {
 289             requestComponent = null;
 290             return;
 291         }
 292 
 293         // initialize pop-up menu
 294         selectionMenu = InputMethodPopupMenu.getInstance(requestComponent, selectInputMethodMenuTitle);
 295 
 296         // we have to rebuild the menu each time because
 297         // some input methods (such as IIIMP) may change
 298         // their list of supported locales dynamically
 299         selectionMenu.removeAll();
 300 
 301         // get information about the currently selected input method
 302         // ??? if there's no current input context, what's the point
 303         // of showing the menu?
 304         String currentSelection = getCurrentSelection();
 305 
 306         // Add menu item for host adapter
 307         if (hostAdapterLocator != null) {
 308             selectionMenu.addOneInputMethodToMenu(hostAdapterLocator, currentSelection);
 309             selectionMenu.addSeparator();
 310         }
 311 
 312         // Add menu items for other input methods
 313         for (int i = 0; i < javaInputMethodLocatorList.size(); i++) {
 314             InputMethodLocator locator = javaInputMethodLocatorList.get(i);
 315             selectionMenu.addOneInputMethodToMenu(locator, currentSelection);
 316         }
 317 
 318         synchronized (this) {
 319             selectionMenu.addToComponent(requestComponent);
 320             requestInputContext = currentInputContext;
 321             selectionMenu.show(requestComponent, 60, 80); // TODO: get proper x, y...
 322             requestComponent = null;
 323         }
 324     }
 325 
 326     private String getCurrentSelection() {
 327         InputContext inputContext = currentInputContext;
 328         if (inputContext != null) {
 329             InputMethodLocator locator = inputContext.getInputMethodLocator();
 330             if (locator != null) {
 331                 return locator.getActionCommandString();
 332             }
 333         }
 334         return null;
 335     }
 336 
 337     synchronized void changeInputMethod(String choice) {
 338         InputMethodLocator locator = null;
 339 
 340         String inputMethodName = choice;
 341         String localeString = null;
 342         int index = choice.indexOf('\n');
 343         if (index != -1) {
 344             localeString = choice.substring(index + 1);
 345             inputMethodName = choice.substring(0, index);
 346         }
 347         if (hostAdapterLocator.getActionCommandString().equals(inputMethodName)) {
 348             locator = hostAdapterLocator;
 349         } else {
 350             for (int i = 0; i < javaInputMethodLocatorList.size(); i++) {
 351                 InputMethodLocator candidate = javaInputMethodLocatorList.get(i);
 352                 String name = candidate.getActionCommandString();
 353                 if (name.equals(inputMethodName)) {
 354                     locator = candidate;
 355                     break;
 356                 }
 357             }
 358         }
 359 
 360         if (locator != null && localeString != null) {
 361             String language = "", country = "", variant = "";
 362             int postIndex = localeString.indexOf('_');
 363             if (postIndex == -1) {
 364                 language = localeString;
 365             } else {
 366                 language = localeString.substring(0, postIndex);
 367                 int preIndex = postIndex + 1;
 368                 postIndex = localeString.indexOf('_', preIndex);
 369                 if (postIndex == -1) {
 370                     country = localeString.substring(preIndex);
 371                 } else {
 372                     country = localeString.substring(preIndex, postIndex);
 373                     variant = localeString.substring(postIndex + 1);
 374                 }
 375             }
 376             Locale locale = new Locale(language, country, variant);
 377             locator = locator.deriveLocator(locale);
 378         }
 379 
 380         if (locator == null)
 381             return;
 382 
 383         // tell the input context about the change
 384         if (requestInputContext != null) {
 385             requestInputContext.changeInputMethod(locator);
 386             requestInputContext = null;
 387 
 388             // remember the selection
 389             putPreferredInputMethod(locator);
 390         }
 391     }
 392 
 393     InputMethodLocator findInputMethod(Locale locale) {
 394         // look for preferred input method first
 395         InputMethodLocator locator = getPreferredInputMethod(locale);
 396         if (locator != null) {
 397             return locator;
 398         }
 399 
 400         if (hostAdapterLocator != null && hostAdapterLocator.isLocaleAvailable(locale)) {
 401             return hostAdapterLocator.deriveLocator(locale);
 402         }
 403 
 404         // Update the locator list
 405         initializeInputMethodLocatorList();
 406 
 407         for (int i = 0; i < javaInputMethodLocatorList.size(); i++) {
 408             InputMethodLocator candidate = javaInputMethodLocatorList.get(i);
 409             if (candidate.isLocaleAvailable(locale)) {
 410                 return candidate.deriveLocator(locale);
 411             }
 412         }
 413         return null;
 414     }
 415 
 416     Locale getDefaultKeyboardLocale() {
 417         Toolkit toolkit = Toolkit.getDefaultToolkit();
 418         if (toolkit instanceof InputMethodSupport) {
 419             return ((InputMethodSupport)toolkit).getDefaultKeyboardLocale();
 420         } else {
 421             return Locale.getDefault();
 422         }
 423     }
 424 
 425     /**
 426      * Returns a InputMethodLocator object that the
 427      * user prefers for the given locale.
 428      *
 429      * @param locale Locale for which the user prefers the input method.
 430      */
 431     private synchronized InputMethodLocator getPreferredInputMethod(Locale locale) {
 432         InputMethodLocator preferredLocator = null;
 433 
 434         if (!hasMultipleInputMethods()) {
 435             // No need to look for a preferred Java input method
 436             return null;
 437         }
 438 
 439         // look for the cached preference first.
 440         preferredLocator = preferredLocatorCache.get(locale.toString().intern());
 441         if (preferredLocator != null) {
 442             return preferredLocator;
 443         }
 444 
 445         // look for the preference in the user preference tree
 446         String nodePath = findPreferredInputMethodNode(locale);
 447         String descriptorName = readPreferredInputMethod(nodePath);
 448         Locale advertised;
 449 
 450         // get the locator object
 451         if (descriptorName != null) {
 452             // check for the host adapter first
 453             if (hostAdapterLocator != null &&
 454                 hostAdapterLocator.getDescriptor().getClass().getName().equals(descriptorName)) {
 455                 advertised = getAdvertisedLocale(hostAdapterLocator, locale);
 456                 if (advertised != null) {
 457                     preferredLocator = hostAdapterLocator.deriveLocator(advertised);
 458                     preferredLocatorCache.put(locale.toString().intern(), preferredLocator);
 459                 }
 460                 return preferredLocator;
 461             }
 462             // look for Java input methods
 463             for (int i = 0; i < javaInputMethodLocatorList.size(); i++) {
 464                 InputMethodLocator locator = javaInputMethodLocatorList.get(i);
 465                 InputMethodDescriptor descriptor = locator.getDescriptor();
 466                 if (descriptor.getClass().getName().equals(descriptorName)) {
 467                     advertised = getAdvertisedLocale(locator, locale);
 468                     if (advertised != null) {
 469                         preferredLocator = locator.deriveLocator(advertised);
 470                         preferredLocatorCache.put(locale.toString().intern(), preferredLocator);
 471                     }
 472                     return preferredLocator;
 473                 }
 474             }
 475 
 476             // maybe preferred input method information is bogus.
 477             writePreferredInputMethod(nodePath, null);
 478         }
 479 
 480         return null;
 481     }
 482 
 483     private String findPreferredInputMethodNode(Locale locale) {
 484         if (userRoot == null) {
 485             return null;
 486         }
 487 
 488         // create locale node relative path
 489         String nodePath = preferredIMNode + "/" + createLocalePath(locale);
 490 
 491         // look for the descriptor
 492         while (!nodePath.equals(preferredIMNode)) {
 493             try {
 494                 if (userRoot.nodeExists(nodePath)) {
 495                     if (readPreferredInputMethod(nodePath) != null) {
 496                         return nodePath;
 497                     }
 498                 }
 499             } catch (BackingStoreException bse) {
 500             }
 501 
 502             // search at parent's node
 503             nodePath = nodePath.substring(0, nodePath.lastIndexOf('/'));
 504         }
 505 
 506         return null;
 507     }
 508 
 509     private String readPreferredInputMethod(String nodePath) {
 510         if ((userRoot == null) || (nodePath == null)) {
 511             return null;
 512         }
 513 
 514         return userRoot.node(nodePath).get(descriptorKey, null);
 515     }
 516 
 517     /**
 518      * Writes the preferred input method descriptor class name into
 519      * the user's Preferences tree in accordance with the given locale.
 520      *
 521      * @param inputMethodLocator input method locator to remember.
 522      */
 523     private synchronized void putPreferredInputMethod(InputMethodLocator locator) {
 524         InputMethodDescriptor descriptor = locator.getDescriptor();
 525         Locale preferredLocale = locator.getLocale();
 526 
 527         if (preferredLocale == null) {
 528             // check available locales of the input method
 529             try {
 530                 Locale[] availableLocales = descriptor.getAvailableLocales();
 531                 if (availableLocales.length == 1) {
 532                     preferredLocale = availableLocales[0];
 533                 } else {
 534                     // there is no way to know which locale is the preferred one, so do nothing.
 535                     return;
 536                 }
 537             } catch (AWTException ae) {
 538                 // do nothing here, either.
 539                 return;
 540             }
 541         }
 542 
 543         // for regions that have only one language, we need to regard
 544         // "xx_YY" as "xx" when putting the preference into tree
 545         if (preferredLocale.equals(Locale.JAPAN)) {
 546             preferredLocale = Locale.JAPANESE;
 547         }
 548         if (preferredLocale.equals(Locale.KOREA)) {
 549             preferredLocale = Locale.KOREAN;
 550         }
 551         if (preferredLocale.equals(new Locale("th", "TH"))) {
 552             preferredLocale = new Locale("th");
 553         }
 554 
 555         // obtain node
 556         String path = preferredIMNode + "/" + createLocalePath(preferredLocale);
 557 
 558         // write in the preference tree
 559         writePreferredInputMethod(path, descriptor.getClass().getName());
 560         preferredLocatorCache.put(preferredLocale.toString().intern(),
 561             locator.deriveLocator(preferredLocale));
 562 
 563         return;
 564     }
 565 
 566     private String createLocalePath(Locale locale) {
 567         String language = locale.getLanguage();
 568         String country = locale.getCountry();
 569         String variant = locale.getVariant();
 570         String localePath = null;
 571         if (!variant.equals("")) {
 572             localePath = "_" + language + "/_" + country + "/_" + variant;
 573         } else if (!country.equals("")) {
 574             localePath = "_" + language + "/_" + country;
 575         } else {
 576             localePath = "_" + language;
 577         }
 578 
 579         return localePath;
 580     }
 581 
 582     private void writePreferredInputMethod(String path, String descriptorName) {
 583         if (userRoot != null) {
 584             Preferences node = userRoot.node(path);
 585 
 586             // record it
 587             if (descriptorName != null) {
 588                 node.put(descriptorKey, descriptorName);
 589             } else {
 590                 node.remove(descriptorKey);
 591             }
 592         }
 593     }
 594 
 595     private Preferences getUserRoot() {
 596         return AccessController.doPrivileged(new PrivilegedAction<Preferences>() {
 597             public Preferences run() {
 598                 return Preferences.userRoot();
 599             }
 600         });
 601     }
 602 
 603     private Locale getAdvertisedLocale(InputMethodLocator locator, Locale locale) {
 604         Locale advertised = null;
 605 
 606         if (locator.isLocaleAvailable(locale)) {
 607             advertised = locale;
 608         } else if (locale.getLanguage().equals("ja")) {
 609             // for Japanese, Korean, and Thai, check whether the input method supports
 610             // language or language_COUNTRY.
 611             if (locator.isLocaleAvailable(Locale.JAPAN)) {
 612                 advertised = Locale.JAPAN;
 613             } else if (locator.isLocaleAvailable(Locale.JAPANESE)) {
 614                 advertised = Locale.JAPANESE;
 615             }
 616         } else if (locale.getLanguage().equals("ko")) {
 617             if (locator.isLocaleAvailable(Locale.KOREA)) {
 618                 advertised = Locale.KOREA;
 619             } else if (locator.isLocaleAvailable(Locale.KOREAN)) {
 620                 advertised = Locale.KOREAN;
 621             }
 622         } else if (locale.getLanguage().equals("th")) {
 623             if (locator.isLocaleAvailable(new Locale("th", "TH"))) {
 624                 advertised = new Locale("th", "TH");
 625             } else if (locator.isLocaleAvailable(new Locale("th"))) {
 626                 advertised = new Locale("th");
 627             }
 628         }
 629 
 630         return advertised;
 631     }
 632 }