javax.help.HelpUtilities Maven / Gradle / Ivy
Show all versions of javahelp Show documentation
/*
* @(#)HelpUtilities.java 1.56 06/10/30
*
* Copyright (c) 2006 Sun Microsystems, Inc. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Sun designates this
* particular file as subject to the "Classpath" exception as provided
* by Sun in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
* CA 95054 USA or visit www.sun.com if you need additional information or
* have any questions.
*/
package javax.help;
import java.net.URL;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Vector;
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.text.CollationElementIterator;
import java.text.Collator;
import java.text.MessageFormat;
import java.text.RuleBasedCollator;
import java.awt.Component;
import java.awt.IllegalComponentStateException;
/**
* Provides a number of utility functions:
*
* Support for Beans, mapping from a Bean class to its HelpSet and to
* its ID.
* Support for finding localized resources.
* Support for getting the default Query Engine
*
* This class has no public constructor.
*
* @author Eduardo Pelegri-Llopart
* @author Roger D. Brinkley
* @version 1.56 10/30/06
*/
public class HelpUtilities {
//=========
/**
* Beans support
*/
/**
* Given the class for a bean, get its HelpSet.
* Returns the helpSetName property of the BeanDescriptor if defined.
* Otherwise it returns a name as follows:
* If the class is in the unnamed package, it returns beanClassNameHelp.hs.
* Otherwise if it's in the form package.ClassName it returns
* package/Help.hs after replacing "." with "/" in package.
*
* @param beanClass The Class
* @return A String with the name of the HelpSet
*/
public static String getHelpSetNameFromBean(Class beanClass) {
String helpSetName;
try {
BeanInfo bi = Introspector.getBeanInfo(beanClass);
helpSetName =
(String) bi.getBeanDescriptor().getValue("helpSetName");
} catch (Exception ex) {
helpSetName = null;
}
if (helpSetName == null) {
String className = beanClass.getName();
int index = className.lastIndexOf(".");
if (index == -1) {
// unnamed package
helpSetName = className + "Help.hs";
} else {
String packageName = className.substring(0, index);
helpSetName = packageName.replace('.','/') + "/Help.hs";
}
}
return helpSetName;
}
/**
* Given the class for a bean, get its ID string.
* Returns the helpID property of the BeanDescriptor if defined,
* otherwise it returns beanName.topID.
*
* @param beanClass the Class
* @return A String with the ID to use
*/
public static String getIDStringFromBean(Class beanClass) {
String helpID;
try {
BeanInfo bi = Introspector.getBeanInfo(beanClass);
helpID = (String) bi.getBeanDescriptor().getValue("helpID");
} catch (Exception ex) {
helpID = null;
}
if (helpID == null) {
String className = beanClass.getName();
helpID = className + ".topID";
}
return helpID;
}
//=======
/**
* Default for the search engine
*/
public static String getDefaultQueryEngine() {
return "com.sun.java.help.search.DefaultSearchEngine";
}
//========
/**
* I18N/ L10N support
*/
/**
* Search for a helpset file following the standard conventions
* used in ResourceBundle.
*/
/**
* Locate a resource relative to a given classloader CL.
* The name of the resource is composed by using FRONT,
* adding _LANG _COUNTRY _VARIANT (with the usual rules)
* and ending with BACK, which will usually be an extension
* like ".hs" for a HelpSet, or ".class" for a class
*
* This method is a convenience method for getLocalizedResource() with
* a tryRead parameter set to false.
*
* This functionality should likely be exposed as part of JDK1.2
* @param cl The ClassLoader to get the resource from. If cl is null the default
* ClassLoader is used.
* @returns the URL to the localized resource or null if not found.
*/
public static URL getLocalizedResource(ClassLoader cl,
String front,
String back,
Locale locale) {
return getLocalizedResource(cl, front, back, locale, false);
}
/**
* Locate a resource relative to a given classloader CL.
* The name of the resource is composed by using FRONT,
* adding _LANG _COUNTRY _VARIANT (with the usual rules)
* and ending with BACK, which is usually an extension
* like ".hs" for a HelpSet, or ".class" for a class
*
* This version accepts an explicit argument to work around some browser bugs.
*
* This functionality should likely be exposed as part of JDK1.2
* @param cl The ClassLoader to get the resource from. If cl is null the default
* ClassLoader is used.
* @returns the URL to the localized resource or null if not found.
*/
public static URL getLocalizedResource(ClassLoader cl,
String front,
String back,
Locale locale,
boolean tryRead) {
URL url;
for (Enumeration tails = getCandidates(locale);
tails.hasMoreElements(); ) {
String tail = (String) tails.nextElement();
String name = (new StringBuffer(front)).append(tail).append(back).toString();
if (cl == null) {
url = ClassLoader.getSystemResource(name);
} else {
url = cl.getResource(name);
}
if (url != null) {
if (tryRead) {
// Try doing an actual read to be sure it exists
try {
InputStream is = url.openConnection().getInputStream();
if (is != null) {
int i = is.read();
is.close();
if (i != -1) {
return url;
}
}
} catch (Throwable t) {
// ignore and continue looking
}
} else {
return url;
}
}
}
return null;
}
private static Hashtable tailsPerLocales = new Hashtable();
/**
* This returns an enumeration of String tails.
*
* The core functionality on which getLocalizedResource is based.
*
* The suffixes are based on (1) the desired locale and (2) the default locale
* in the following order from lower-level (more specific) to parent-level
* (less specific):
* "_" + language1 + "_" + country1 + "_" + variant1
*
"_" + language1 + "_" + country1
*
"_" + language1
*
""
*
"_" + language2 + "_" + country2 + "_" + variant2
*
"_" + language2 + "_" + country2
*
"_" + language2
*
* The enumeration is of StringBuffer.
* We pay some attention to efficiency in case a method like this is promoted,
* hence we cache per locale.
*/
public static synchronized Enumeration getCandidates(Locale locale) {
Vector tails;
LocalePair pair = new LocalePair(locale, Locale.getDefault());
tails = (Vector) tailsPerLocales.get(pair);
if (tails != null) {
debug("getCandidates - cached copy");
return tails.elements();
}
String lname1 = locale.toString();
StringBuffer name1 = new StringBuffer("_").append(lname1);
if (lname1 == null) {
name1.setLength(0);
}
tails = new Vector();
// Now add them to the vector
while (name1.length() != 0) {
debug(" adding ", name1);
String s = name1.toString();
tails.addElement(s);
int lastUnder = s.lastIndexOf('_');
if (lastUnder != -1) {
name1.setLength(lastUnder);
}
}
debug(" addign -- null -- ");
tails.addElement("");
if (locale != Locale.getDefault()) {
// Otherwise, no need to search twice
String lname2 = Locale.getDefault().toString();
StringBuffer name2 = new StringBuffer("_").append(lname2);
if (lname2 == null) {
name2.setLength(0);
}
while (name2.length() != 0) {
debug(" adding ", name2);
String s = name2.toString();
tails.addElement(s);
int lastUnder = s.lastIndexOf('_');
if (lastUnder != -1) {
name2.setLength(lastUnder);
}
}
}
tailsPerLocales.put(pair, tails);
debug("tails is == ", tails);
return tails.elements();
}
/**
* Auxiliary class used in dealing with locale.
*/
static class LocalePair {
LocalePair(Locale locale1, Locale locale2) {
this.locale1 = locale1;
this.locale2 = locale2;
}
public int hashCode() {
return locale1.hashCode() + locale2.hashCode();
}
public boolean equals(Object obj) {
if (obj == null || ! (obj instanceof LocalePair)) {
return false;
} else {
LocalePair p = (LocalePair) obj;
return locale1.equals(p.locale1) && locale2.equals(p.locale2);
}
}
Locale locale1;
Locale locale2;
}
// ==========
/**
* Get a localized Error String
*/
private static Hashtable bundles;
private static ResourceBundle lastBundle = null;
private static Locale lastLocale = null;
/**
* Gets the locale of a component. If the component is null
* it returns the defaultLocale. If the call to component.getLocale
* returns an IllegalComponentStateException, the defaultLocale is
* returned.
*/
public static Locale getLocale(Component c) {
if (c == null) {
return Locale.getDefault();
}
try {
return c.getLocale();
} catch (IllegalComponentStateException ex) {
return Locale.getDefault();
}
}
static synchronized private ResourceBundle getBundle(Locale l) {
if (lastLocale == l) {
return lastBundle;
}
if (bundles == null) {
bundles = new Hashtable();
}
ResourceBundle back = (ResourceBundle) bundles.get(l);
if (back == null) {
try {
back = ResourceBundle.getBundle("javax.help.resources.Constants",
l);
} catch (MissingResourceException ex) {
throw new Error("Fatal: Resource for javahelp is missing");
}
bundles.put(l, back);
}
lastBundle = back;
lastLocale = l;
return back;
}
/**
* Get the Text message for the default locale.
*
* The getString version does not involve a format.
*/
static public String getString(String key) {
return getString(Locale.getDefault(), key);
}
static public String getText(String key) {
return getText(Locale.getDefault(), key, null, null);
}
/**
* @param s1 The first parameter of a string. A null is valid for s1.
*/
static public String getText(String key, String s1) {
return getText(Locale.getDefault(), key, s1, null);
}
/**
* @param s1 The first parameter of a string. A null is valid for s1.
* @param s2 The first parameter of a string. A null is valid for s2.
*/
static public String getText(String key, String s1, String s2) {
return getText(Locale.getDefault(), key, s1, s2);
}
/**
* @param s1 The first parameter of a string. A null is valid for s1.
* @param s2 The first parameter of a string. A null is valid for s2.
* @param s3 The first parameter of a string. A null is valid for s3.
*/
static public String getText(String key, String s1, String s2, String s3) {
return getText(Locale.getDefault(), key, s1, s2, s3);
}
/**
* Versions with an explicit locale.
*/
static public String getString(Locale l, String key) {
ResourceBundle bundle = getBundle(l);
try {
return bundle.getString(key);
} catch (MissingResourceException ex) {
throw new Error("Fatal: Localization data for JavaHelp is broken. Missing "+key+" key.");
}
}
static public String[] getStringArray(Locale l, String key) {
ResourceBundle bundle = getBundle(l);
try {
return bundle.getStringArray(key);
} catch (MissingResourceException ex) {
throw new Error("Fatal: Localization data for JavaHelp is broken. Missing "+key+" key.");
}
}
/**
* @param s1 The first parameter of a string. A null is valid for s1.
* @param s2 The first parameter of a string. A null is valid for s2.
* @param s3 The first parameter of a string. A null is valid for s3.
*/
static public String getText(Locale l, String key) {
return getText(l, key, null, null, null);
}
/**
* @param s1 The first parameter of a string. A null is valid for s1.
*/
static public String getText(Locale l, String key, String s1) {
return getText(l, key, s1, null, null);
}
/**
* @param s1 The first parameter of a string. A null is valid for s1.
* @param s2 The first parameter of a string. A null is valid for s2.
*/
static public String getText(Locale l, String key, String s1, String s2) {
return getText(l, key, s1, s2, null);
}
/**
* @param s1 The first parameter of a string. A null is valid for s1.
* @param s2 The first parameter of a string. A null is valid for s2.
* @param s3 The first parameter of a string. A null is valid for s3.
*/
static public String getText(Locale l, String key,
String s1, String s2, String s3) {
ResourceBundle bundle = getBundle(l);
// workaround bugs in JDK 1.1.6
// This mimic's what JDK 1.2 will do.
if (s1 == null) {
s1 = "null";
}
if (s2 == null) {
s2 = "null";
}
if (s3 == null) {
s3 = "null";
}
// end workaround
try {
String fmt = bundle.getString(key);
String[] args = {s1, s2, s3};
MessageFormat format = new MessageFormat(fmt);
// workaround a bug in MessageFormat.setLocale
try {
format.setLocale(l);
} catch (NullPointerException ee) {
}
return format.format(args);
} catch (MissingResourceException ex) {
throw new Error("Fatal: Localization data for JavaHelp is broken. Missing "+key+" key.");
}
}
/**
* Convenient method for creating a locale from a lang string.
* Takes the lang string in the form of "language_country_variant"
* or "language-country-variant" and
* parses the string and creates an appropriate locale.
* @param lang A String representation of a locale, with the language,
* country and variant separated by underbars. Language is always lower
* case, and country is always upper case. If the language is missing the
* String begins with an underbar. If both language and country fields are
* missing, a null Locale is returned. If lang is null a null Locale is
* returned
* @returns The locale based on the string or null if the Locale could
* not be constructed or lang was null.
*/
public static Locale localeFromLang(String lang) {
String language;
String country;
String variant=null;
Locale newlocale=null;
if (lang == null) {
return newlocale;
}
int lpt = lang.indexOf("_");
int lpt2 = lang.indexOf("-");
if (lpt == -1 && lpt2 == -1) {
language = lang;
country = "";
newlocale = new Locale(language, country);
} else {
if (lpt == -1 && lpt2 != -1) {
lpt = lpt2;
}
language = lang.substring(0, lpt);
int cpt = lang.indexOf("_", lpt+1);
int cpt2 = lang.indexOf("-", lpt+1);
if (cpt == -1 && cpt2 == -1) {
country = lang.substring(lpt+1);
newlocale = new Locale(language, country);
} else {
if (cpt == -1 && cpt2 != -1) {
cpt = cpt2;
}
country = lang.substring(lpt+1, cpt);
variant = lang.substring(cpt+1);
newlocale = new Locale(language, country, variant);
}
}
return newlocale;
}
/**
* Returns information about whether a string is
* contained in another string. Compares the character data stored in two
* different strings based on the collation rules.
*/
public static boolean isStringInString(RuleBasedCollator rbc,
String source,
String target) {
// The basic algorithm here is that we use CollationElementIterators
// to step through both the source and target strings. We compare each
// collation element in the source string against the corresponding one
// in the target, checking for differences.
//
// If a difference is found, we set to false.
//
// However, it's not that simple. If we find a tertiary difference
// (e.g. 'A' vs. 'a') near the beginning of a string, it can be
// overridden by a primary difference (e.g. "A" vs. "B") later in
// the string. For example, "AA" < "aB", even though 'A' > 'a'.
//
// To keep track of this, we use strengthResult to keep track of the
// strength of the most significant difference that has been found
// so far. When we find a difference whose strength is greater than
// strengthResult, it overrides the last difference (if any) that
// was found.
debug("isStringInString source=" + source + " targe =" + target);
// Initial sanity checks
if (source == null || target == null) {
return false;
}
if (source.length() == 0 && target.length() == 0) {
return true;
}
// on to the real code
int strengthResult = Collator.IDENTICAL;
CollationElementIterator sourceCursor, targetCursor;
boolean isFrenchSec = false;
// set up the RuleBasedCollator and extract
// various attributes
rbc.setDecomposition(Collator.FULL_DECOMPOSITION);
String rules = rbc.getRules();
if (rules.startsWith("@")) {
// I suspect we are cheating here when multiple rules are merged
// One would think you could get this attribute but you can't
isFrenchSec = true;
}
// Get the CollationElementIterators for the source and target
sourceCursor = rbc.getCollationElementIterator(source);
targetCursor = rbc.getCollationElementIterator(target);
// Various locale specific matching parameters
int sOrder = 0, tOrder = 0;
int pSOrder, pTOrder;
short secSOrder, secTOrder;
short terSOrder, terTOrder;
boolean gets = true, gett = true;
int toffset = 0;
boolean initialCheckSecTer, checkSecTer, checkTertiary;
startSearchForFirstMatch:
while(true) {
debug("while(true) toffset=" + toffset);
// Set the starting point for this comparison
try {
sourceCursor.setOffset(0);
} catch (NoSuchMethodError ex3) {
// ingnore it
}
sOrder = sourceCursor.next();
try {
targetCursor.setOffset(toffset);
} catch (NoSuchMethodError ex4) {
// Can't really do anything here. Return false.
} catch (Exception e) {
// We can get an exception when the toffset is really the
// end of string
// Just catch it and return false;
return false;
}
tOrder = targetCursor.next();
if (tOrder == CollationElementIterator.NULLORDER) {
break;
}
debug("sOrder=" + sOrder + " tOrder=" + tOrder);
//Find the first character in the target that matches the first
// character in the source. Look for a complete match of
// primary, secondary and tertiary. Lacking that look for
// a match of just primary.
while (tOrder != CollationElementIterator.NULLORDER) {
if (sOrder == tOrder) {
try {
toffset = targetCursor.getOffset();
} catch (NoSuchMethodError ex) {
//ignore it
}
break;
}
pSOrder = CollationElementIterator.primaryOrder(sOrder);
pTOrder = CollationElementIterator.primaryOrder(tOrder);
if (pSOrder == pTOrder) {
try {
toffset = targetCursor.getOffset();
} catch (NoSuchMethodError ex2) {
//ignore it
}
break;
}
tOrder = targetCursor.next();
debug("next tOrder=" + tOrder);
}
// If at end there is no match
if (tOrder == CollationElementIterator.NULLORDER) {
return false;
}
gets = false;
gett = false;
initialCheckSecTer = rbc.getStrength() >= Collator.SECONDARY;
checkSecTer = initialCheckSecTer;
checkTertiary = rbc.getStrength() >= Collator.TERTIARY;
while (true) {
// Get the next collation element in each of the strings, unless
// we've been requested to skip it.
if (gets) {
sOrder = sourceCursor.next();
} else {
gets = true;
}
if (gett) {
tOrder = targetCursor.next();
} else {
gett = true;
}
// If we've hit the end of one of the strings, jump out of the loop
if ((sOrder == CollationElementIterator.NULLORDER)||
(tOrder == CollationElementIterator.NULLORDER)) {
debug("One string at end");
break;
}
pSOrder = CollationElementIterator.primaryOrder(sOrder);
pTOrder = CollationElementIterator.primaryOrder(tOrder);
// If there's no difference at this position, we can skip it
if (sOrder == tOrder) {
if (isFrenchSec && pSOrder != 0) {
if (!checkSecTer) {
// in french, a secondary difference more to the
// right is stronger,
// so accents have to be checked with each base
// element
checkSecTer = initialCheckSecTer;
// but tertiary differences are less important
// than the first
// secondary difference, so checking tertiary
// remains disabled
checkTertiary = false;
}
}
debug("No diff at this positon continue");
continue;
}
// Compare primary differences first.
if ( pSOrder != pTOrder ) {
if (sOrder == 0) {
// The entire source element is ignorable.
// Skip to the next source element,
// but don't fetch another target element.
gett = false;
continue;
}
if (tOrder == 0) {
// The entire target element is ignorable.
// Skip to the next target element,
// but don't fetch another source element.
gets = false;
continue;
}
// The source and target elements aren't ignorable,
// but it's still possible
// for the primary component of one of the elements
// to be ignorable....
if (pSOrder == 0) {
// The source's primary is ignorable,
// but the target's isn't. We treat ignorables
// as a secondary differenc
if (checkSecTer) {
// (strength is SECONDARY)
targetCursor.next();
toffset = targetCursor.getOffset();
debug("Strength is secondary pSOrder === 0");
continue startSearchForFirstMatch;
}
// Skip to the next source element,
// but don't fetch another target element.
gett = false;
} else if (pTOrder == 0) {
// record differences - see the comment above.
if (checkSecTer) {
// (stength is SECONDARY)
targetCursor.next();
toffset = targetCursor.getOffset();
debug("Strength is secondary - pTOrder == 0");
continue startSearchForFirstMatch;
}
// Skip to the next source element,
// but don't fetch another target element.
gets = false;
} else {
// Neither of the orders is ignorable,
// and we already know that the primary
// orders are different because of the
// (pSOrder != pTOrder) test above.
// Record the difference and stop the
// comparison.
targetCursor.next();
toffset = targetCursor.getOffset();
debug("Order are ignorable");
continue startSearchForFirstMatch;
}
} else { // else of if ( pSOrder != pTOrder )
// primary order is the same, but complete order is
// different. So there
// are no base elements at this point, only
// ignorables (Since the strings are
// normalized)
if (checkSecTer) {
// a secondary or tertiary difference may
// still matter
secSOrder = CollationElementIterator.secondaryOrder(sOrder);
secTOrder = CollationElementIterator.secondaryOrder(tOrder);
if (secSOrder != secTOrder) {
// there is a secondary difference
targetCursor.next();
toffset = targetCursor.getOffset();
debug("Secondary Difference");
continue startSearchForFirstMatch;
// (strength is SECONDARY)
// (even in french, only the first
// secondary difference within
// a base character matters)
} else {
if (checkTertiary) {
// a tertiary difference may still matter
terSOrder = CollationElementIterator.tertiaryOrder(sOrder);
terTOrder = CollationElementIterator.tertiaryOrder(tOrder);
if (terSOrder != terTOrder) {
// there is a tertiary difference
targetCursor.next();
toffset = targetCursor.getOffset();
debug("Tertiary difference");
continue startSearchForFirstMatch;
// (strength is TERTIARY)
}
}
}
} // if (checkSecTer)
} // if ( pSOrder != pTOrder )
} // while()
// There might be a match
// Just do some final checking
if (sOrder != CollationElementIterator.NULLORDER) {
// (tOrder must be CollationElementIterator::NULLORDER,
// since this point is only reached when sOrder or
// tOrder is NULLORDER.)
// The source string has more elements,
// but the target string hasn't.
do {
if (CollationElementIterator.primaryOrder(sOrder) != 0) {
// We found an additional non-ignorable base
// character in the source string.
// This is a primary difference,
// so the source is greater
targetCursor.next();
toffset = targetCursor.getOffset();
debug("Additional non-ignborable base character in source string - source is greater");
continue startSearchForFirstMatch;
// (strength is PRIMARY)
}
else if (CollationElementIterator.secondaryOrder(sOrder) != 0) {
// Additional secondary elements mean the
// source string is greater
if (checkSecTer) {
targetCursor.next();
toffset = targetCursor.getOffset();
debug("Additional secondary elements source is greater");
continue startSearchForFirstMatch;
// (strength is SECONDARY)
}
}
} while ((sOrder = sourceCursor.next()) != CollationElementIterator.NULLORDER);
}
// Either the target string has more elements,
// but the source string hasn't or neither the target string
// or the source string have any more elemnts.
// In either case the source was contained in target
// return true;
return true;
}
return false;
}
/**
* Debug support
*/
private static final boolean debug = false;
private static void debug(Object msg1, Object msg2, Object msg3) {
if (debug) {
System.err.println("HelpUtilities: "+msg1+msg2+msg3);
}
}
private static void debug(Object msg1) { debug(msg1,"",""); }
private static void debug(Object msg1, Object msg2) {
debug(msg1,msg2,"");
}
}