View Javadoc
1   /**
2    * This file Copyright (c) 2019 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.ui.framework.layout;
35  
36  import static com.vaadin.ui.declarative.Design.*;
37  
38  import info.magnolia.freemarker.FreemarkerHelper;
39  import info.magnolia.resourceloader.ResourceOrigin;
40  import info.magnolia.ui.field.EditorPropertyDefinition;
41  
42  import java.io.ByteArrayInputStream;
43  import java.io.InputStream;
44  import java.io.Reader;
45  import java.io.StringWriter;
46  import java.nio.charset.StandardCharsets;
47  import java.util.HashMap;
48  import java.util.Map;
49  import java.util.Optional;
50  
51  import javax.inject.Inject;
52  
53  import org.slf4j.Logger;
54  import org.slf4j.LoggerFactory;
55  
56  import com.vaadin.data.Binder;
57  import com.vaadin.data.HasValue;
58  import com.vaadin.ui.Component;
59  import com.vaadin.ui.Label;
60  import com.vaadin.ui.Layout;
61  import com.vaadin.ui.VerticalLayout;
62  import com.vaadin.ui.declarative.Design;
63  import com.vaadin.ui.declarative.DesignContext;
64  
65  /**
66   * DeclarativeLayoutProducer is able to produce a layout that is a mixture of vaadin components and magnolia form fields.
67   * Declarative layouts allow the creator to set custom attributes like `size-full` or `spacing="false"` and also support the use of custom pre-defined components.
68   * The declarative syntax is defined at https://vaadin.com/docs/v8/framework/application/application-declarative.html
69   *
70   * @see DeclarativeLayoutDefinition
71   */
72  public class DeclarativeLayoutProducer implements FieldLayoutProducer<DeclarativeLayoutDefinition> {
73  
74      private static final Logger log = LoggerFactory.getLogger(DeclarativeLayoutProducer.class);
75  
76      private FreemarkerHelper freemarkerHelper;
77      private ResourceOrigin resourceOrigin;
78      private final Binder binder;
79  
80      @Inject
81      public DeclarativeLayoutProducer(FreemarkerHelper freemarkerHelper, ResourceOrigin resourceOrigin, Binder binder) {
82          this.freemarkerHelper = freemarkerHelper;
83          this.resourceOrigin = resourceOrigin;
84          this.binder = binder;
85      }
86  
87      @Override
88      public Layout createLayout(DeclarativeLayoutDefinition definition, Map<EditorPropertyDefinition, Component> mappings) {
89          Design.ComponentMapper defaultComponentMapper = getComponentMapper();
90          try (final Reader templateReader = resourceOrigin.getByPath(definition.getTemplate()).openReader()) {
91              StringWriter ftlEnhancedTemplateWriter = new StringWriter();
92              freemarkerHelper.render(templateReader, new HashMap<>(), ftlEnhancedTemplateWriter);
93              InputStream enhancedTemplateInputStream = new ByteArrayInputStream(ftlEnhancedTemplateWriter.toString().getBytes(StandardCharsets.UTF_8));
94              setComponentMapper(new MagnoliaComponentMapper(defaultComponentMapper, mappings));
95              Layout../../../../../info/magnolia/ui/field/Layout.html#Layout">Layout layout = (Layout) Design.read(enhancedTemplateInputStream);
96              layout.setStyleName("declarative-layout");
97              return layout;
98          } catch (Exception e) {
99              log.error(String.format("Error while processing the template %s.", definition.getTemplate()), e);
100             Label label = new Label(String.format("%s: %s", e.getClass().getName(), e.getMessage()));
101             label.setStyleName("error-message");
102             Layout errorLayout = new VerticalLayout(label);
103             errorLayout.setStyleName("declarative-layout");
104             return errorLayout;
105         } finally {
106             setComponentMapper(defaultComponentMapper);
107         }
108     }
109 
110     private class MagnoliaComponentMapper implements Design.ComponentMapper {
111         private static final String FORM_TAG_PREFIX = "form-";
112         private Design.ComponentMapper defaultComponentMapper;
113         private Map<String, Component> componentsByName;
114 
115         MagnoliaComponentMapper(Design.ComponentMapper defaultComponentMapper, Map<EditorPropertyDefinition, Component> mappings) {
116             this.defaultComponentMapper = defaultComponentMapper;
117             this.componentsByName = new HashMap<>();
118             mappings.forEach((definition, component) -> addComponentMapping(componentsByName, definition.getName(), component));
119         }
120 
121         private void addComponentMapping(Map<String, Component> componentsByName, String componentName, Component component) {
122             String normalizedComponentName = componentName.toLowerCase();
123             if (componentsByName.containsKey(normalizedComponentName)) {
124                 throw new IllegalArgumentException(String.format("Only one component with the name %s is allowed in the declarative design.", normalizedComponentName));
125             }
126             componentsByName.put(normalizedComponentName, component);
127         }
128 
129         @Override
130         public Component tagToComponent(String tag, Design.ComponentFactory componentFactory, DesignContext context) {
131             if (context.getPackage("mgnl") == null) {
132                 context.addPackagePrefix("mgnl", "info.magnolia.ui.vaadin.form.custom");
133             }
134             if (!tag.startsWith(FORM_TAG_PREFIX)) {
135                 Component component = defaultComponentMapper.tagToComponent(tag, componentFactory, context);
136                 if (component instanceof HasValue) {
137                     context.addComponentCreationListener(event -> {
138                         String componentLocalId = context.getComponentLocalId(component);
139                         if (!binder.getBinding(componentLocalId).isPresent()) {
140                             binder.bind((HasValue<?>) component, componentLocalId);
141                         }
142                     });
143                 }
144                 return component;
145             }
146             String normalizedComponentName = tag.substring(FORM_TAG_PREFIX.length());
147             Component magnoliaComponent = componentsByName.get(normalizedComponentName);
148             if (magnoliaComponent == null) {
149                 throw new IllegalArgumentException(String.format("Cannot find magnolia component %s in the current mappings.", normalizedComponentName));
150             }
151             return magnoliaComponent;
152         }
153 
154         @Override
155         public String componentToTag(Component component, DesignContext context) {
156             Optional<String> optionalComponentName = getNameFromComponent(component);
157             return optionalComponentName.map(componentName -> FORM_TAG_PREFIX + componentName)
158                     .orElseGet(() -> defaultComponentMapper.componentToTag(component, context));
159         }
160 
161         private Optional<String> getNameFromComponent(Component component) {
162             return componentsByName.entrySet().stream()
163                     .filter(entry -> entry.getValue().equals(component))
164                     .map(Map.Entry::getKey)
165                     .findFirst();
166         }
167     }
168 }