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