From 79c84a7ba1d3457e10383eef91cfbc03dc85973d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Joseph?= Date: Tue, 14 Jan 2014 18:12:45 +0100 Subject: [PATCH] WIP ___ case-insensitive enums --- .../node2bean/impl/EnumAwareConvertUtilsBean.java | 8 +- .../jcr/node2bean/impl/EnumCaseInsensitive.java | 81 +++++++++++ .../info/magnolia/jcr/node2bean/Node2BeanTest.java | 45 ++++++ .../info/magnolia/jcr/node2bean/SampleEnum.java | 2 +- .../node2bean/impl/EnumCaseInsensitiveTest.java | 157 +++++++++++++++++++++ 5 files changed, 290 insertions(+), 3 deletions(-) create mode 100644 magnolia-core/src/main/java/info/magnolia/jcr/node2bean/impl/EnumCaseInsensitive.java create mode 100644 magnolia-core/src/test/java/info/magnolia/jcr/node2bean/impl/EnumCaseInsensitiveTest.java diff --git a/magnolia-core/src/main/java/info/magnolia/jcr/node2bean/impl/EnumAwareConvertUtilsBean.java b/magnolia-core/src/main/java/info/magnolia/jcr/node2bean/impl/EnumAwareConvertUtilsBean.java index 78a4227..0a1d8fa 100644 --- a/magnolia-core/src/main/java/info/magnolia/jcr/node2bean/impl/EnumAwareConvertUtilsBean.java +++ b/magnolia-core/src/main/java/info/magnolia/jcr/node2bean/impl/EnumAwareConvertUtilsBean.java @@ -50,16 +50,20 @@ class EnumAwareConvertUtilsBean extends ConvertUtilsBean { final Converter converter = super.lookup(clazz); // no specific converter for this class, so it's neither a String, (which has a default converter), // nor any known object that has a custom converter for it. It might be an enum ! + // We can't just register the converter - because they're registered on a per class basis. if (converter == null && clazz.isEnum()) { return enumConverter; } return converter; } - private class EnumConverter implements Converter { + private static class EnumConverter implements Converter { + private final EnumCaseInsensitive enumFinder = new EnumCaseInsensitive(); + @Override public Object convert(Class type, Object value) { - return Enum.valueOf(type, (String) value); + return enumFinder.valueOf(type, (String) value); } } + } diff --git a/magnolia-core/src/main/java/info/magnolia/jcr/node2bean/impl/EnumCaseInsensitive.java b/magnolia-core/src/main/java/info/magnolia/jcr/node2bean/impl/EnumCaseInsensitive.java new file mode 100644 index 0000000..ffd082b --- /dev/null +++ b/magnolia-core/src/main/java/info/magnolia/jcr/node2bean/impl/EnumCaseInsensitive.java @@ -0,0 +1,81 @@ +/** + * This file Copyright (c) 2014 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.jcr.node2bean.impl; + +import java.util.HashMap; +import java.util.Map; + +/** + * A utility to retrieve enum constants in a case-insensitive manner. + */ +class EnumCaseInsensitive { + private final Map, Map> enumsCache = new HashMap, Map>(16); // 16 enums in the system seems reasonable ? + + > E valueOf(Class type, String name) { + if (!type.isEnum()) { + throw new IllegalArgumentException(type + " is not an enum"); + } + Map thisEnumsCache = (Map) enumsCache.get(type); + if (thisEnumsCache == null) { + thisEnumsCache = new HashMap(8); // otoh, 8 entries per enum is probably more than enough + enumsCache.put(type, thisEnumsCache); + } + E value = thisEnumsCache.get(name); + if (value == null) { + value = getEnumIgnoreCase(type, name); + thisEnumsCache.put(name, value); + } + return value; + } + + /** + * After some performance test, we verified that, obviously, the below is slower than Enum.valueOf(). + * Tried doing some improvements with EnumSet and caching of enums, but that didn't seem to help. + */ + > T getEnumIgnoreCase(Class type, String name) { + // first check case-sensitively + for (T e : type.getEnumConstants()) { + if (e.name().equals(name)) { + return e; + } + } + // then case-insensitively + for (T e : type.getEnumConstants()) { + if (e.name().equalsIgnoreCase(name)) { + return e; + } + } + throw new IllegalArgumentException("No enum const named " + name + " for " + type); + } +} diff --git a/magnolia-core/src/test/java/info/magnolia/jcr/node2bean/Node2BeanTest.java b/magnolia-core/src/test/java/info/magnolia/jcr/node2bean/Node2BeanTest.java index 2fa5e90..d175b96 100644 --- a/magnolia-core/src/test/java/info/magnolia/jcr/node2bean/Node2BeanTest.java +++ b/magnolia-core/src/test/java/info/magnolia/jcr/node2bean/Node2BeanTest.java @@ -463,6 +463,51 @@ public class Node2BeanTest { } @Test + public void canConvertStringsToEnumCaseInsensitive() throws IOException, RepositoryException, Node2BeanException { + // GIVEN + Session session = SessionTestUtil.createSession("test", + "/parent.class=info.magnolia.jcr.node2bean.BeanWithEnum\n" + + "/parent.value=Hello\n" + + "/parent.sample=TwO\n" + ); + Node2BeanProcessorImpl n2b = new Node2BeanProcessorImpl(typeMapping, transformer); + + // WHEN + BeanWithEnum bean = (BeanWithEnum) n2b.toBean(session.getNode("/parent")); + + // THEN + assertNotNull(bean); + assertNotNull(bean.getSample()); + assertTrue(bean.getSample().getClass().isEnum()); + assertEquals("Hello", bean.getValue()); + assertEquals(SampleEnum.two, bean.getSample()); + } + + @Test + public void favorsExactCaseEnum() throws IOException, RepositoryException, Node2BeanException { + // GIVEN + // we know SampleEnum.three is defined before SampleEnum.THREE, but to make sure the test wasn't invalidated by other changes, let's check here: + assertArrayEquals(new SampleEnum[]{SampleEnum.one, SampleEnum.two, SampleEnum.three, SampleEnum.THREE}, SampleEnum.class.getEnumConstants()); + + Session session = SessionTestUtil.createSession("test", + "/parent.class=info.magnolia.jcr.node2bean.BeanWithEnum\n" + + "/parent.value=Hello\n" + + "/parent.sample=THREE\n" + ); + Node2BeanProcessorImpl n2b = new Node2BeanProcessorImpl(typeMapping, transformer); + + // WHEN + BeanWithEnum bean = (BeanWithEnum) n2b.toBean(session.getNode("/parent")); + + // THEN + assertNotNull(bean); + assertNotNull(bean.getSample()); + assertTrue(bean.getSample().getClass().isEnum()); + assertEquals("Hello", bean.getValue()); + assertEquals(SampleEnum.THREE, bean.getSample()); + } + + @Test public void testCanSpecifySpecificMapImplementation() throws Exception { // GIVEN Session session = SessionTestUtil.createSession("test", diff --git a/magnolia-core/src/test/java/info/magnolia/jcr/node2bean/SampleEnum.java b/magnolia-core/src/test/java/info/magnolia/jcr/node2bean/SampleEnum.java index fe0c264..b269e42 100644 --- a/magnolia-core/src/test/java/info/magnolia/jcr/node2bean/SampleEnum.java +++ b/magnolia-core/src/test/java/info/magnolia/jcr/node2bean/SampleEnum.java @@ -37,5 +37,5 @@ package info.magnolia.jcr.node2bean; * Sample Enum. */ public enum SampleEnum { - one, two, three; + one, two, three, THREE; } diff --git a/magnolia-core/src/test/java/info/magnolia/jcr/node2bean/impl/EnumCaseInsensitiveTest.java b/magnolia-core/src/test/java/info/magnolia/jcr/node2bean/impl/EnumCaseInsensitiveTest.java new file mode 100644 index 0000000..eff504f --- /dev/null +++ b/magnolia-core/src/test/java/info/magnolia/jcr/node2bean/impl/EnumCaseInsensitiveTest.java @@ -0,0 +1,157 @@ +/** + * This file Copyright (c) 2014 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.jcr.node2bean.impl; + +import info.magnolia.jcr.node2bean.SampleEnum; +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +public class EnumCaseInsensitiveTest { + + @Test + public void convertEnumSameCase() { + assertEquals(SampleEnum.two, new EnumCaseInsensitive().valueOf(SampleEnum.class, "two")); + } + + @Test + public void convertEnumIsCaseInsensitive() { + assertEquals(SampleEnum.two, new EnumCaseInsensitive().valueOf(SampleEnum.class, "TWO")); + } + + @Test + public void convertEnumIsCaseInsensitiveButWillReturnAMatchingCaseEnumMemberIfItExists() { + // whoTheHellWantsEnumsWithMembersWhereTheNamesOnlyDifferByCase() + + // Bot THREE and three exist. THREE is declared *after* three, but should be returned either "in priority". + assertEquals(SampleEnum.THREE, new EnumCaseInsensitive().valueOf(SampleEnum.class, "THREE")); + assertEquals(SampleEnum.three, new EnumCaseInsensitive().valueOf(SampleEnum.class, "three")); + + // For inputs with non-matching cases, we'll return the first match, which is not guarantee but so far happens to be following the declaration order. + assertEquals(SampleEnum.three, new EnumCaseInsensitive().valueOf(SampleEnum.class, "Three")); + } + + // + // ***************************************************************************************************************** + // Below are some alternative implementations I tried out, along with some basic perf test/comparison. + // At the time of writing, the current implementation is about 2x as slow as a simple Enum.valueOf, which was to be + // expected. It does show that the cache is however useful, as a more naive implementation would be 4x slower. + // ***************************************************************************************************************** + // + + // @Test + public void currentImpl() { + final EnumCaseInsensitive enumAwareConvertUtilsBean = new EnumCaseInsensitive(); + final long s = System.nanoTime(); + for (int i = 0; i < 10000000; i++) { + SampleEnum e = enumAwareConvertUtilsBean.valueOf(SampleEnum.class, "two"); + e = enumAwareConvertUtilsBean.valueOf(SampleEnum.class, "three"); + e = enumAwareConvertUtilsBean.valueOf(SampleEnum.class, "tHRee"); + e = enumAwareConvertUtilsBean.valueOf(SampleEnum.class, "one"); + e = enumAwareConvertUtilsBean.valueOf(SampleEnum.class, "thrEe"); + } + final long end = System.nanoTime(); + System.out.println(" EnumCaseInsensitive.valueOf took " + ((end - s) / 1000) + "µs"); + } + + // @Test + public void basic() { + final long s = System.nanoTime(); + for (int i = 0; i < 10000000; i++) { + SampleEnum e = getEnumBasic(SampleEnum.class, "two"); + e = getEnumBasic(SampleEnum.class, "three"); + e = getEnumBasic(SampleEnum.class, "three"); + e = getEnumBasic(SampleEnum.class, "one"); + e = getEnumBasic(SampleEnum.class, "three"); + } + final long end = System.nanoTime(); + System.out.println(" getEnumBasic took " + ((end - s) / 1000) + "µs"); + } + + // @Test + public void caseInsensitiveOnly() { + final long s = System.nanoTime(); + for (int i = 0; i < 10000000; i++) { + SampleEnum e = getEnumCaseInsensitive(SampleEnum.class, "two"); + e = getEnumCaseInsensitive(SampleEnum.class, "three"); + e = getEnumCaseInsensitive(SampleEnum.class, "tHRee"); + e = getEnumCaseInsensitive(SampleEnum.class, "one"); + e = getEnumCaseInsensitive(SampleEnum.class, "thrEe"); + } + final long end = System.nanoTime(); + System.out.println(" getEnumCaseInsensitive took " + ((end - s) / 1000) + "µs"); + } + + // @Test + public void ignoreCaseButFavorSameCaseFirst() { + final long s = System.nanoTime(); + for (int i = 0; i < 10000000; i++) { + SampleEnum e = getEnumIgnoreCase(SampleEnum.class, "two"); + e = getEnumIgnoreCase(SampleEnum.class, "three"); + e = getEnumIgnoreCase(SampleEnum.class, "tHRee"); + e = getEnumIgnoreCase(SampleEnum.class, "one"); + e = getEnumIgnoreCase(SampleEnum.class, "thrEe"); + } + final long end = System.nanoTime(); + System.out.println("ignoreCaseButFavorSameCaseFirst took " + ((end - s) / 1000) + "µs"); + } + + /** + * Case-sensitive. + */ + static > T getEnumBasic(Class type, String name) { + return Enum.valueOf(type, name); + } + + /** + * No optimization for correct-case inputs, no cache. + * Will not work nicely if an enum has members with a same-name-but-different-case. + */ + static > T getEnumCaseInsensitive(Class type, String name) { + for (T e : type.getEnumConstants()) { + if (e.name().equalsIgnoreCase(name)) { + return e; + } + } + throw new IllegalArgumentException("No enum const named " + name + " for " + type); + } + + /** + * This simply delegates to current implementation, without the caching. + */ + static > T getEnumIgnoreCase(Class type, String name) { + return enumFinder.getEnumIgnoreCase(type, name); + } + private static final EnumCaseInsensitive enumFinder = new EnumCaseInsensitive(); + +} -- 1.8.4.2