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 = "name";
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     //FIXME: Try to abstract content type, should not depend on JCR implementation
190     public PropertyDefinition getNameField() {
191         if (!(relatedContentType.getModel() instanceof ConfiguredJcrModelDefinition)) {
192             return null;
193         }
194         ConfiguredJcrModelDefinition model = (ConfiguredJcrModelDefinition) relatedContentType.getModel();
195         if (model.getProperties()
196                 .stream()
197                 .anyMatch(p -> p.getName().equals(NAME_FIELD_NAME))) {
198             return null;
199         }
200         ConfiguredPropertyDefinition nameProperty = new ConfiguredPropertyDefinition();
201         nameProperty.setName(NAME_FIELD_NAME);
202         nameProperty.setRequired(true);
203         return nameProperty;
204     }
205 
206     public String getAppName(String contentType) {
207         if (StringUtils.equalsIgnoreCase(relatedContentType.getName(), contentType)) {
208             return StringUtils.defaultIfEmpty((String) baseData.get("name"), relatedContentType.getName());
209         }
210 
211         return appDescriptorRegistry.getAllProviders().stream()
212                 .filter(DefinitionProvider::isValid)
213                 .map(DefinitionProvider::get)
214                 .filter(ContentTypeAppDescriptor.class::isInstance)
215                 .map(ContentTypeAppDescriptor.class::cast)
216                 .filter(appDescriptor -> StringUtils.equalsIgnoreCase(contentType, appDescriptor.getContentType()))
217                 .findFirst()
218                 .map(AppDescriptor::getName)
219                 .orElse(EMPTY);
220     }
221 
222     public String getLinkFieldAppName(String ref) {
223         String contentType = StringUtils.removeStart(ref, CONTENT_TYPE_REFERENCE_PREFIX);
224         if (StringUtils.equalsIgnoreCase(relatedContentType.getName(), contentType)) {
225             return StringUtils.defaultIfEmpty((String) baseData.get("name"), SELF_APP_REFERENCE);
226         }
227         // compute a lazy reference to the content-type, let LinkFieldFactory resolve actual app name at runtime
228         return CONTENT_TYPE_REFERENCE_PREFIX + contentType;
229     }
230 
231     public String getSubAppNodeType() {
232         if (relatedContentType.getModel() != null && relatedContentType.getModel() instanceof JcrModelDefinition) {
233             return StringUtils.defaultIfBlank(((JcrModelDefinition) relatedContentType.getModel()).getNodeType(), NodeTypes.Content.NAME);
234         }
235         return NodeTypes.Content.NAME;
236     }
237 
238     public String inferFieldType(String propertyType) {
239         propertyType = propertyType.toLowerCase();
240         if (FIELD_TYPE_MAPPING.containsKey(propertyType)) {
241             return FIELD_TYPE_MAPPING.get(propertyType);
242         }
243         return "textField";
244     }
245 
246     public String convertFieldType(String propertyType) {
247         if (propertyType.equalsIgnoreCase("Decimal")) {
248             return BigDecimal.class.getName();
249         } else if (propertyType.equalsIgnoreCase("Double")) {
250             return Double.class.getName();
251         } else if (propertyType.equalsIgnoreCase("Long")) {
252             return Long.class.getName();
253         }
254         return String.class.getName();
255     }
256 
257     public Optional<String> getConverterClassForPropertyType(String propertyType) {
258         if (propertyType.equalsIgnoreCase("Decimal")) {
259             return Optional.of(StringToBigDecimalConverter.class.getName());
260         } else if (propertyType.equalsIgnoreCase("Double")) {
261             return Optional.of(StringToDoubleConverter.class.getName());
262         } else if (propertyType.equalsIgnoreCase("Long")) {
263             return Optional.of(StringToLongConverter.class.getName());
264         }
265         return Optional.empty();
266     }
267 
268     public ModelDefinition lookupSubModelDefinition(String modelName) {
269         ModelDefinition modelDefinition = relatedContentType.getModel();
270         if (modelDefinition == null || CollectionUtils.isEmpty(modelDefinition.getSubModels())) {
271             return null;
272         }
273         return modelDefinition.getSubModels().stream()
274                 .filter(modelDef -> modelDef.getName().equalsIgnoreCase(modelName))
275                 .findFirst().orElse(null);
276     }
277 
278     public boolean canResolveModel(String modelName) {
279         List<String> visited = new ArrayList<>();
280         if (isCyclicModelReference(dependencyMatrix, modelName.toLowerCase(), modelName.toLowerCase(), visited)) {
281             log.error("Detected cyclic dependencies path: {}", Arrays.toString(visited.toArray()));
282             return false;
283         }
284         return true;
285     }
286 
287     public String getDefaultNodeTypeOfSubModel() {
288         return NodeTypes.ContentNode.NAME;
289     }
290 
291     private Map<String, List<String>> buildDependencyMatrix(ModelDefinition modelDefinition) {
292         if (modelDefinition == null || CollectionUtils.isEmpty(modelDefinition.getSubModels())) {
293             return Collections.emptyMap();
294         }
295         Map<String, List<String>> dependencyMatrix = new HashMap<>();
296         for (SubModelDefinition subModel : modelDefinition.getSubModels()) {
297             if (!CollectionUtils.isEmpty(subModel.getProperties())) {
298                 List<String> child = subModel.getProperties().stream()
299                         .filter(property -> lookupSubModelDefinition(property.getType()) != null)
300                         .map(property -> property.getType().toLowerCase())
301                         .distinct()
302                         .collect(toList());
303                 dependencyMatrix.put(subModel.getName().toLowerCase(), child);
304             }
305         }
306         return dependencyMatrix;
307     }
308 
309     private boolean isCyclicModelReference(Map<String, List<String>> matrix, String start, String model, List<String> visited) {
310         if (visited.contains(model) && StringUtils.equalsIgnoreCase(model, start)) {
311             return true;
312         }
313         visited.add(model);
314         for (String element : matrix.get(model)) {
315             if (isCyclicModelReference(matrix, start, element, visited)) {
316                 return true;
317             }
318         }
319         visited.remove(model);
320         return false;
321     }
322 
323     public Optional<DefinitionProvider<ContentTypeDefinition>> getContentTypeDefinitionProvider(String contentTypeName) {
324         return contentTypeRegistry.getAllProviders()
325                 .stream()
326                 .filter(provider -> provider.isValid() && StringUtils.equalsIgnoreCase(provider.get().getName(), contentTypeName))
327                 .findFirst();
328     }
329 
330     public Optional<DefinitionProvider<ContentTypeDefinition>> resolveReferenceContentTypeDefinitionProvider(String refContentType) {
331         return getContentTypeDefinitionProvider(StringUtils.removeStart(refContentType, CONTENT_TYPE_REFERENCE_PREFIX));
332     }
333 
334     private Map<String, Object> resolveYamlTemplate(String templatePath, YamlReader yamlReader, Map<String, Object> data) {
335         try {
336             StringWriter writer = new StringWriter();
337             freemarkerHelper.render(templatePath, data, writer);
338             return yamlReader.readToMap(new InMemoryFileResource(writer.toString()));
339         } catch (Exception ex) {
340             log.error("Can't resolve content-type YAML template: {}", ex.getMessage());
341             return Collections.emptyMap();
342         }
343     }
344 
345     private class InMemoryFileResource implements Resource {
346         private String content;
347 
348         InMemoryFileResource(String content) {
349             this.content = content;
350         }
351 
352         @Override
353         public ResourceOrigin getOrigin() {
354             return null;
355         }
356 
357         @Override
358         public boolean isFile() {
359             return true;
360         }
361 
362         @Override
363         public boolean isDirectory() {
364             return false;
365         }
366 
367         @Override
368         public boolean isEditable() {
369             return false;
370         }
371 
372         @Override
373         public String getPath() {
374             return null;
375         }
376 
377         @Override
378         public String getName() {
379             return null;
380         }
381 
382         @Override
383         public long getLastModified() {
384             return 0;
385         }
386 
387         @Override
388         public Resource getParent() {
389             return null;
390         }
391 
392         @Override
393         public List<Resource> listChildren() {
394             return null;
395         }
396 
397         @Override
398         public InputStream openStream() throws IOException {
399             return IOUtils.toInputStream(content, "UTF-8");
400         }
401 
402         @Override
403         public Reader openReader() throws IOException {
404             return new StringReader(content);
405         }
406     }
407 }