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.ui.form.field.factory.LinkFieldFactory.*;
37 import static java.util.stream.Collectors.toList;
38 import static org.apache.commons.lang3.StringUtils.EMPTY;
39
40 import info.magnolia.cms.util.SimpleFreemarkerHelper;
41 import info.magnolia.config.maputil.ConfigurationMapOverlay;
42 import info.magnolia.config.maputil.ToMap;
43 import info.magnolia.config.registry.DefinitionProvider;
44 import info.magnolia.config.source.yaml.YamlReader;
45 import info.magnolia.config.source.yaml.construct.MgnlYamlConstruct;
46 import info.magnolia.config.source.yaml.dependency.YamlFileDependency;
47 import info.magnolia.jcr.util.NodeTypes;
48 import info.magnolia.resourceloader.Resource;
49 import info.magnolia.resourceloader.ResourceOrigin;
50 import info.magnolia.types.ContentTypeDefinition;
51 import info.magnolia.types.ContentTypeRegistry;
52 import info.magnolia.types.model.ConfiguredPropertyDefinition;
53 import info.magnolia.types.model.ModelDefinition;
54 import info.magnolia.types.model.PropertyDefinition;
55 import info.magnolia.types.model.SubModelDefinition;
56 import info.magnolia.types.model.jcr.ConfiguredJcrModelDefinition;
57 import info.magnolia.types.model.jcr.JcrModelDefinition;
58 import info.magnolia.ui.api.app.AppDescriptor;
59 import info.magnolia.ui.api.app.contenttypes.ContentTypeAppDescriptor;
60 import info.magnolia.ui.api.app.registry.AppDescriptorRegistry;
61 import info.magnolia.ui.vaadin.integration.jcr.ModelConstants;
62
63 import java.io.IOException;
64 import java.io.InputStream;
65 import java.io.Reader;
66 import java.io.StringReader;
67 import java.io.StringWriter;
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.regex.Matcher;
76 import java.util.regex.Pattern;
77
78 import org.apache.commons.collections4.CollectionUtils;
79 import org.apache.commons.io.IOUtils;
80 import org.apache.commons.lang3.StringUtils;
81 import org.slf4j.Logger;
82 import org.slf4j.LoggerFactory;
83 import org.yaml.snakeyaml.nodes.Node;
84
85 import com.google.common.collect.ImmutableMap;
86
87
88
89
90
91
92
93 public class AppWithContentType extends MgnlYamlConstruct {
94
95 public static final String TAG_PREFIX = "!with-type";
96
97 private static final Logger log = LoggerFactory.getLogger(AppWithContentType.class);
98 private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile("^!with-type:(?<type>.+)$");
99 private static final String APP_TEMPLATE_PATH = "/contenttypes/appTemplate.yaml";
100 private static final Map<String, String> FIELD_TYPE_MAPPING = ImmutableMap.of(
101 "date", "date",
102 "boolean", "checkbox");
103
104 private final ContentTypeRegistry contentTypeRegistry;
105 private final AppDescriptorRegistry appDescriptorRegistry;
106 private final SimpleFreemarkerHelper freemarkerHelper;
107 private final ResourceOrigin resourceOrigin;
108 private final YamlReader yamlReader;
109
110 private ContentTypeDefinition relatedContentType;
111 private Map<String, List<String>> dependencyMatrix;
112 private Map<String, Object> baseData;
113
114 private static final String NAME_FIELD_NAME = "name";
115
116 public AppWithContentType(ContentTypeRegistry contentTypeRegistry, AppDescriptorRegistry appDescriptorRegistry, YamlReader yamlReader, ResourceOrigin resourceOrigin) {
117 super(problem -> {});
118 this.contentTypeRegistry = contentTypeRegistry;
119 this.appDescriptorRegistry = appDescriptorRegistry;
120 this.yamlReader = yamlReader;
121 this.resourceOrigin = resourceOrigin;
122 this.freemarkerHelper = new SimpleFreemarkerHelper(getClass(), false);
123 }
124
125 @Override
126 public Object construct(Node node) {
127 baseData = ToMap.toMap(getConstructor().getConstructByNodeType(node).construct(node));
128
129 Matcher contentTypeMatcher = CONTENT_TYPE_PATTERN.matcher(node.getTag().getValue());
130 if (!contentTypeMatcher.matches()) {
131 throw new IllegalStateException("Content-type reference is passed incorrectly! " + node.getTag().getValue());
132 }
133
134 String contentTypeName = contentTypeMatcher.group("type");
135 Optional<DefinitionProvider<ContentTypeDefinition>> provider = getContentTypeDefinitionProvider(contentTypeName);
136 if (!provider.isPresent()) {
137 log.error("Content type {} is not found.", contentTypeName);
138 return Collections.emptyMap();
139 }
140
141
142 String path = provider.get().getMetadata().getLocation();
143 final YamlFileDependency dependency = new YamlFileDependency(resourceOrigin, path, yamlReader, getConstructor().getDependencyAggregator(), this::reportProblem);
144
145 if (!dependency.exists()) {
146 log.error("Resource dependency at [{}] does not exist.", path);
147 return Collections.emptyMap();
148 }
149
150 getConstructor().getDependencyAggregator().addDependency(dependency);
151
152 relatedContentType = provider.get().get();
153 dependencyMatrix = buildDependencyMatrix(relatedContentType.getModel());
154 Map<String, Object> data = new HashMap<>();
155 data.put("contentType", relatedContentType);
156 data.put("appfn", this);
157 Map<String, Object> contentApp = resolveYamlTemplate(APP_TEMPLATE_PATH, yamlReader, data);
158 return ConfigurationMapOverlay
159 .of(contentApp)
160 .by(baseData)
161 .at("/")
162 .overlay();
163 }
164
165 public String getSubAppLabel() {
166 return StringUtils.defaultIfEmpty((String) baseData.get("label"), getAppName(relatedContentType.getName()));
167 }
168
169 public PropertyDefinition getNameField() {
170 if (!(relatedContentType.getModel() instanceof ConfiguredJcrModelDefinition)) {
171 return null;
172 }
173 ConfiguredJcrModelDefinition model = (ConfiguredJcrModelDefinition) relatedContentType.getModel();
174 if (model.getProperties()
175 .stream()
176 .anyMatch(p -> p.getName().equals(NAME_FIELD_NAME) || p.getName().equals(ModelConstants.JCR_NAME))) {
177 return null;
178 }
179 ConfiguredPropertyDefinition nameProperty = new ConfiguredPropertyDefinition();
180 nameProperty.setName(NAME_FIELD_NAME);
181 nameProperty.setRequired(true);
182 return nameProperty;
183 }
184
185 public String getAppName(String contentType) {
186 if (StringUtils.equalsIgnoreCase(relatedContentType.getName(), contentType)) {
187 return StringUtils.defaultIfEmpty((String) baseData.get("name"), relatedContentType.getName());
188 }
189
190 return appDescriptorRegistry.getAllProviders().stream()
191 .filter(DefinitionProvider::isValid)
192 .map(DefinitionProvider::get)
193 .filter(ContentTypeAppDescriptor.class::isInstance)
194 .map(ContentTypeAppDescriptor.class::cast)
195 .filter(appDescriptor -> StringUtils.equalsIgnoreCase(contentType, appDescriptor.getContentType()))
196 .findFirst()
197 .map(AppDescriptor::getName)
198 .orElse(EMPTY);
199 }
200
201 public String getLinkFieldAppName(String contentType) {
202 if (StringUtils.equalsIgnoreCase(relatedContentType.getName(), contentType)) {
203 return StringUtils.defaultIfEmpty((String) baseData.get("name"), SELF_APP_REFERENCE);
204 }
205
206 return CONTENT_TYPE_REFERENCE_PREFIX + contentType;
207 }
208
209 public String getSubAppNodeType() {
210 if (relatedContentType.getModel() != null && relatedContentType.getModel() instanceof JcrModelDefinition) {
211 return StringUtils.defaultIfBlank(((JcrModelDefinition) relatedContentType.getModel()).getNodeType(), NodeTypes.Content.NAME);
212 }
213 return NodeTypes.Content.NAME;
214 }
215
216 public String inferFieldType(String propertyType) {
217 propertyType = propertyType.toLowerCase();
218 if (FIELD_TYPE_MAPPING.containsKey(propertyType)) {
219 return FIELD_TYPE_MAPPING.get(propertyType);
220 }
221 return "text";
222 }
223
224 public ModelDefinition lookupSubModelDefinition(String modelName) {
225 ModelDefinition modelDefinition = relatedContentType.getModel();
226 if (modelDefinition == null || CollectionUtils.isEmpty(modelDefinition.getSubModels())) {
227 return null;
228 }
229 return modelDefinition.getSubModels().stream()
230 .filter(modelDef -> modelDef.getName().equalsIgnoreCase(modelName))
231 .findFirst().orElse(null);
232 }
233
234 public boolean canResolveModel(String modelName) {
235 List<String> visited = new ArrayList<>();
236 if (isCyclicModelReference(dependencyMatrix, modelName.toLowerCase(), modelName.toLowerCase(), visited)) {
237 log.error("Detected cyclic dependencies path: {}", Arrays.toString(visited.toArray()));
238 return false;
239 }
240 return true;
241 }
242
243 public String getDefaultNodeTypeOfSubModel() {
244 return NodeTypes.ContentNode.NAME;
245 }
246
247 private Map<String, List<String>> buildDependencyMatrix(ModelDefinition modelDefinition) {
248 if (modelDefinition == null || CollectionUtils.isEmpty(modelDefinition.getSubModels())) {
249 return Collections.emptyMap();
250 }
251 Map<String, List<String>> dependencyMatrix = new HashMap<>();
252 for (SubModelDefinition subModel : modelDefinition.getSubModels()) {
253 if (!CollectionUtils.isEmpty(subModel.getProperties())) {
254 List<String> child = subModel.getProperties().stream()
255 .filter(property -> lookupSubModelDefinition(property.getType()) != null)
256 .map(property -> property.getType().toLowerCase())
257 .distinct()
258 .collect(toList());
259 dependencyMatrix.put(subModel.getName().toLowerCase(), child);
260 }
261 }
262 return dependencyMatrix;
263 }
264
265 private boolean isCyclicModelReference(Map<String, List<String>> matrix, String start, String model, List<String> visited) {
266 if (visited.contains(model) && StringUtils.equalsIgnoreCase(model, start)) {
267 return true;
268 }
269 visited.add(model);
270 for (String element : matrix.get(model)) {
271 if (isCyclicModelReference(matrix, start, element, visited)) {
272 return true;
273 }
274 }
275 visited.remove(model);
276 return false;
277 }
278
279 public Optional<DefinitionProvider<ContentTypeDefinition>> getContentTypeDefinitionProvider(String contentTypeName) {
280 return contentTypeRegistry.getAllProviders()
281 .stream()
282 .filter(provider -> StringUtils.equalsIgnoreCase(provider.get().getName(), contentTypeName))
283 .findFirst();
284 }
285
286 private Map<String, Object> resolveYamlTemplate(String templatePath, YamlReader yamlReader, Map<String, Object> data) {
287 try {
288 StringWriter writer = new StringWriter();
289 freemarkerHelper.render(templatePath, data, writer);
290 return yamlReader.readToMap(new InMemoryFileResource(writer.toString()));
291 } catch (Exception ex) {
292 log.error("Can't resolve content-type YAML template: {}", ex.getMessage());
293 return Collections.emptyMap();
294 }
295 }
296
297 private class InMemoryFileResource implements Resource {
298 private String content;
299
300 InMemoryFileResource(String content) {
301 this.content = content;
302 }
303
304 @Override
305 public ResourceOrigin getOrigin() {
306 return null;
307 }
308
309 @Override
310 public boolean isFile() {
311 return true;
312 }
313
314 @Override
315 public boolean isDirectory() {
316 return false;
317 }
318
319 @Override
320 public boolean isEditable() {
321 return false;
322 }
323
324 @Override
325 public String getPath() {
326 return null;
327 }
328
329 @Override
330 public String getName() {
331 return null;
332 }
333
334 @Override
335 public long getLastModified() {
336 return 0;
337 }
338
339 @Override
340 public Resource getParent() {
341 return null;
342 }
343
344 @Override
345 public List<Resource> listChildren() {
346 return null;
347 }
348
349 @Override
350 public InputStream openStream() throws IOException {
351 return IOUtils.toInputStream(content, "UTF-8");
352 }
353
354 @Override
355 public Reader openReader() throws IOException {
356 return new StringReader(content);
357 }
358 }
359 }