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.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 
 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 
 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 
 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 
 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 
 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      */
 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 
 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 
 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 inputMethodLocator input method locator to remember.
 523      */
 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 
 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 
 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 
 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 }