diff options
8 files changed, 374 insertions, 85 deletions
diff --git a/client-compiler/src/com/vaadin/sass/linker/SassLinker.java b/client-compiler/src/com/vaadin/sass/linker/SassLinker.java index 82a228a166..a568ca9672 100644 --- a/client-compiler/src/com/vaadin/sass/linker/SassLinker.java +++ b/client-compiler/src/com/vaadin/sass/linker/SassLinker.java @@ -35,7 +35,7 @@ public class SassLinker extends AbstractLinker { @Override public String getDescription() { - return "Compiling SASS files in public folders to standard CSS"; + return "Compiling SCSS files in public folders to standard CSS"; } @Override @@ -47,8 +47,8 @@ public class SassLinker extends AbstractLinker { // The artifact to return ArtifactSet toReturn = new ArtifactSet(artifacts); - // The temporary sass files provided from the artefacts - List<FileInfo> sassFiles = new ArrayList<FileInfo>(); + // The temporary scss files provided from the artefacts + List<FileInfo> scssFiles = new ArrayList<FileInfo>(); // The public files are provided as inputstream, but the compiler // needs real files, as they can contain references to other @@ -56,10 +56,13 @@ public class SassLinker extends AbstractLinker { String tempFolderName = new Date().getTime() + File.separator; File tempFolder = createTempDir(tempFolderName); - // Create the temporary files + // Can't search here specifically for public resources, as the type + // is different during compilation. This means we have to loop + // through all the artifacts for (EmittedArtifact resource : artifacts .find(EmittedArtifact.class)) { + // Create the temporary files. String partialPath = resource.getPartialPath(); if (partialPath.endsWith(".scss")) { @@ -85,46 +88,54 @@ public class SassLinker extends AbstractLinker { tempfile); // Store the file info for the compilation - sassFiles.add(new FileInfo(tempfile, partialPath)); + scssFiles.add(new FileInfo(tempfile, partialPath)); - // In my oppinion, the SASS file does not need to be - // output to the web content, as they can't be used - // there + // In my opinion, the SCSS file does not need to be + // output to the web content folder, as they can't + // be used there toReturn.remove(resource); + } else { + logger.log(TreeLogger.WARN, "Duplicate file " + + tempfile.getPath()); } } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + logger.log(TreeLogger.ERROR, + "Could not write temporary file " + fileName, e); } } } // Compile the files and store them in the artifact - logger.log(TreeLogger.INFO, "Processing " + sassFiles.size() + logger.log(TreeLogger.INFO, "Processing " + scssFiles.size() + " Sass file(s)"); - for (FileInfo fileInfo : sassFiles) { + for (FileInfo fileInfo : scssFiles) { logger.log(TreeLogger.INFO, " " + fileInfo.originalScssPath + " -> " + fileInfo.getOriginalCssPath()); - ScssStylesheet scss; - try { - scss = ScssStylesheet.get(fileInfo.getAbsolutePath()); - scss.compile(); - InputStream is = new ByteArrayInputStream(scss.toString() - .getBytes()); + ScssStylesheet scss = ScssStylesheet.get(fileInfo + .getAbsolutePath()); + if (!fileInfo.isMixin()) { + scss.compile(); + InputStream is = new ByteArrayInputStream(scss + .toString().getBytes()); + + toReturn.add(this.emitInputStream(logger, is, + fileInfo.getOriginalCssPath())); + } - toReturn.add(this.emitInputStream(logger, is, - fileInfo.getOriginalCssPath())); + fileInfo.getFile().delete(); } catch (CSSException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + logger.log(TreeLogger.ERROR, "SCSS compilation failed for " + + fileInfo.getOriginalCssPath(), e); } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + logger.log( + TreeLogger.ERROR, + "Could not write CSS file for " + + fileInfo.getOriginalCssPath(), e); } catch (Exception e) { - // TODO Auto-generated catch block - e.printStackTrace(); + logger.log(TreeLogger.ERROR, "SCSS compilation failed for " + + fileInfo.getOriginalCssPath(), e); } } @@ -134,6 +145,13 @@ public class SassLinker extends AbstractLinker { return artifacts; } + /** + * Writes the contents of an InputStream out to a file. + * + * @param contents + * @param tempfile + * @throws IOException + */ private void writeFromInputStream(InputStream contents, File tempfile) throws IOException { // write the inputStream to a FileOutputStream @@ -170,8 +188,7 @@ public class SassLinker extends AbstractLinker { } /** - * Temporal storage for file info from Artifact - * + * Temporal storage for file info from Artifact. */ private class FileInfo { private String originalScssPath; @@ -182,6 +199,10 @@ public class SassLinker extends AbstractLinker { this.originalScssPath = originalScssPath; } + public boolean isMixin() { + return file.getName().startsWith("_"); + } + public String getAbsolutePath() { return file.getAbsolutePath(); } @@ -190,6 +211,9 @@ public class SassLinker extends AbstractLinker { return originalScssPath.substring(0, originalScssPath.length() - 5) + ".css"; } - } + public File getFile() { + return file; + } + } } diff --git a/server/src/com/vaadin/data/fieldgroup/FieldGroup.java b/server/src/com/vaadin/data/fieldgroup/FieldGroup.java index 8803054857..dad2e26497 100644 --- a/server/src/com/vaadin/data/fieldgroup/FieldGroup.java +++ b/server/src/com/vaadin/data/fieldgroup/FieldGroup.java @@ -693,10 +693,11 @@ public class FieldGroup implements Serializable { * Binds member fields found in the given object. * <p> * This method processes all (Java) member fields whose type extends - * {@link Field} and that can be mapped to a property id. Property id - * mapping is done based on the field name or on a @{@link PropertyId} - * annotation on the field. All non-null fields for which a property id can - * be determined are bound to the property id. + * {@link Field} and that can be mapped to a property id. Property ids are + * searched in the following order: @{@link PropertyId} annotations, exact + * field name matches and the case-insensitive matching that ignores + * underscores. All non-null fields for which a property id can be + * determined are bound to the property id. * </p> * <p> * For example: @@ -733,11 +734,12 @@ public class FieldGroup implements Serializable { * that have not been initialized. * <p> * This method processes all (Java) member fields whose type extends - * {@link Field} and that can be mapped to a property id. Property id - * mapping is done based on the field name or on a @{@link PropertyId} - * annotation on the field. Fields that are not initialized (null) are built - * using the field factory. All non-null fields for which a property id can - * be determined are bound to the property id. + * {@link Field} and that can be mapped to a property id. Property ids are + * searched in the following order: @{@link PropertyId} annotations, exact + * field name matches and the case-insensitive matching that ignores + * underscores. Fields that are not initialized (null) are built using the + * field factory. All non-null fields for which a property id can be + * determined are bound to the property id. * </p> * <p> * For example: @@ -777,11 +779,12 @@ public class FieldGroup implements Serializable { * member fields that have not been initialized. * <p> * This method processes all (Java) member fields whose type extends - * {@link Field} and that can be mapped to a property id. Property id - * mapping is done based on the field name or on a @{@link PropertyId} - * annotation on the field. Fields that are not initialized (null) are built - * using the field factory is buildFields is true. All non-null fields for - * which a property id can be determined are bound to the property id. + * {@link Field} and that can be mapped to a property id. Property ids are + * searched in the following order: @{@link PropertyId} annotations, exact + * field name matches and the case-insensitive matching that ignores + * underscores. Fields that are not initialized (null) are built using the + * field factory is buildFields is true. All non-null fields for which a + * property id can be determined are bound to the property id. * </p> * * @param objectWithMemberFields @@ -792,6 +795,10 @@ public class FieldGroup implements Serializable { */ protected void buildAndBindMemberFields(Object objectWithMemberFields, boolean buildFields) throws BindException { + if (getItemDataSource() == null) { + // no data source set, cannot find property ids + return; + } Class<?> objectClass = objectWithMemberFields.getClass(); for (java.lang.reflect.Field memberField : getFieldsInDeclareOrder(objectClass)) { @@ -812,7 +819,11 @@ public class FieldGroup implements Serializable { // @PropertyId(propertyId) always overrides property id propertyId = propertyIdAnnotation.value(); } else { - propertyId = memberField.getName(); + propertyId = findPropertyId(memberField); + if (propertyId == null) { + // Property id was not found, skip this field + continue; + } } // Ensure that the property id exists @@ -873,6 +884,51 @@ public class FieldGroup implements Serializable { } } + /** + * Searches for a property id from the current itemDataSource that matches + * the given memberField. + * <p> + * If perfect match is not found, uses a case insensitive search that also + * ignores underscores. Returns null if no match is found. Throws a + * SearchException if no item data source has been set. + * </p> + * <p> + * The propertyId search logic used by + * {@link #buildAndBindMemberFields(Object, boolean) + * buildAndBindMemberFields} can easily be customized by overriding this + * method. No other changes are needed. + * </p> + * + * @param memberField + * The field an object id is searched for + * @return + */ + protected Object findPropertyId(java.lang.reflect.Field memberField) { + String fieldName = memberField.getName(); + if (getItemDataSource() == null) { + throw new SearchException( + "Property id type for field '" + + fieldName + + "' could not be determined. No item data source has been set."); + } + Item dataSource = getItemDataSource(); + if (dataSource.getItemProperty(fieldName) != null) { + return fieldName; + } else { + String minifiedFieldName = fieldName.toLowerCase().replace("_", ""); + for (Object itemPropertyId : dataSource.getItemPropertyIds()) { + if (itemPropertyId instanceof String) { + String itemPropertyName = (String) itemPropertyId; + if (minifiedFieldName.equals(itemPropertyName.toLowerCase() + .replace("_", ""))) { + return itemPropertyName; + } + } + } + } + return null; + } + public static class CommitException extends Exception { public CommitException() { @@ -909,6 +965,18 @@ public class FieldGroup implements Serializable { } + public static class SearchException extends RuntimeException { + + public SearchException(String message) { + super(message); + } + + public SearchException(String message, Throwable t) { + super(message, t); + } + + } + /** * Builds a field and binds it to the given property id using the field * binder. diff --git a/server/tests/src/com/vaadin/tests/server/component/fieldgroup/CaseInsensitiveBinding.java b/server/tests/src/com/vaadin/tests/server/component/fieldgroup/CaseInsensitiveBinding.java new file mode 100644 index 0000000000..9b768ef77f --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/fieldgroup/CaseInsensitiveBinding.java @@ -0,0 +1,84 @@ +package com.vaadin.tests.server.component.fieldgroup; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import com.vaadin.data.fieldgroup.FieldGroup; +import com.vaadin.data.util.ObjectProperty; +import com.vaadin.data.util.PropertysetItem; +import com.vaadin.ui.FormLayout; +import com.vaadin.ui.TextField; + +public class CaseInsensitiveBinding { + + @Test + public void caseInsensitivityAndUnderscoreRemoval() { + PropertysetItem item = new PropertysetItem(); + item.addItemProperty("LastName", new ObjectProperty<String>("Sparrow")); + + class MyForm extends FormLayout { + TextField lastName = new TextField("Last name"); + + public MyForm() { + + // Should bind to the LastName property + addComponent(lastName); + } + } + + MyForm form = new MyForm(); + + FieldGroup binder = new FieldGroup(item); + binder.bindMemberFields(form); + + assertTrue("Sparrow".equals(form.lastName.getValue())); + } + + @Test + public void UnderscoreRemoval() { + PropertysetItem item = new PropertysetItem(); + item.addItemProperty("first_name", new ObjectProperty<String>("Jack")); + + class MyForm extends FormLayout { + TextField firstName = new TextField("First name"); + + public MyForm() { + // Should bind to the first_name property + addComponent(firstName); + } + } + + MyForm form = new MyForm(); + + FieldGroup binder = new FieldGroup(item); + binder.bindMemberFields(form); + + assertTrue("Jack".equals(form.firstName.getValue())); + } + + @Test + public void perfectMatchPriority() { + PropertysetItem item = new PropertysetItem(); + item.addItemProperty("first_name", new ObjectProperty<String>( + "Not this")); + item.addItemProperty("firstName", new ObjectProperty<String>("This")); + + class MyForm extends FormLayout { + TextField firstName = new TextField("First name"); + + public MyForm() { + // should bind to the firstName property, not first_name property + addComponent(firstName); + } + } + + MyForm form = new MyForm(); + + FieldGroup binder = new FieldGroup(item); + binder.bindMemberFields(form); + + assertTrue("This".equals(form.firstName.getValue())); + } + +} diff --git a/theme-compiler/tests/src/com/vaadin/sass/testcases/scss/AbstractDirectoryScanningSassTests.java b/theme-compiler/tests/src/com/vaadin/sass/testcases/scss/AbstractDirectoryScanningSassTests.java index c86866b591..38915fe3e2 100644 --- a/theme-compiler/tests/src/com/vaadin/sass/testcases/scss/AbstractDirectoryScanningSassTests.java +++ b/theme-compiler/tests/src/com/vaadin/sass/testcases/scss/AbstractDirectoryScanningSassTests.java @@ -31,25 +31,17 @@ import junit.framework.TestCase; import org.apache.commons.io.IOUtils; import org.junit.Assert; -import org.junit.Test; import com.vaadin.sass.internal.ScssStylesheet; +import com.vaadin.sass.testcases.scss.SassTestRunner.FactoryTest; public abstract class AbstractDirectoryScanningSassTests extends TestCase { - private String scssResourceName; - - protected AbstractDirectoryScanningSassTests(String scssResourceName) { - this.scssResourceName = scssResourceName; - } - - public static Collection<Object[]> getScssResourceNames(URL directoryUrl) + public static Collection<String> getScssResourceNames(URL directoryUrl) throws URISyntaxException { - List<Object[]> resources = new ArrayList<Object[]>(); - // temporary instance to enable subclasses to define where to scan for - // files + List<String> resources = new ArrayList<String>(); for (File scssFile : getScssFiles(directoryUrl)) { - resources.add(new Object[] { scssFile.getName() }); + resources.add(scssFile.getName()); } return resources; } @@ -72,8 +64,8 @@ public abstract class AbstractDirectoryScanningSassTests extends TestCase { protected abstract URL getResourceURL(String path); - @Test - public void compareScssWithCss() throws Exception { + @FactoryTest + public void compareScssWithCss(String scssResourceName) throws Exception { String referenceCss; File scssFile = getSassLangResourceFile(scssResourceName); File cssFile = getCssFile(scssFile); diff --git a/theme-compiler/tests/src/com/vaadin/sass/testcases/scss/AutomaticSassTests.java b/theme-compiler/tests/src/com/vaadin/sass/testcases/scss/AutomaticSassTests.java index 1e5ec1ad37..fbccae349a 100644 --- a/theme-compiler/tests/src/com/vaadin/sass/testcases/scss/AutomaticSassTests.java +++ b/theme-compiler/tests/src/com/vaadin/sass/testcases/scss/AutomaticSassTests.java @@ -20,15 +20,11 @@ import java.net.URL; import java.util.Collection; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; -@RunWith(Parameterized.class) -public class AutomaticSassTests extends AbstractDirectoryScanningSassTests { +import com.vaadin.sass.testcases.scss.SassTestRunner.TestFactory; - public AutomaticSassTests(String scssResourceName) { - super(scssResourceName); - } +@RunWith(SassTestRunner.class) +public class AutomaticSassTests extends AbstractDirectoryScanningSassTests { @Override protected URL getResourceURL(String path) { @@ -39,8 +35,8 @@ public class AutomaticSassTests extends AbstractDirectoryScanningSassTests { return AutomaticSassTests.class.getResource("/automatic" + path); } - @Parameters - public static Collection<Object[]> getScssResourceNames() + @TestFactory + public static Collection<String> getScssResourceNames() throws URISyntaxException { return getScssResourceNames(getResourceURLInternal("")); } diff --git a/theme-compiler/tests/src/com/vaadin/sass/testcases/scss/SassLangTests.java b/theme-compiler/tests/src/com/vaadin/sass/testcases/scss/SassLangTests.java index 7aabb9d23c..d0e53a8726 100644 --- a/theme-compiler/tests/src/com/vaadin/sass/testcases/scss/SassLangTests.java +++ b/theme-compiler/tests/src/com/vaadin/sass/testcases/scss/SassLangTests.java @@ -20,15 +20,11 @@ import java.net.URL; import java.util.Collection; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; -@RunWith(Parameterized.class) -public class SassLangTests extends AbstractDirectoryScanningSassTests { +import com.vaadin.sass.testcases.scss.SassTestRunner.TestFactory; - public SassLangTests(String scssResourceName) { - super(scssResourceName); - } +@RunWith(SassTestRunner.class) +public class SassLangTests extends AbstractDirectoryScanningSassTests { @Override protected URL getResourceURL(String path) { @@ -39,8 +35,8 @@ public class SassLangTests extends AbstractDirectoryScanningSassTests { return SassLangTests.class.getResource("/sasslang" + path); } - @Parameters - public static Collection<Object[]> getScssResourceNames() + @TestFactory + public static Collection<String> getScssResourceNames() throws URISyntaxException { return getScssResourceNames(getResourceURLInternal("")); } diff --git a/theme-compiler/tests/src/com/vaadin/sass/testcases/scss/SassLangTestsBroken.java b/theme-compiler/tests/src/com/vaadin/sass/testcases/scss/SassLangTestsBroken.java index 6e9ed007cd..a84a8ca814 100644 --- a/theme-compiler/tests/src/com/vaadin/sass/testcases/scss/SassLangTestsBroken.java +++ b/theme-compiler/tests/src/com/vaadin/sass/testcases/scss/SassLangTestsBroken.java @@ -20,15 +20,11 @@ import java.net.URL; import java.util.Collection; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; -@RunWith(Parameterized.class) -public class SassLangTestsBroken extends AbstractDirectoryScanningSassTests { +import com.vaadin.sass.testcases.scss.SassTestRunner.TestFactory; - public SassLangTestsBroken(String scssResourceName) { - super(scssResourceName); - } +@RunWith(SassTestRunner.class) +public class SassLangTestsBroken extends AbstractDirectoryScanningSassTests { @Override protected URL getResourceURL(String path) { @@ -39,8 +35,8 @@ public class SassLangTestsBroken extends AbstractDirectoryScanningSassTests { return SassLangTestsBroken.class.getResource("/sasslangbroken" + path); } - @Parameters - public static Collection<Object[]> getScssResourceNames() + @TestFactory + public static Collection<String> getScssResourceNames() throws URISyntaxException { return getScssResourceNames(getResourceURLInternal("")); } diff --git a/theme-compiler/tests/src/com/vaadin/sass/testcases/scss/SassTestRunner.java b/theme-compiler/tests/src/com/vaadin/sass/testcases/scss/SassTestRunner.java new file mode 100644 index 0000000000..f871d43b6c --- /dev/null +++ b/theme-compiler/tests/src/com/vaadin/sass/testcases/scss/SassTestRunner.java @@ -0,0 +1,133 @@ +/* + * Copyright 2012 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.sass.testcases.scss; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import org.junit.runners.BlockJUnit4ClassRunner; +import org.junit.runners.Parameterized; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.InitializationError; + +/** + * Test runner that executes methods annotated with @{@link FactoryTest} with + * all the values returned by a method annotated with @{@link TestFactory} as + * their parameters parameter. + * + * This runner is loosely based on FactoryTestRunner by Ted Young + * (http://tedyoung.me/2011/01/23/junit-runtime-tests-custom-runners/). The + * generated test names give information about the parameters used (unlike + * {@link Parameterized}). + * + * @since 7.0 + */ +public class SassTestRunner extends BlockJUnit4ClassRunner { + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface TestFactory { + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface FactoryTest { + } + + public SassTestRunner(Class<?> klass) throws InitializationError { + super(klass); + } + + @Override + protected List<FrameworkMethod> computeTestMethods() { + List<FrameworkMethod> tests = new LinkedList<FrameworkMethod>(); + + // Final all methods in our test class marked with @TestFactory. + for (FrameworkMethod method : getTestClass().getAnnotatedMethods( + TestFactory.class)) { + // Make sure the TestFactory method is static + if (!Modifier.isStatic(method.getMethod().getModifiers())) { + throw new IllegalArgumentException("TestFactory " + method + + " must be static."); + } + + // Execute the method (statically) + Object params; + try { + params = method.getMethod().invoke( + getTestClass().getJavaClass()); + } catch (Throwable t) { + throw new RuntimeException("Could not run test factory method " + + method.getName()); + } + + // Did the factory return an array? If so, make it a list. + if (params.getClass().isArray()) { + params = Arrays.asList((Object[]) params); + } + + // Did the factory return a scalar object? If so, put it in a list. + if (!(params instanceof Iterable<?>)) { + params = Collections.singletonList(params); + } + + // For each object returned by the factory. + for (Object param : (Iterable<?>) params) { + // Find any methods marked with @SassTest. + for (FrameworkMethod m : getTestClass().getAnnotatedMethods( + FactoryTest.class)) { + tests.add(new ParameterizedFrameworkMethod(m.getMethod(), + new Object[] { param })); + } + } + } + + return tests; + } + + private static class ParameterizedFrameworkMethod extends FrameworkMethod { + private Object[] params; + + public ParameterizedFrameworkMethod(Method method, Object[] params) { + super(method); + this.params = params; + } + + @Override + public Object invokeExplosively(Object target, Object... params) + throws Throwable { + // Executes the test method with the supplied parameters (returned + // by the + // TestFactory) and not the instance generated by FrameworkMethod. + return super.invokeExplosively(target, this.params); + } + + @Override + public String getName() { + return String.format("%s[%s]", getMethod().getName(), + Arrays.toString(params)); + } + } +} |