I just realized that gmail wasn't replying to the list. Sorry. For posterity...
On Tue, Dec 16, 2008 at 3:34 PM, Matthew Hanlon <mrhan...@gmail.com> wrote: > Below is the updated CaseInsensitiveClassResolver I implemented per your > suggestions. It uses MatchingResources from wicketstuff annotation. It > scans packages on-demand and caches the results for future lookup. It seems > fairly fast, and due to the cache it doesn't have to re-scan the classpath. > Also below is my CaseInsensitivePackageRequestTargetUrlCodingStrategy > (Gasp! What an awfully long name.). > If you think it's worthwhile, I'd love to contribute it to > wicketstuff-minis. How would one got about doing that? > > public class CaseInsensitiveClassResolver implements IClassResolver { > > private static final Logger logger = > LoggerFactory.getLogger(CaseInsensitiveClassResolver.class); > private DefaultClassResolver resolver = new DefaultClassResolver(); > public Iterator<URL> getResources(String name) { > return resolver.getResources(name); > } > private Map<String, Map<String, Class<?>>> cache = new HashMap<String, > Map<String,Class<?>>>(); > public Class<?> resolveClass(String classname) throws > ClassNotFoundException { > Class<?> clazz = null; > try { > clazz = resolver.resolveClass(classname); > } catch (ClassNotFoundException e1) { > clazz = resolveClassCaseInsensitive(classname); > } catch (NoClassDefFoundError e2) { > clazz = resolveClassCaseInsensitive(classname); > } > if (clazz == null) { > throw new ClassNotFoundException("Unable to resolve class for name " + > classname); > } > return clazz; > } > public Class<?> resolveClassCaseInsensitive(String classname) { > if (logger.isDebugEnabled()) { > logger.debug("Class not found for " + classname + ". Trying to look up > case-insensitive."); > } > String packageName = classname.substring(0, classname.lastIndexOf('.')); > if (! cache.containsKey(packageName)) { > cache.put(packageName, scan(getPatternForPackage(packageName))); > } > return cache.get(packageName).get(classname.toLowerCase()); > } > /** > * Get the Spring search pattern given a package name or part of a > package name > * @param packageName a package name > * @return a Spring search pattern for the given package > */ > public String getPatternForPackage(String packageName) > { > if (packageName == null) packageName = ""; > packageName = packageName.replace('.', '/'); > if (!packageName.endsWith("/")) > { > packageName += '/'; > } > > return "classpath*:" + packageName + "**/*.class"; > } > > /** > * > * @param pattern > */ > private Map<String, Class<?>> scan(final String pattern) { > Map<String, Class<?>> classMap = new HashMap<String, Class<?>>(); > MatchingResources resources = new MatchingResources(pattern); > MetadataReaderFactory f = new SimpleMetadataReaderFactory(); > for (Resource r : resources.getAllMatches()) { > MetadataReader meta = null; > try > { > meta = f.getMetadataReader(r); > } > catch (IOException e) > { > throw new RuntimeException("Unable to get MetadataReader > for " + r, e); > } > try { > ClassMetadata cmd = meta.getClassMetadata(); > String classname = cmd.getClassName(); > try { > classMap.put(classname.toLowerCase(), > getClass().getClassLoader().loadClass(classname)); > } catch (ClassNotFoundException e) { > logger.error("Error loading class for name " + classname); > } > } catch (Throwable e) { > logger.error("Unknown Error.", e); > } > } > return classMap; > } > > } > > public class CaseInsensitivePackageRequestTargetUrlCodingStrategy extends > PackageRequestTargetUrlCodingStrategy { > > private static final Logger log = > LoggerFactory.getLogger(CaseInsensitivePackageRequestTargetUrlCodingStrategy.class); > public CaseInsensitivePackageRequestTargetUrlCodingStrategy(final String > path, PackageName packageName) { > super(path, packageName); > this.packageName = packageName; > } > > /** package for this mount. */ > private final PackageName packageName; > private IClassResolver resolver = new CaseInsensitiveClassResolver(); > /** > * @see > org.apache.wicket.request.target.coding.IRequestTargetUrlCodingStrategy#decode(org.apache.wicket.request.RequestParameters) > */ > public IRequestTarget decode(RequestParameters requestParameters) > { > String remainder = > requestParameters.getPath().substring(getMountPath().length()); > final String parametersFragment; > int ix = remainder.indexOf('/', 1); > if (ix == -1) > { > ix = remainder.length(); > parametersFragment = ""; > } > else > { > parametersFragment = remainder.substring(ix); > } > > if (remainder.startsWith("/")) > { > remainder = remainder.substring(1); > ix--; > } > else > { > // There is nothing after the mount path! > return null; > } > > final String bookmarkablePageClassName = packageName + "." + > remainder.substring(0, ix); > Class bookmarkablePageClass; > try > { > bookmarkablePageClass = resolver.resolveClass(bookmarkablePageClassName); > } > catch (Exception e) > { > log.debug(e.getMessage()); > return null; > } > PageParameters parameters = new > PageParameters(decodeParameters(parametersFragment, > requestParameters.getParameters())); > > String pageMapName = > (String)parameters.remove(WebRequestCodingStrategy.PAGEMAP); > pageMapName = WebRequestCodingStrategy.decodePageMapName(pageMapName); > requestParameters.setPageMapName(pageMapName); > > // do some extra work for checking whether this is a normal request to a > // bookmarkable page, or a request to a stateless page (in which case a > // wicket:interface parameter should be available > final String interfaceParameter = (String)parameters > .remove(WebRequestCodingStrategy.INTERFACE_PARAMETER_NAME); > > if (interfaceParameter != null) > { > WebRequestCodingStrategy.addInterfaceParameters(interfaceParameter, > requestParameters); > return new BookmarkableListenerInterfaceRequestTarget(pageMapName, > bookmarkablePageClass, parameters, requestParameters.getComponentPath(), > requestParameters.getInterfaceName(), > requestParameters.getVersionNumber()); > } > else > { > return new BookmarkablePageRequestTarget(pageMapName, > bookmarkablePageClass, parameters); > } > } > } > > > > On Fri, Dec 12, 2008 at 10:57 AM, Jeremy Thomerson < > jer...@wickettraining.com> wrote: > >> One other thing - I think that the contract of IClassResolver would mean >> that rather than returning null, you throw a ClassNotFoundException. >> >> >> On Fri, Dec 12, 2008 at 8:24 AM, Matthew Hanlon <mrhan...@gmail.com>wrote: >> >>> Great ideas, thanks for the input. I agree on all points. My initial >>> implementation is certainly the naive approach, basically a proof of >>> concept. I'll look into what you mention in 4 and let you know what I find. >>> >>> >>> >>> On Fri, Dec 12, 2008 at 12:44 AM, Jeremy Thomerson < >>> jer...@wickettraining.com> wrote: >>> >>>> Sounds like an interesting idea. Here are a few thoughts I had after >>>> seeing it. Hopefully these are helpful. >>>> >>>> 1 - Say you had a page "CustomerAdminLoginPage" - this yields 4,194,304 >>>> combinations! >>>> Cache the result - either the class you found or the fact that you could >>>> not >>>> find a class. (you will have 2 to the nth power, where n equals the length >>>> of the simple name) >>>> 2 - DON'T use StringBuilder just to split it later - that's not what >>>> it's for! It's very slow and is constantly resizing it's internal array. >>>> You could use something like the code I pasted below to use a single array, >>>> initialized ahead of time to the proper size. >>>> >>>> 3 - I would suggest not even holding an array of possible combos - >>>> longer class names take a ton of memory because of all the millions of >>>> strings created. If you must go through all combos to try to find a match, >>>> just look for the match in your loop rather than looping to create an array >>>> of combos and then re-looping to try to find a match. >>>> >>>> 4 - Now - I would suggest seeing if you can avoid looping through all >>>> possible combos altogether. Look at how Wicket Annotations (in Wicket >>>> Stuff) does classpath scanning... I would think that this would be much >>>> more efficient - scan the package ahead of time and find all classes in the >>>> package and cache their names. Then just do a case-insensitive look into >>>> your cache - this saves you all the memory and processing trouble of ever >>>> computing all the combos and trying to load potentially millions of >>>> non-existent classes. >>>> >>>> 5 - If you get this to work and work well, add it to wicketstuff-minis >>>> or a similar project where others can easily use it - let me know if you >>>> need help accomplishing that. >>>> >>>> >>>> Here's an example of an improved method of finding the combos - probably >>>> could still be improved considerably, but this is a significant improvement >>>> over your first rough draft. (Although see point 4 - I recommend not even >>>> using this method at all) >>>> >>>> private static int capsCombinations(String[] combos, String word, >>>> int startIndex, int arrayIndex) { >>>> if (arrayIndex == 0) { >>>> word = word.toLowerCase(); >>>> combos[arrayIndex++] = word; >>>> } >>>> if (arrayIndex == combos.length) { >>>> return arrayIndex; >>>> } else { >>>> while (startIndex < word.length()) { >>>> char[] chars = word.toCharArray(); >>>> chars[startIndex] = >>>> Character.toUpperCase(chars[startIndex]); >>>> String string = String.valueOf(chars); >>>> combos[arrayIndex++] = string; >>>> arrayIndex = capsCombinations(combos, string, >>>> ++startIndex, arrayIndex); >>>> } >>>> return arrayIndex; >>>> } >>>> } >>>> >>>> public static void main(String[] args) throws Exception { >>>> long start = System.currentTimeMillis(); >>>> String name = "CustomerAdminLoginPage"; >>>> String[] combos = new String[(int) Math.pow(2, name.length())]; >>>> capsCombinations(combos, name, 0, 0); >>>> System.out.print(combos.length + " combos - took "); >>>> System.out.print((System.currentTimeMillis() - start) + " millis >>>> - used "); >>>> Runtime rt = Runtime.getRuntime(); >>>> System.out.println((rt.totalMemory() / 1048576) + "MB of >>>> memory"); >>>> } >>>> >>>> -- >>>> Jeremy Thomerson >>>> http://www.wickettraining.com >>>> >>>> >>>> On Thu, Dec 11, 2008 at 2:55 PM, Matthew Hanlon <mrhan...@gmail.com>wrote: >>>> >>>>> I am looking for some feedback any may have on this: >>>>> Let's say I've mounted a package "com.company.package" using >>>>> PackageRequestTargetUrlCodingStrategy >>>>> on "/foo." So I have several pages, /foo/Bar, /foo/Baz, etc. Now, I >>>>> want my >>>>> page mounts to be case-insensitive in the case that a user has caps >>>>> lock on >>>>> or types in all lower case or whatever. For >>>>> PackageRequestTargetUrlCodingStrategy this works for the "/foo" part, >>>>> but >>>>> not the classname part, obviously. >>>>> >>>>> So I implemented a CaseInsensitiveClassResolver that delegates to a >>>>> DefaultClassResolver. In the case that the DefaultClassResolver cannot >>>>> find >>>>> the class, the CaseInsensitiveClassResolver tries to load the class by >>>>> trying different combinations of upper/lower case in the classname. So, >>>>> for >>>>> "bar" it would try to resolve "com.company.package.Bar," " >>>>> com.company.package.bAr," "com.company.package.baR," etc, obviously >>>>> finding >>>>> "com.company.package.Bar" and returning that class. >>>>> >>>>> This works pretty well. Now, obviously it's not the most efficient >>>>> thing, >>>>> possibly having to catch several >>>>> ClassNotFoundException/NoClassDefFoundError exceptions >>>>> before finding the class (assuming the name exists and is spelled >>>>> correctly, >>>>> just with wrong case). But it might be better than returning a 404 on a >>>>> page >>>>> simply due to improper case. I wouldn't expect it to happen often, as >>>>> more >>>>> often than not the user will probably use a link the get to the pages, >>>>> and >>>>> so no typing at all. But in the rare case... >>>>> >>>>> So, any thoughts? >>>>> >>>>> Here's the code: >>>>> >>>>> public class CaseInsensitiveClassResolver implements IClassResolver { >>>>> >>>>> private static final Logger logger = >>>>> LoggerFactory.getLogger(CaseInsensitiveClassResolver.class); >>>>> private DefaultClassResolver resolver = new DefaultClassResolver(); >>>>> public Iterator<URL> getResources(String name) { >>>>> return resolver.getResources(name); >>>>> } >>>>> >>>>> public Class<?> resolveClass(String classname) { >>>>> Class<?> clazz = null; >>>>> try { >>>>> clazz = resolver.resolveClass(classname); >>>>> } catch (ClassNotFoundException e1) { >>>>> clazz = resolveClassCaseInsensitive(classname); >>>>> } catch (NoClassDefFoundError e2) { >>>>> clazz = resolveClassCaseInsensitive(classname); >>>>> } >>>>> return clazz; >>>>> } >>>>> public Class<?> resolveClassCaseInsensitive(String classname) throws >>>>> ClassNotFoundException { >>>>> if (logger.isDebugEnabled()) { >>>>> logger.debug("Class not found for " + classname + ". Trying to look up >>>>> case-insensitive."); >>>>> } >>>>> String packageName = classname.substring(0, >>>>> classname.lastIndexOf('.')); >>>>> String simpleName = classname.substring(classname.lastIndexOf('.') + >>>>> 1); >>>>> String combos = capsCombinations(simpleName.toLowerCase(), 0); >>>>> Class<?> cls = null; >>>>> for (String combo : combos.split(",")) { >>>>> try { >>>>> cls = resolver.resolveClass(packageName + "." + combo); >>>>> } catch (ClassNotFoundException e1) { >>>>> if (logger.isDebugEnabled()) { >>>>> logger.debug("Class not found for " + packageName + "." + combo + "."); >>>>> } >>>>> } catch (NoClassDefFoundError e2) { >>>>> if (logger.isDebugEnabled()) { >>>>> logger.debug("Class not found for " + packageName + "." + combo + "."); >>>>> } >>>>> } >>>>> if (cls != null) { >>>>> if (logger.isDebugEnabled()) { >>>>> logger.debug("Class found for " + packageName + "." + combo + "."); >>>>> } >>>>> return cls; >>>>> } >>>>> } >>>>> return null; >>>>> } >>>>> >>>>> private String capsCombinations(String word, int startIndex) { >>>>> StringBuilder sb = new StringBuilder(word); >>>>> if (word.equals(word.toUpperCase())) { >>>>> return sb.toString(); >>>>> } else { >>>>> for (; startIndex < word.length();) { >>>>> char[] chars = word.toCharArray(); >>>>> chars[startIndex] = Character.toUpperCase(chars[startIndex]); >>>>> sb.append(","); >>>>> sb.append(capsCombinations(new String(chars), ++startIndex)); >>>>> } >>>>> return sb.toString(); >>>>> } >>>>> } >>>>> } >>>>> -- >>>>> Matthew Rollins Hanlon >>>>> http://squareoftwo.org >>>>> _____________________ >>>>> Hanlon's Razor: >>>>> "Never attribute to malice that which can be adequately explained by >>>>> stupidity." >>>>> http://wikipedia.org/wiki/Hanlon's_razor >>>>> >>>> >>>> >>>> >>>> >>> >>> >>> -- >>> Matthew Rollins Hanlon >>> http://squareoftwo.org >>> _____________________ >>> Hanlon's Razor: >>> "Never attribute to malice that which can be adequately explained by >>> stupidity." >>> http://wikipedia.org/wiki/Hanlon's_razor >>> >> >> >> >> -- >> Jeremy Thomerson >> http://www.wickettraining.com >> > > > > -- > Matthew Rollins Hanlon > http://squareoftwo.org > _____________________ > Hanlon's Razor: > "Never attribute to malice that which can be adequately explained by > stupidity." > http://wikipedia.org/wiki/Hanlon's_razor > -- Matthew Rollins Hanlon http://squareoftwo.org _____________________ Hanlon's Razor: "Never attribute to malice that which can be adequately explained by stupidity." http://wikipedia.org/wiki/Hanlon's_razor