1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
93
94
95
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
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
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
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 }