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