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
98
99
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
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
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
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 }