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  
41  import java.io.ByteArrayInputStream;
42  import java.io.InputStream;
43  import java.io.Reader;
44  import java.io.StringWriter;
45  import java.nio.charset.StandardCharsets;
46  import java.util.HashMap;
47  import java.util.Map;
48  import java.util.Optional;
49  
50  import javax.inject.Inject;
51  
52  import org.slf4j.Logger;
53  import org.slf4j.LoggerFactory;
54  
55  import com.vaadin.data.Binder;
56  import com.vaadin.data.HasValue;
57  import com.vaadin.ui.Component;
58  import com.vaadin.ui.Label;
59  import com.vaadin.ui.Layout;
60  import com.vaadin.ui.VerticalLayout;
61  import com.vaadin.ui.declarative.Design;
62  import com.vaadin.ui.declarative.DesignContext;
63  
64  /**
65   * DeclarativeLayoutProducer is able to produce a layout that is a mixture of vaadin components and magnolia form fields.
66   * 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.
67   * The declarative syntax is defined at https://vaadin.com/docs/v8/framework/application/application-declarative.html
68   *
69   * @see DeclarativeLayoutDefinition
70   */
71  public class DeclarativeLayoutProducer implements LayoutProducer<DeclarativeLayoutDefinition> {
72  
73      private static final Logger log = LoggerFactory.getLogger(DeclarativeLayoutProducer.class);
74      private FreemarkerHelper freemarkerHelper;
75      private ResourceOrigin resourceOrigin;
76      private final Binder binder;
77  
78      @Inject
79      public DeclarativeLayoutProducer(FreemarkerHelper freemarkerHelper, ResourceOrigin resourceOrigin, Binder binder) {
80          this.freemarkerHelper = freemarkerHelper;
81          this.resourceOrigin = resourceOrigin;
82          this.binder = binder;
83      }
84  
85      @Override
86      public synchronized Layout createLayout(DeclarativeLayoutDefinition definition, Map<String, Component> mappings) {
87          Design.ComponentMapper defaultComponentMapper = getComponentMapper();
88          try (final Reader templateReader = resourceOrigin.getByPath(definition.getTemplate()).openReader()) {
89              StringWriter ftlEnhancedTemplateWriter = new StringWriter();
90              freemarkerHelper.render(templateReader, new HashMap<>(), ftlEnhancedTemplateWriter);
91              InputStream enhancedTemplateInputStream = new ByteArrayInputStream(ftlEnhancedTemplateWriter.toString().getBytes(StandardCharsets.UTF_8));
92              setComponentMapper(new MagnoliaComponentMapper(defaultComponentMapper, mappings));
93              Layout layout = (Layout) Design.read(enhancedTemplateInputStream);
94              layout.setStyleName("declarative-layout");
95              return layout;
96          } catch (Exception e) {
97              log.error(String.format("Error while processing the template %s.", definition.getTemplate()), e);
98              Label label = new Label(String.format("%s: %s", e.getClass().getName(), e.getMessage()));
99              label.setStyleName("error-message");
100             Layout errorLayout = new VerticalLayout(label);
101             errorLayout.setStyleName("declarative-layout");
102             return errorLayout;
103         } finally {
104             setComponentMapper(defaultComponentMapper);
105         }
106     }
107 
108     private class MagnoliaComponentMapper implements Design.ComponentMapper {
109         private static final String FORM_TAG_PREFIX = "form-";
110         private Design.ComponentMapper defaultComponentMapper;
111         private Map<String, Component> componentsByName;
112 
113         MagnoliaComponentMapper(Design.ComponentMapper defaultComponentMapper, Map<String, Component> mappings) {
114             this.defaultComponentMapper = defaultComponentMapper;
115             this.componentsByName = new HashMap<>();
116             mappings.forEach((name, component) -> addComponentMapping(componentsByName, name, component));
117         }
118 
119         private void addComponentMapping(Map<String, Component> componentsByName, String componentName, Component component) {
120             String normalizedComponentName = componentName.toLowerCase();
121             if (componentsByName.containsKey(normalizedComponentName)) {
122                 throw new IllegalArgumentException(String.format("Only one component with the name %s is allowed in the declarative design.", normalizedComponentName));
123             }
124             componentsByName.put(normalizedComponentName, component);
125         }
126 
127         @Override
128         public Component tagToComponent(String tag, Design.ComponentFactory componentFactory, DesignContext context) {
129             if (context.getPackage("mgnl") == null) {
130                 context.addPackagePrefix("mgnl", "info.magnolia.ui.vaadin.form.custom");
131             }
132             if (!tag.startsWith(FORM_TAG_PREFIX)) {
133                 Component component = defaultComponentMapper.tagToComponent(tag, componentFactory, context);
134                 if (component instanceof HasValue) {
135                     context.addComponentCreationListener(event -> {
136                         String componentLocalId = context.getComponentLocalId(component);
137                         if (!binder.getBinding(componentLocalId).isPresent()) {
138                             binder.bind((HasValue<?>) component, componentLocalId);
139                         }
140                     });
141                 }
142                 return component;
143             }
144             String normalizedComponentName = tag.substring(FORM_TAG_PREFIX.length());
145             Component magnoliaComponent = componentsByName.get(normalizedComponentName);
146             if (magnoliaComponent == null) {
147                 throw new IllegalArgumentException(String.format("Cannot find magnolia component %s in the current mappings.", normalizedComponentName));
148             }
149             return magnoliaComponent;
150         }
151 
152         @Override
153         public String componentToTag(Component component, DesignContext context) {
154             Optional<String> optionalComponentName = getNameFromComponent(component);
155             return optionalComponentName.map(componentName -> FORM_TAG_PREFIX + componentName)
156                     .orElseGet(() -> defaultComponentMapper.componentToTag(component, context));
157         }
158 
159         private Optional<String> getNameFromComponent(Component component) {
160             return componentsByName.entrySet().stream()
161                     .filter(entry -> entry.getValue().equals(component))
162                     .map(Map.Entry::getKey)
163                     .findFirst();
164         }
165     }
166 }