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