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