From 49ded4acd2682cc7dc888fb5d57b8afe47dc30d5 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Johannes=20Dahlstr=C3=B6m?= Date: Fri, 24 Apr 2015 15:54:23 +0300 Subject: [PATCH] Fix for declarative FontIcon support (#17275) Change-Id: I5d61ed7003811f95bba4ded71937bb08742936c5 --- server/src/com/vaadin/server/FontAwesome.java | 25 ++- .../com/vaadin/server/GenericFontIcon.java | 144 ++++++++++++++ .../com/vaadin/server/ResourceReference.java | 2 +- .../ui/declarative/DesignFormatter.java | 9 +- .../converters/DesignResourceConverter.java | 185 +++++++++++++++--- .../tests/design/DesignFormatterTest.java | 78 +++++++- .../vaadin/shared/ApplicationConstants.java | 8 + .../com/vaadin/shared/VaadinUriResolver.java | 2 +- 8 files changed, 420 insertions(+), 33 deletions(-) create mode 100644 server/src/com/vaadin/server/GenericFontIcon.java diff --git a/server/src/com/vaadin/server/FontAwesome.java b/server/src/com/vaadin/server/FontAwesome.java index f226e0c320..4c60b90233 100644 --- a/server/src/com/vaadin/server/FontAwesome.java +++ b/server/src/com/vaadin/server/FontAwesome.java @@ -538,7 +538,7 @@ public enum FontAwesome implements FontIcon { YOUTUBE_PLAY(0XF16A), // YOUTUBE_SQUARE(0XF166); - private static final String fontFamily = "FontAwesome"; + public static final String FONT_FAMILY = "FontAwesome"; private int codepoint; FontAwesome(int codepoint) { @@ -563,7 +563,7 @@ public enum FontAwesome implements FontIcon { */ @Override public String getFontFamily() { - return fontFamily; + return FontAwesome.FONT_FAMILY; } /* @@ -578,8 +578,25 @@ public enum FontAwesome implements FontIcon { @Override public String getHtml() { - return "&#x" + Integer.toHexString(codepoint) + ";"; + return GenericFontIcon.getHtml(FontAwesome.FONT_FAMILY, codepoint); + } + + /** + * Finds an instance of FontAwesome with given codepoint + * + * @since 7.5 + * @param codepoint + * @return FontAwesome instance with a specific codepoint or null if there + * isn't any + */ + public static FontAwesome fromCodepoint(final int codepoint) { + for (FontAwesome f : values()) { + if (f.getCodepoint() == codepoint) { + return f; + } + } + throw new IllegalArgumentException("Codepoint " + codepoint + + " not found in FontAwesome"); } } diff --git a/server/src/com/vaadin/server/GenericFontIcon.java b/server/src/com/vaadin/server/GenericFontIcon.java new file mode 100644 index 0000000000..ba61390fb0 --- /dev/null +++ b/server/src/com/vaadin/server/GenericFontIcon.java @@ -0,0 +1,144 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.server; + +/** + * A generic implementation of {@link FontIcon} interface + * + * @since + * @author Vaadin Ltd + */ +@SuppressWarnings("serial") +public class GenericFontIcon implements FontIcon { + private final String fontFamily; + private final int codePoint; + + /** + * Creates a new instance of GenericFontIcon with given font family and + * codepoint + * + * @param fontFamily + * Name of the type face that is used to display icons + * @param codepoint + * Numerical code point in the font + */ + public GenericFontIcon(String fontFamily, int codepoint) { + this.fontFamily = fontFamily; + codePoint = codepoint; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.server.FontIcon#getFontFamily() + */ + @Override + public String getFontFamily() { + return fontFamily; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.server.Resource#getMIMEType() + */ + @Override + public String getMIMEType() { + throw new UnsupportedOperationException(FontIcon.class.getSimpleName() + + " should not be used where a MIME type is needed."); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.server.FontIcon#getCodepoint() + */ + @Override + public int getCodepoint() { + return codePoint; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.server.FontIcon#getHtml() + */ + @Override + public String getHtml() { + return getHtml(fontFamily, codePoint); + } + + /** + * Utility method for generating HTML that displays an icon from specific + * fontFamiliy with a given codePoint in the font + * + * @since 7.5 + * @param fontFamily + * Name of the font family + * @param codePoint + * Icon's character code point in the font + * @return + */ + public static String getHtml(String fontFamily, int codePoint) { + return "&#x" + Integer.toHexString(codePoint) + ";"; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + codePoint; + result = prime * result + + ((fontFamily == null) ? 0 : fontFamily.hashCode()); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof GenericFontIcon)) { + return false; + } + GenericFontIcon other = (GenericFontIcon) obj; + if (codePoint != other.codePoint) { + return false; + } + if (fontFamily == null) { + if (other.fontFamily != null) { + return false; + } + } else if (!fontFamily.equals(other.fontFamily)) { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/server/src/com/vaadin/server/ResourceReference.java b/server/src/com/vaadin/server/ResourceReference.java index 31dfa41ef9..aeb06394b7 100644 --- a/server/src/com/vaadin/server/ResourceReference.java +++ b/server/src/com/vaadin/server/ResourceReference.java @@ -64,7 +64,7 @@ public class ResourceReference extends URLReference { String uri = getConnectorResourceBase(prefix, connector); return uri; } else if (resource instanceof ThemeResource) { - final String uri = "theme://" + final String uri = ApplicationConstants.THEME_PROTOCOL_PREFIX + ((ThemeResource) resource).getResourceId(); return uri; } else if (resource instanceof FontIcon) { diff --git a/server/src/com/vaadin/ui/declarative/DesignFormatter.java b/server/src/com/vaadin/ui/declarative/DesignFormatter.java index 728b3d1077..b1d2520631 100644 --- a/server/src/com/vaadin/ui/declarative/DesignFormatter.java +++ b/server/src/com/vaadin/ui/declarative/DesignFormatter.java @@ -325,9 +325,8 @@ public class DesignFormatter implements Serializable { // component. return (Converter) stringObjectConverter; } - if (sourceType.isEnum()) { - return (Converter) stringEnumConverter; - } else if (converterMap.containsKey(sourceType)) { + + if (converterMap.containsKey(sourceType)) { return ((Converter) converterMap.get(sourceType)); } else if (!strict) { for (Class supported : converterMap.keySet()) { @@ -336,6 +335,10 @@ public class DesignFormatter implements Serializable { } } } + + if (sourceType.isEnum()) { + return (Converter) stringEnumConverter; + } return null; } diff --git a/server/src/com/vaadin/ui/declarative/converters/DesignResourceConverter.java b/server/src/com/vaadin/ui/declarative/converters/DesignResourceConverter.java index 21f20e6403..ff73b61eb2 100644 --- a/server/src/com/vaadin/ui/declarative/converters/DesignResourceConverter.java +++ b/server/src/com/vaadin/ui/declarative/converters/DesignResourceConverter.java @@ -16,14 +16,20 @@ package com.vaadin.ui.declarative.converters; import java.io.File; +import java.io.Serializable; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; import com.vaadin.data.util.converter.Converter; import com.vaadin.server.ExternalResource; import com.vaadin.server.FileResource; import com.vaadin.server.FontAwesome; +import com.vaadin.server.FontIcon; +import com.vaadin.server.GenericFontIcon; import com.vaadin.server.Resource; import com.vaadin.server.ThemeResource; +import com.vaadin.shared.ApplicationConstants; import com.vaadin.ui.declarative.DesignAttributeHandler; /** @@ -33,21 +39,28 @@ import com.vaadin.ui.declarative.DesignAttributeHandler; * @since 7.4 * @author Vaadin Ltd */ +@SuppressWarnings("serial") public class DesignResourceConverter implements Converter { @Override public Resource convertToModel(String value, Class targetType, Locale locale) throws Converter.ConversionException { - if (value.startsWith("http://") || value.startsWith("https://") - || value.startsWith("ftp://") || value.startsWith("ftps://")) { - return new ExternalResource(value); - } else if (value.startsWith("theme://")) { - return new ThemeResource(value.substring(8)); - } else if (value.startsWith("font://")) { - return FontAwesome.valueOf(value.substring(7)); - } else { - return new FileResource(new File(value)); + if (!value.contains("://")) { + // assume it'is "file://" protocol, one that is used to access a + // file on a given path on the server, this will later be striped + // out anyway + value = "file://" + value; + } + + String protocol = value.split("://")[0]; + try { + ResourceConverterByProtocol converter = ResourceConverterByProtocol + .valueOf(protocol.toUpperCase(Locale.ENGLISH)); + return converter.parse(value); + } catch (IllegalArgumentException iae) { + throw new ConversionException("Unrecognized protocol: " + protocol, + iae); } } @@ -55,20 +68,10 @@ public class DesignResourceConverter implements Converter { public String convertToPresentation(Resource value, Class targetType, Locale locale) throws Converter.ConversionException { - if (value instanceof ExternalResource) { - return ((ExternalResource) value).getURL(); - } else if (value instanceof ThemeResource) { - return "theme://" + ((ThemeResource) value).getResourceId(); - } else if (value instanceof FontAwesome) { - return "font://" + ((FontAwesome) value).name(); - } else if (value instanceof FileResource) { - String path = ((FileResource) value).getSourceFile().getPath(); - if (File.separatorChar != '/') { - // make sure we use '/' as file separator in templates - return path.replace(File.separatorChar, '/'); - } else { - return path; - } + ResourceConverterByProtocol byType = ResourceConverterByProtocol + .byType(value.getClass()); + if (byType != null) { + return byType.format(value); } else { throw new Converter.ConversionException("unknown Resource type - " + value.getClass().getName()); @@ -85,4 +88,140 @@ public class DesignResourceConverter implements Converter { return String.class; } + private static interface ProtocolResourceConverter extends Serializable { + public String format(Resource value); + + public Resource parse(String value); + } + + private static enum ResourceConverterByProtocol implements + ProtocolResourceConverter { + + HTTP, HTTPS, FTP, FTPS, THEME { + + @Override + public Resource parse(String value) { + // strip "theme://" from the url, use the rest as the resource + // id + return new ThemeResource(value.substring(8)); + } + + @Override + public String format(Resource value) + throws Converter.ConversionException { + return ApplicationConstants.THEME_PROTOCOL_PREFIX + + ((ThemeResource) value).getResourceId(); + } + }, + FONTICON { + @Override + public Resource parse(String value) { + final String address = (value.split("://", 2))[1]; + final String[] familyAndCode = address.split("/", 2); + final int codepoint = Integer.valueOf( + familyAndCode[1].substring(2), 16); + + if (FontAwesome.FONT_FAMILY.equals(familyAndCode[0])) { + try { + return FontAwesome.fromCodepoint(codepoint); + } catch (IllegalArgumentException iae) { + throw new ConversionException( + "Unknown codepoint in FontAwesome: " + + codepoint, iae); + } + } + + FontIcon generic = new GenericFontIcon(familyAndCode[0], + codepoint); + return generic; + + } + + @Override + public String format(Resource value) + throws Converter.ConversionException { + FontIcon icon = (FontIcon) value; + return ApplicationConstants.FONTICON_PROTOCOL_PREFIX + + icon.getFontFamily() + "/0x" + + Integer.toHexString(icon.getCodepoint()); + } + }, + @Deprecated + FONT { + @Override + public Resource parse(String value) { + // Deprecated, 7.4 syntax is + // font://"+FontAwesome.valueOf(foo) eg. "font://AMBULANCE" + final String iconName = (value.split("://", 2))[1]; + + try { + return FontAwesome.valueOf(iconName); + } catch (IllegalArgumentException iae) { + throw new ConversionException("Unknown FontIcon constant: " + + iconName, iae); + } + } + + @Override + public String format(Resource value) + throws Converter.ConversionException { + throw new UnsupportedOperationException("Use " + + ResourceConverterByProtocol.FONTICON.toString() + + " instead"); + } + }, + FILE { + @Override + public Resource parse(String value) { + return new FileResource(new File(value.split("://")[1])); + } + + @Override + public String format(Resource value) + throws Converter.ConversionException { + String path = ((FileResource) value).getSourceFile().getPath(); + if (File.separatorChar != '/') { + // make sure we use '/' as file separator in templates + return path.replace(File.separatorChar, '/'); + } else { + return path; + } + } + + }; + + @Override + public Resource parse(String value) { + // default behavior for HTTP, HTTPS, FTP and FTPS + return new ExternalResource(value); + } + + @Override + public String format(Resource value) + throws Converter.ConversionException { + // default behavior for HTTP, HTTPS, FTP and FTPS + return ((ExternalResource) value).getURL(); + } + + private static Map, ResourceConverterByProtocol> typeToConverter = new HashMap, ResourceConverterByProtocol>(); + static { + typeToConverter.put(ExternalResource.class, HTTP); + // ^ any of non-specialized would actually work + typeToConverter.put(ThemeResource.class, THEME); + typeToConverter.put(FontIcon.class, FONTICON); + typeToConverter.put(FileResource.class, FILE); + + } + + public static ResourceConverterByProtocol byType( + Class resourceType) { + for (Class type : typeToConverter.keySet()) { + if (type.isAssignableFrom(resourceType)) { + return typeToConverter.get(type); + } + } + return null; + } + } + } diff --git a/server/tests/src/com/vaadin/tests/design/DesignFormatterTest.java b/server/tests/src/com/vaadin/tests/design/DesignFormatterTest.java index 681b9d80a3..dcabd6c637 100644 --- a/server/tests/src/com/vaadin/tests/design/DesignFormatterTest.java +++ b/server/tests/src/com/vaadin/tests/design/DesignFormatterTest.java @@ -18,6 +18,7 @@ package com.vaadin.tests.design; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import java.io.File; import java.math.BigDecimal; import java.text.SimpleDateFormat; import java.util.Date; @@ -34,8 +35,12 @@ import com.vaadin.event.ShortcutAction.KeyCode; import com.vaadin.event.ShortcutAction.ModifierKey; import com.vaadin.server.ExternalResource; import com.vaadin.server.FileResource; +import com.vaadin.server.FontAwesome; +import com.vaadin.server.FontIcon; +import com.vaadin.server.GenericFontIcon; import com.vaadin.server.Resource; import com.vaadin.server.ThemeResource; +import com.vaadin.shared.ApplicationConstants; import com.vaadin.shared.util.SharedUtil; import com.vaadin.ui.declarative.DesignFormatter; @@ -265,6 +270,78 @@ public class DesignFormatterTest { } } + @Test + public void testResourceFormat() { + String httpUrl = "http://example.com/icon.png"; + String httpsUrl = "https://example.com/icon.png"; + String themePath = "icons/icon.png"; + String fontAwesomeUrl = "fonticon://FontAwesome/0xf0f9"; + String someOtherFontUrl = "fonticon://SomeOther/0xF0F9"; + String fileSystemPath = "c:\\app\\resources\\icon.png"; + + assertEquals(httpUrl, formatter.format(new ExternalResource(httpUrl))); + assertEquals(httpsUrl, formatter.format(new ExternalResource(httpsUrl))); + assertEquals(ApplicationConstants.THEME_PROTOCOL_PREFIX + themePath, + formatter.format(new ThemeResource(themePath))); + + assertEquals(fontAwesomeUrl, formatter.format(FontAwesome.AMBULANCE)); + assertEquals(someOtherFontUrl.toLowerCase(), + formatter.format(new GenericFontIcon("SomeOther", 0xf0f9)) + .toLowerCase()); + + assertEquals(fileSystemPath, + formatter.format(new FileResource(new File(fileSystemPath)))); + } + + @Test(expected = ConversionException.class) + public void testResourceParseException() { + String someRandomResourceUrl = "random://url"; + formatter.parse(someRandomResourceUrl, Resource.class); + } + + @Test(expected = ConversionException.class) + public void testResourceFormatException() { + formatter.format(new Resource() { // must use unknown resource type + @Override + public String getMIMEType() { + // TODO Auto-generated method stub + return null; + } + }); + } + + @Test + public void testResourceParse() { + String httpUrl = "http://example.com/icon.png"; + String httpsUrl = "https://example.com/icon.png"; + String themePath = "icons/icon.png"; + String fontAwesomeUrl = "fonticon://FontAwesome/0xf0f9"; + String someOtherFont = "fonticon://SomeOther/0xF0F9"; + String fontAwesomeUrlOld = "font://AMBULANCE"; + String fileSystemPath = "c:\\app\\resources\\icon.png"; + + assertEquals(new ExternalResource(httpUrl).getURL(), + formatter.parse(httpUrl, ExternalResource.class).getURL()); + assertEquals(new ExternalResource(httpsUrl).getURL(), + formatter.parse(httpsUrl, ExternalResource.class).getURL()); + assertEquals( + new ThemeResource(themePath), + formatter.parse(ApplicationConstants.THEME_PROTOCOL_PREFIX + + themePath, ThemeResource.class)); + assertEquals(FontAwesome.AMBULANCE, + formatter.parse(fontAwesomeUrlOld, FontAwesome.class)); + assertEquals(FontAwesome.AMBULANCE, + formatter.parse(fontAwesomeUrl, FontAwesome.class)); + assertEquals(new GenericFontIcon("SomeOther", 0xF0F9), + formatter.parse(someOtherFont, FontIcon.class)); + + assertEquals( + new FileResource(new File(fileSystemPath)).getSourceFile(), + formatter.parse(fileSystemPath, FileResource.class) + .getSourceFile()); + + } + /** * A static method to allow comparison two different actions. * @@ -294,5 +371,4 @@ public class DesignFormatterTest { } return false; } - } diff --git a/shared/src/com/vaadin/shared/ApplicationConstants.java b/shared/src/com/vaadin/shared/ApplicationConstants.java index d7aaee6267..fa6aa33fc0 100644 --- a/shared/src/com/vaadin/shared/ApplicationConstants.java +++ b/shared/src/com/vaadin/shared/ApplicationConstants.java @@ -39,6 +39,14 @@ public class ApplicationConstants implements Serializable { public static final String PUBLISHED_PROTOCOL_NAME = "published"; public static final String PUBLISHED_PROTOCOL_PREFIX = PUBLISHED_PROTOCOL_NAME + "://"; + /** + * Prefix used for theme resource URLs + * + * @see com.vaadin.server.ThemeResource + * @since + */ + public static final String THEME_PROTOCOL_PREFIX = "theme://"; + public static final String UIDL_SECURITY_TOKEN_ID = "Vaadin-Security-Key"; @Deprecated diff --git a/shared/src/com/vaadin/shared/VaadinUriResolver.java b/shared/src/com/vaadin/shared/VaadinUriResolver.java index b45d32f71a..ee8d13f10f 100644 --- a/shared/src/com/vaadin/shared/VaadinUriResolver.java +++ b/shared/src/com/vaadin/shared/VaadinUriResolver.java @@ -58,7 +58,7 @@ public abstract class VaadinUriResolver implements Serializable { if (vaadinUri == null) { return null; } - if (vaadinUri.startsWith("theme://")) { + if (vaadinUri.startsWith(ApplicationConstants.THEME_PROTOCOL_PREFIX)) { final String themeUri = getThemeUri(); vaadinUri = themeUri + vaadinUri.substring(7); } -- 2.39.5