View Javadoc
1   /**
2    * This file Copyright (c) 2018 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.app.contenttypes;
35  
36  import static info.magnolia.ui.form.field.factory.LinkFieldFactory.*;
37  import static java.util.stream.Collectors.toList;
38  import static org.apache.commons.lang3.StringUtils.EMPTY;
39  
40  import info.magnolia.cms.util.SimpleFreemarkerHelper;
41  import info.magnolia.config.maputil.ConfigurationMapOverlay;
42  import info.magnolia.config.maputil.ToMap;
43  import info.magnolia.config.registry.DefinitionProvider;
44  import info.magnolia.config.source.yaml.YamlReader;
45  import info.magnolia.config.source.yaml.construct.MgnlYamlConstruct;
46  import info.magnolia.config.source.yaml.dependency.YamlFileDependency;
47  import info.magnolia.jcr.util.NodeTypes;
48  import info.magnolia.resourceloader.Resource;
49  import info.magnolia.resourceloader.ResourceOrigin;
50  import info.magnolia.types.ContentTypeDefinition;
51  import info.magnolia.types.ContentTypeRegistry;
52  import info.magnolia.types.model.ConfiguredPropertyDefinition;
53  import info.magnolia.types.model.ModelDefinition;
54  import info.magnolia.types.model.PropertyDefinition;
55  import info.magnolia.types.model.SubModelDefinition;
56  import info.magnolia.types.model.jcr.ConfiguredJcrModelDefinition;
57  import info.magnolia.types.model.jcr.JcrModelDefinition;
58  import info.magnolia.ui.api.app.AppDescriptor;
59  import info.magnolia.ui.api.app.contenttypes.ContentTypeAppDescriptor;
60  import info.magnolia.ui.api.app.registry.AppDescriptorRegistry;
61  import info.magnolia.ui.vaadin.integration.jcr.ModelConstants;
62  
63  import java.io.IOException;
64  import java.io.InputStream;
65  import java.io.Reader;
66  import java.io.StringReader;
67  import java.io.StringWriter;
68  import java.util.ArrayList;
69  import java.util.Arrays;
70  import java.util.Collections;
71  import java.util.HashMap;
72  import java.util.List;
73  import java.util.Map;
74  import java.util.Optional;
75  import java.util.regex.Matcher;
76  import java.util.regex.Pattern;
77  
78  import org.apache.commons.collections4.CollectionUtils;
79  import org.apache.commons.io.IOUtils;
80  import org.apache.commons.lang3.StringUtils;
81  import org.slf4j.Logger;
82  import org.slf4j.LoggerFactory;
83  import org.yaml.snakeyaml.nodes.Node;
84  
85  import com.google.common.collect.ImmutableMap;
86  
87  /**
88   * Snake YAML construct which takes over the YAML processing of
89   * a content app and injects the boilerplate configuration resolved
90   * from a content type definition and templates. Bound to the
91   * tag <strong>!with-type:{type-name}</strong>.
92   */
93  public class AppWithContentType extends MgnlYamlConstruct {
94  
95      public static final String TAG_PREFIX = "!with-type";
96  
97      private static final Logger log = LoggerFactory.getLogger(AppWithContentType.class);
98      private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile("^!with-type:(?<type>.+)$");
99      private static final String APP_TEMPLATE_PATH = "/contenttypes/appTemplate.yaml";
100     private static final Map<String, String> FIELD_TYPE_MAPPING = ImmutableMap.of(
101             "date", "date",
102             "boolean", "checkbox");
103 
104     private final ContentTypeRegistry contentTypeRegistry;
105     private final AppDescriptorRegistry appDescriptorRegistry;
106     private final SimpleFreemarkerHelper freemarkerHelper;
107     private final ResourceOrigin resourceOrigin;
108     private final YamlReader yamlReader;
109 
110     private ContentTypeDefinition relatedContentType;
111     private Map<String, List<String>> dependencyMatrix;
112     private Map<String, Object> baseData;
113 
114     private static final String NAME_FIELD_NAME = "name";
115 
116     public AppWithContentType(ContentTypeRegistry contentTypeRegistry, AppDescriptorRegistry appDescriptorRegistry, YamlReader yamlReader, ResourceOrigin resourceOrigin) {
117         super(problem -> {});
118         this.contentTypeRegistry = contentTypeRegistry;
119         this.appDescriptorRegistry = appDescriptorRegistry;
120         this.yamlReader = yamlReader;
121         this.resourceOrigin = resourceOrigin;
122         this.freemarkerHelper = new SimpleFreemarkerHelper(getClass(), false);
123     }
124 
125     @Override
126     public Object construct(Node node) {
127         baseData = ToMap.toMap(getConstructor().getConstructByNodeType(node).construct(node));
128 
129         Matcher contentTypeMatcher = CONTENT_TYPE_PATTERN.matcher(node.getTag().getValue());
130         if (!contentTypeMatcher.matches()) {
131             throw new IllegalStateException("Content-type reference is passed incorrectly! " + node.getTag().getValue());
132         }
133 
134         String contentTypeName = contentTypeMatcher.group("type");
135         Optional<DefinitionProvider<ContentTypeDefinition>> provider = getContentTypeDefinitionProvider(contentTypeName);
136         if (!provider.isPresent()) {
137             log.error("Content type {} is not found.", contentTypeName);
138             return Collections.emptyMap();
139         }
140 
141         // Add CT dependency to app
142         String path = provider.get().getMetadata().getLocation();
143         final YamlFileDependency dependency = new YamlFileDependency(resourceOrigin, path, yamlReader, getConstructor().getDependencyAggregator(), this::reportProblem);
144 
145         if (!dependency.exists()) {
146             log.error("Resource dependency at [{}] does not exist.", path);
147             return Collections.emptyMap();
148         }
149 
150         getConstructor().getDependencyAggregator().addDependency(dependency);
151 
152         relatedContentType = provider.get().get();
153         dependencyMatrix = buildDependencyMatrix(relatedContentType.getModel());
154         Map<String, Object> data = new HashMap<>();
155         data.put("contentType", relatedContentType);
156         data.put("appfn", this);
157         Map<String, Object> contentApp = resolveYamlTemplate(APP_TEMPLATE_PATH, yamlReader, data);
158         return ConfigurationMapOverlay
159                 .of(contentApp)
160                 .by(baseData)
161                 .at("/")
162                 .overlay();
163     }
164 
165     public String getSubAppLabel() {
166         return StringUtils.defaultIfEmpty((String) baseData.get("label"), getAppName(relatedContentType.getName()));
167     }
168 
169     public PropertyDefinition getNameField() {
170         if (!(relatedContentType.getModel() instanceof ConfiguredJcrModelDefinition)) {
171             return null;
172         }
173         ConfiguredJcrModelDefinition model = (ConfiguredJcrModelDefinition) relatedContentType.getModel();
174         if (model.getProperties()
175                 .stream()
176                 .anyMatch(p -> p.getName().equals(NAME_FIELD_NAME) || p.getName().equals(ModelConstants.JCR_NAME))) {
177             return null;
178         }
179         ConfiguredPropertyDefinition nameProperty = new ConfiguredPropertyDefinition();
180         nameProperty.setName(NAME_FIELD_NAME);
181         nameProperty.setRequired(true);
182         return nameProperty;
183     }
184 
185     public String getAppName(String contentType) {
186         if (StringUtils.equalsIgnoreCase(relatedContentType.getName(), contentType)) {
187             return StringUtils.defaultIfEmpty((String) baseData.get("name"), relatedContentType.getName());
188         }
189 
190         return appDescriptorRegistry.getAllProviders().stream()
191                 .filter(DefinitionProvider::isValid)
192                 .map(DefinitionProvider::get)
193                 .filter(ContentTypeAppDescriptor.class::isInstance)
194                 .map(ContentTypeAppDescriptor.class::cast)
195                 .filter(appDescriptor -> StringUtils.equalsIgnoreCase(contentType, appDescriptor.getContentType()))
196                 .findFirst()
197                 .map(AppDescriptor::getName)
198                 .orElse(EMPTY);
199     }
200 
201     public String getLinkFieldAppName(String contentType) {
202         if (StringUtils.equalsIgnoreCase(relatedContentType.getName(), contentType)) {
203             return StringUtils.defaultIfEmpty((String) baseData.get("name"), SELF_APP_REFERENCE);
204         }
205         // compute a lazy reference to the content-type, let LinkFieldFactory resolve actual app name at runtime
206         return CONTENT_TYPE_REFERENCE_PREFIX + contentType;
207     }
208 
209     public String getSubAppNodeType() {
210         if (relatedContentType.getModel() != null && relatedContentType.getModel() instanceof JcrModelDefinition) {
211             return StringUtils.defaultIfBlank(((JcrModelDefinition) relatedContentType.getModel()).getNodeType(), NodeTypes.Content.NAME);
212         }
213         return NodeTypes.Content.NAME;
214     }
215 
216     public String inferFieldType(String propertyType) {
217         propertyType = propertyType.toLowerCase();
218         if (FIELD_TYPE_MAPPING.containsKey(propertyType)) {
219             return FIELD_TYPE_MAPPING.get(propertyType);
220         }
221         return "text";
222     }
223 
224     public ModelDefinition lookupSubModelDefinition(String modelName) {
225         ModelDefinition modelDefinition = relatedContentType.getModel();
226         if (modelDefinition == null || CollectionUtils.isEmpty(modelDefinition.getSubModels())) {
227             return null;
228         }
229         return modelDefinition.getSubModels().stream()
230                 .filter(modelDef -> modelDef.getName().equalsIgnoreCase(modelName))
231                 .findFirst().orElse(null);
232     }
233 
234     public boolean canResolveModel(String modelName) {
235         List<String> visited = new ArrayList<>();
236         if (isCyclicModelReference(dependencyMatrix, modelName.toLowerCase(), modelName.toLowerCase(), visited)) {
237             log.error("Detected cyclic dependencies path: {}", Arrays.toString(visited.toArray()));
238             return false;
239         }
240         return true;
241     }
242 
243     public String getDefaultNodeTypeOfSubModel() {
244         return NodeTypes.ContentNode.NAME;
245     }
246 
247     private Map<String, List<String>> buildDependencyMatrix(ModelDefinition modelDefinition) {
248         if (modelDefinition == null || CollectionUtils.isEmpty(modelDefinition.getSubModels())) {
249             return Collections.emptyMap();
250         }
251         Map<String, List<String>> dependencyMatrix = new HashMap<>();
252         for (SubModelDefinition subModel : modelDefinition.getSubModels()) {
253             if (!CollectionUtils.isEmpty(subModel.getProperties())) {
254                 List<String> child = subModel.getProperties().stream()
255                         .filter(property -> lookupSubModelDefinition(property.getType()) != null)
256                         .map(property -> property.getType().toLowerCase())
257                         .distinct()
258                         .collect(toList());
259                 dependencyMatrix.put(subModel.getName().toLowerCase(), child);
260             }
261         }
262         return dependencyMatrix;
263     }
264 
265     private boolean isCyclicModelReference(Map<String, List<String>> matrix, String start, String model, List<String> visited) {
266         if (visited.contains(model) && StringUtils.equalsIgnoreCase(model, start)) {
267             return true;
268         }
269         visited.add(model);
270         for (String element : matrix.get(model)) {
271             if (isCyclicModelReference(matrix, start, element, visited)) {
272                 return true;
273             }
274         }
275         visited.remove(model);
276         return false;
277     }
278 
279     public Optional<DefinitionProvider<ContentTypeDefinition>> getContentTypeDefinitionProvider(String contentTypeName) {
280         return contentTypeRegistry.getAllProviders()
281                 .stream()
282                 .filter(provider -> StringUtils.equalsIgnoreCase(provider.get().getName(), contentTypeName))
283                 .findFirst();
284     }
285 
286     private Map<String, Object> resolveYamlTemplate(String templatePath, YamlReader yamlReader, Map<String, Object> data) {
287         try {
288             StringWriter writer = new StringWriter();
289             freemarkerHelper.render(templatePath, data, writer);
290             return yamlReader.readToMap(new InMemoryFileResource(writer.toString()));
291         } catch (Exception ex) {
292             log.error("Can't resolve content-type YAML template: {}", ex.getMessage());
293             return Collections.emptyMap();
294         }
295     }
296 
297     private class InMemoryFileResource implements Resource {
298         private String content;
299 
300         InMemoryFileResource(String content) {
301             this.content = content;
302         }
303 
304         @Override
305         public ResourceOrigin getOrigin() {
306             return null;
307         }
308 
309         @Override
310         public boolean isFile() {
311             return true;
312         }
313 
314         @Override
315         public boolean isDirectory() {
316             return false;
317         }
318 
319         @Override
320         public boolean isEditable() {
321             return false;
322         }
323 
324         @Override
325         public String getPath() {
326             return null;
327         }
328 
329         @Override
330         public String getName() {
331             return null;
332         }
333 
334         @Override
335         public long getLastModified() {
336             return 0;
337         }
338 
339         @Override
340         public Resource getParent() {
341             return null;
342         }
343 
344         @Override
345         public List<Resource> listChildren() {
346             return null;
347         }
348 
349         @Override
350         public InputStream openStream() throws IOException {
351             return IOUtils.toInputStream(content, "UTF-8");
352         }
353 
354         @Override
355         public Reader openReader() throws IOException {
356             return new StringReader(content);
357         }
358     }
359 }