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