001/* 002 * Copyright 2010 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.cms.contenttype; 017 018import java.io.File; 019import java.io.FileInputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.Collections; 025import java.util.HashMap; 026import java.util.HashSet; 027import java.util.LinkedHashMap; 028import java.util.List; 029import java.util.Map; 030import java.util.Optional; 031import java.util.Set; 032import java.util.regex.Pattern; 033import java.util.stream.Collectors; 034 035import org.apache.avalon.framework.activity.Disposable; 036import org.apache.avalon.framework.component.ComponentException; 037import org.apache.avalon.framework.configuration.Configuration; 038import org.apache.avalon.framework.configuration.ConfigurationException; 039import org.apache.avalon.framework.configuration.DefaultConfiguration; 040import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 041import org.apache.avalon.framework.context.Context; 042import org.apache.avalon.framework.context.ContextException; 043import org.apache.avalon.framework.context.Contextualizable; 044import org.apache.avalon.framework.service.ServiceException; 045import org.apache.avalon.framework.service.ServiceManager; 046import org.apache.avalon.framework.thread.ThreadSafe; 047import org.apache.cocoon.Constants; 048import org.apache.cocoon.components.LifecycleHelper; 049import org.apache.cocoon.util.log.SLF4JLoggerAdapter; 050import org.apache.commons.lang3.RandomStringUtils; 051import org.apache.commons.lang3.StringUtils; 052import org.apache.excalibur.source.Source; 053import org.xml.sax.SAXException; 054 055import org.ametys.cms.content.references.RichTextOutgoingReferencesExtractor; 056import org.ametys.cms.content.referencetable.HierarchicalReferenceTablesHelper; 057import org.ametys.cms.contenttype.indexing.CustomIndexingField; 058import org.ametys.cms.contenttype.indexing.CustomMetadataIndexingField; 059import org.ametys.cms.contenttype.indexing.DefaultMetadataIndexingField; 060import org.ametys.cms.contenttype.indexing.IndexingField; 061import org.ametys.cms.contenttype.indexing.IndexingModel; 062import org.ametys.cms.contenttype.indexing.MetadataIndexingField; 063import org.ametys.cms.contenttype.indexing.SemanticAnnotationIndexingField; 064import org.ametys.cms.data.type.ModelItemTypeConstants; 065import org.ametys.cms.model.ContentRestrictedCompositeDefinition; 066import org.ametys.cms.model.ContentRestrictedRepeaterDefinition; 067import org.ametys.cms.model.parsing.ContentRestrictedCompositeDefinitionParser; 068import org.ametys.cms.model.parsing.ContentRestrictedRepeaterDefinitionParser; 069import org.ametys.cms.model.restrictions.ContentRestrictedModelItemHelper; 070import org.ametys.cms.model.restrictions.ContentRestrictedModelItemHelper.FirstRestrictionsChecksState; 071import org.ametys.cms.model.restrictions.RestrictedModelItem; 072import org.ametys.cms.model.restrictions.Restrictions; 073import org.ametys.cms.repository.Content; 074import org.ametys.cms.repository.ContentAttributeTypeExtensionPoint; 075import org.ametys.cms.transformation.RichTextTransformer; 076import org.ametys.cms.transformation.docbook.DocbookOutgoingReferencesExtractor; 077import org.ametys.cms.transformation.docbook.DocbookTransformer; 078import org.ametys.plugins.repository.AmetysRepositoryException; 079import org.ametys.runtime.i18n.I18nizableText; 080import org.ametys.runtime.model.ElementDefinition; 081import org.ametys.runtime.model.Enumerator; 082import org.ametys.runtime.model.ModelItem; 083import org.ametys.runtime.model.ModelItemContainer; 084import org.ametys.runtime.model.ModelItemGroup; 085import org.ametys.runtime.model.ModelViewItem; 086import org.ametys.runtime.model.ModelViewItemGroup; 087import org.ametys.runtime.model.SimpleViewItemGroup; 088import org.ametys.runtime.model.TemporaryViewReference; 089import org.ametys.runtime.model.View; 090import org.ametys.runtime.model.ViewElement; 091import org.ametys.runtime.model.ViewItem; 092import org.ametys.runtime.model.ViewItemAccessor; 093import org.ametys.runtime.model.ViewItemContainer; 094import org.ametys.runtime.model.ViewParser; 095import org.ametys.runtime.model.exception.UndefinedItemPathException; 096import org.ametys.runtime.model.type.ElementType; 097import org.ametys.runtime.parameter.AbstractParameterParser; 098import org.ametys.runtime.parameter.StaticEnumerator; 099import org.ametys.runtime.parameter.Validator; 100import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 101 102import com.google.common.collect.HashMultimap; 103import com.google.common.collect.Multimap; 104 105/** 106 * Type of content which is retrieved from a XML configuration. 107 * TODO document xml configuration 108 * ... 109 * Provides access based on rights and current workflow steps.<p> 110 * It used a configuration file with the following format: 111 * <code><br> 112 * <restrict-to><br> 113 * [<right type="read|write" id="RIGHT_ID"/>]* 114 * <!-- logical OR between several right id of the same type --><br> 115 * [<workflow type="read|write" step="3"/>]* 116 * <!-- logical OR between several workflow step of the same type --><br> 117 * [<cannot type="read|write"/>]*<br> 118 * </restrict-to><br> 119 * </code> 120 */ 121public class DefaultContentType extends AbstractContentTypeDescriptor implements ContentType, Contextualizable, ThreadSafe, Disposable 122{ 123 /** Suffix for global validator role. */ 124 protected static final String __GLOBAL_VALIDATOR_ROLE_PREFIX = "_globalvalidator"; 125 126 static Pattern __annotationNamePattern; 127 128 private static final String __VIEW_TAG_NAME_WITH_NEW_ATTRIBUTE_API_SYNTAX = "view"; 129 private static final String __VIEW_TAG_NAME_WITH_OLD_METADATA_API_SYNTAX = "metadata-set"; 130 private static final String __GROUP_TAG_NAME_WITH_OLD_METADATA_API_SYNTAX = "fieldset"; 131 private static final String __ATTRIBUTE_REF_TAG_NAME_WITH_OLD_METADATA_API_SYNTAX = "metadata-ref"; 132 133 /** Metadata definitions. */ 134 @Deprecated 135 protected Map<String, MetadataDefinition> _metadata = new LinkedHashMap<>(); 136 /** Model items */ 137 protected Map<String, ModelItem> _modelItems = new LinkedHashMap<>(); 138 /** The right needed to create a content of this type, or null if no right is needed. */ 139 protected String _right; 140 /** The abstract property */ 141 protected boolean _abstract; 142 /** Default workflow name. */ 143 protected Optional<String> _defaultWorkflowName; 144 /** The tags */ 145 protected Set<String> _tags; 146 /** The parent attribute definition */ 147 protected ContentAttributeDefinition _parentAttributeDefinition; 148 /** Service manager. */ 149 protected ServiceManager _manager; 150 /** Avalon Context. */ 151 protected Context _context; 152 /** Cocoon Context */ 153 protected org.apache.cocoon.environment.Context _cocoonContext; 154 /** The restrictions helper */ 155 protected ContentRestrictedModelItemHelper _restrictedModelItemHelper; 156 /** Default rich text transformer. */ 157 protected RichTextTransformer _richTextTransformer; 158 /** Docbook (rich text) outgoing references extractor. */ 159 protected RichTextOutgoingReferencesExtractor _richTextOutgoingReferencesExtractor; 160 /** Potential global validators. */ 161 protected List<ContentValidator> _globalValidators; 162 /** Potentiel richtext updater */ 163 protected RichTextUpdater _richTextUpdater; 164 /** Indexing model */ 165 protected IndexingModel _indexingModel; 166 /** The helper component for hierarchical simple contents */ 167 protected HierarchicalReferenceTablesHelper _hierarchicalSimpleContentsHelper; 168 /** List of overridden attributes */ 169 protected List<String> _overriddenAttributes = new ArrayList<>(); 170 /** List of overridden views */ 171 protected List<String> _overriddenViews = new ArrayList<>(); 172 /** Non-internal views */ 173 protected Map<String, View> _views = new LinkedHashMap<>(); 174 175 /** The parser for content attribute's definitions */ 176 protected ContentAttributeDefinitionParser _attributeDefinitionParser; 177 /** The parser for content compisite's definitions */ 178 protected ContentRestrictedCompositeDefinitionParser _compositeDefinitionParser; 179 /** The parser for content repeater's definitions */ 180 protected ContentRestrictedRepeaterDefinitionParser _repeaterDefinitionParser; 181 /** The parser for dublin core attribute's definitions */ 182 protected DublinCoreAttributeDefinitionParser _dublinCoreAttributeDefinitionParser; 183 184 /** 185 * ComponentManager pour les Validator 186 * @deprecated use {@link #_validatorManager} instead 187 */ 188 @Deprecated 189 private ThreadSafeComponentManager<Validator> _oldValidatorManager; 190 191 // ComponentManager for validators 192 private ThreadSafeComponentManager<Validator> _validatorManager; 193 194 // ComponentManager pour les Global Validators 195 private ThreadSafeComponentManager<ContentValidator> _globalValidatorsManager; 196 197 private ContentTypeReservedAttributeNameExtensionPoint _contentTypeReservedAttributeNameExtensionPoint; 198 199 /** 200 * ComponentManager pour les Enumerator 201 * @deprecated use {@link #_enumeratorManager} instead 202 */ 203 @Deprecated 204 private ThreadSafeComponentManager<org.ametys.runtime.parameter.Enumerator> _oldEnumeratorManager; 205 206 // ComponentManager for enumerators 207 private ThreadSafeComponentManager<Enumerator> _enumeratorManager; 208 209 // ComponentManager pour les CustomIndexingField 210 private ThreadSafeComponentManager<CustomIndexingField> _customFieldManager; 211 212 // ComponentManager pour les CustomMetadataIndexingField 213 private ThreadSafeComponentManager<CustomMetadataIndexingField> _customMetadataIndexingFieldManager; 214 215 // Content attribute types extesion point 216 private ContentAttributeTypeExtensionPoint _contentAttributeTypeExtensionPoint; 217 218 private boolean _isSimple; 219 220 private boolean _isMultilingual; 221 222 @Override 223 public void service(ServiceManager manager) throws ServiceException 224 { 225 super.service(manager); 226 _manager = manager; 227 _richTextTransformer = (RichTextTransformer) manager.lookup(DocbookTransformer.ROLE); 228 _richTextOutgoingReferencesExtractor = (RichTextOutgoingReferencesExtractor) manager.lookup(DocbookOutgoingReferencesExtractor.ROLE); 229 _richTextUpdater = (RichTextUpdater) manager.lookup(DocbookRichTextUpdater.ROLE); 230 _hierarchicalSimpleContentsHelper = (HierarchicalReferenceTablesHelper) manager.lookup(HierarchicalReferenceTablesHelper.ROLE); 231 _restrictedModelItemHelper = (ContentRestrictedModelItemHelper) manager.lookup(ContentRestrictedModelItemHelper.ROLE); 232 _contentAttributeTypeExtensionPoint = (ContentAttributeTypeExtensionPoint) manager.lookup(ContentAttributeTypeExtensionPoint.ROLE); 233 } 234 235 /** 236 * Get the ContentTypeReservedAttributeNameExtensionPoint instance 237 * @return the instance 238 */ 239 protected ContentTypeReservedAttributeNameExtensionPoint _getContentTypeReservedAttributeNameExtensionPoint() 240 { 241 if (_contentTypeReservedAttributeNameExtensionPoint == null) 242 { 243 try 244 { 245 _contentTypeReservedAttributeNameExtensionPoint = (ContentTypeReservedAttributeNameExtensionPoint) _manager.lookup(ContentTypeReservedAttributeNameExtensionPoint.ROLE); 246 } 247 catch (ServiceException e) 248 { 249 throw new RuntimeException(e); 250 } 251 } 252 return _contentTypeReservedAttributeNameExtensionPoint; 253 } 254 255 @Override 256 public void contextualize(Context context) throws ContextException 257 { 258 _context = context; 259 _cocoonContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 260 } 261 262 @Override 263 public void dispose() 264 { 265 _oldValidatorManager.dispose(); 266 _oldValidatorManager = null; 267 268 _validatorManager.dispose(); 269 _validatorManager = null; 270 271 _globalValidatorsManager.dispose(); 272 _globalValidatorsManager = null; 273 274 _oldEnumeratorManager.dispose(); 275 _oldEnumeratorManager = null; 276 277 _enumeratorManager.dispose(); 278 _enumeratorManager = null; 279 280 _customFieldManager.dispose(); 281 _customFieldManager = null; 282 283 _customMetadataIndexingFieldManager.dispose(); 284 _customMetadataIndexingFieldManager = null; 285 } 286 287 @Override 288 protected Configuration getRootConfiguration(Configuration configuration) 289 { 290 return configuration.getChild("content-type"); 291 } 292 293 @Override 294 protected Configuration getOverridenConfiguration() throws ConfigurationException 295 { 296 Configuration overridenConf = null; 297 File ctFile = new File(_cocoonContext.getRealPath("/WEB-INF/param/content-types/_override/" + _id + ".xml")); 298 299 if (ctFile.exists()) 300 { 301 try (InputStream is = new FileInputStream(ctFile)) 302 { 303 304 overridenConf = new DefaultConfigurationBuilder(true).build(is); 305 } 306 catch (Exception ex) 307 { 308 throw new ConfigurationException("Unable to parse overriden configuration for content type '" + _id + "' at WEB-INF/param/content-types/_override/" + _id + ".xml", overridenConf, ex); 309 } 310 } 311 312 return overridenConf; 313 } 314 315 @Override 316 public void configure(Configuration configuration) throws ConfigurationException 317 { 318 _oldValidatorManager = new ThreadSafeComponentManager<>(); 319 _oldValidatorManager.setLogger(getLogger()); 320 _oldValidatorManager.contextualize(_context); 321 _oldValidatorManager.service(_manager); 322 323 _validatorManager = new ThreadSafeComponentManager<>(); 324 _validatorManager.setLogger(getLogger()); 325 _validatorManager.contextualize(_context); 326 _validatorManager.service(_manager); 327 328 _globalValidatorsManager = new ThreadSafeComponentManager<>(); 329 _globalValidatorsManager.setLogger(getLogger()); 330 _globalValidatorsManager.contextualize(_context); 331 _globalValidatorsManager.service(_manager); 332 333 _oldEnumeratorManager = new ThreadSafeComponentManager<>(); 334 _oldEnumeratorManager.setLogger(getLogger()); 335 _oldEnumeratorManager.contextualize(_context); 336 _oldEnumeratorManager.service(_manager); 337 338 _enumeratorManager = new ThreadSafeComponentManager<>(); 339 _enumeratorManager.setLogger(getLogger()); 340 _enumeratorManager.contextualize(_context); 341 _enumeratorManager.service(_manager); 342 343 _customFieldManager = new ThreadSafeComponentManager<>(); 344 _customFieldManager.setLogger(getLogger()); 345 _customFieldManager.contextualize(_context); 346 _customFieldManager.service(_manager); 347 348 _customMetadataIndexingFieldManager = new ThreadSafeComponentManager<>(); 349 _customMetadataIndexingFieldManager.setLogger(getLogger()); 350 _customMetadataIndexingFieldManager.contextualize(_context); 351 _customMetadataIndexingFieldManager.service(_manager); 352 353 Configuration rootConfiguration = getRootConfiguration(configuration); 354 355 _abstract = rootConfiguration.getAttributeAsBoolean("abstract", false); 356 357 _configureSuperTypes(rootConfiguration); 358 359 _configureLabels(rootConfiguration); 360 _configureIcons(rootConfiguration); 361 362 _configureCSSFiles(rootConfiguration); 363 364 // Tags 365 _tags = new HashSet<>(); 366 367 if (rootConfiguration.getChild("tags", false) != null) 368 { 369 if (rootConfiguration.getChild("tags").getAttributeAsBoolean("inherited", false)) 370 { 371 // Get tags from super types 372 for (String superTypeId : _superTypeIds) 373 { 374 ContentType superType = _cTypeEP.getExtension(superTypeId); 375 _tags.addAll(superType.getTags()); 376 } 377 } 378 _tags.addAll(_parseTags (rootConfiguration.getChild("tags"))); 379 } 380 381 _configureDefaultWorkflowName(rootConfiguration); 382 383 // Rights 384 _right = rootConfiguration.getChild("right").getValue(null); 385 386 _isSimple = true; 387 for (String superTypeId : _superTypeIds) 388 { 389 ContentType superType = _cTypeEP.getExtension(superTypeId); 390 if (superType == null) 391 { 392 throw new ConfigurationException("The content type '" + this.getId() + " cannot extends the unexisting type '" + superTypeId + "'"); 393 } 394 if (!superType.isSimple()) 395 { 396 _isSimple = false; 397 break; 398 } 399 } 400 401 _isMultilingual = false; 402 for (String superTypeId : _superTypeIds) 403 { 404 ContentType superType = _cTypeEP.getExtension(superTypeId); 405 if (superType.isMultilingual()) 406 { 407 _isMultilingual = true; 408 break; 409 } 410 } 411 412 // Attribute definitions 413 _configureAttributeDefinitions (rootConfiguration); 414 415 // Parent content type 416 _configureParentContentType(rootConfiguration); 417 418 // Views 419 _configureViews(rootConfiguration); 420 _configureMetadataSets (rootConfiguration); 421 422 _checkForReservedAttributeName(); 423 424 if (!_abstract && !hasTag(TAG_MIXIN)) 425 { 426 if (!_views.containsKey("details")) 427 { 428 throw new ConfigurationException("Mandatory view named 'details' is missing for content type " + _id, configuration); 429 } 430 if (!_views.containsKey("main")) 431 { 432 throw new ConfigurationException("Mandatory view named 'main' is missing for content type " + _id, configuration); 433 } 434 } 435 436 // Global validators 437 _configureGlobalValidators (rootConfiguration); 438 439 // Indexing model 440 _configureIndexingModel (rootConfiguration); 441 } 442 443 private void _checkForReservedAttributeName() throws ConfigurationException 444 { 445 Set<String> reservedNames = _getContentTypeReservedAttributeNameExtensionPoint().getExtensionsIds(); 446 447 List<String> usedReservedKeywords = new ArrayList<>(reservedNames); 448 usedReservedKeywords.retainAll(_modelItems.keySet()); 449 450 if (!usedReservedKeywords.isEmpty()) 451 { 452 throw new ConfigurationException("In content type '" + _id + "', one or more attributes are named with a reserved keyword: " + StringUtils.join(usedReservedKeywords, ", ") 453 + ". The reserved keywords are: {" + StringUtils.join(reservedNames, ", ") + "}"); 454 } 455 } 456 457 /** 458 * Configure attribute definitions 459 * @param mainConfig The content type configuration 460 * @throws ConfigurationException if an error occurred 461 */ 462 protected void _configureAttributeDefinitions (Configuration mainConfig) throws ConfigurationException 463 { 464 _attributeDefinitionParser = new ContentAttributeDefinitionParser(_contentAttributeTypeExtensionPoint, _enumeratorManager, _validatorManager); 465 _compositeDefinitionParser = new ContentRestrictedCompositeDefinitionParser(_contentAttributeTypeExtensionPoint); 466 _repeaterDefinitionParser = new ContentRestrictedRepeaterDefinitionParser(_contentAttributeTypeExtensionPoint); 467 _dublinCoreAttributeDefinitionParser = new DublinCoreAttributeDefinitionParser(_contentAttributeTypeExtensionPoint, _enumeratorManager, _validatorManager, _dcProvider); 468 MetadataAndRepeaterDefinitionParser defParser = new MetadataAndRepeaterDefinitionParser(_oldEnumeratorManager, _oldValidatorManager); 469 470 // First, get metadata from super type if applicable. 471 _modelItems.putAll(_contentTypesHelper.getModelItems(_superTypeIds)); 472 _metadata.putAll(_contentTypesHelper.getMetadataDefinitions(_superTypeIds)); 473 474 Map<String, Configuration> attributeConfiguration = new LinkedHashMap<>(); 475 _getApplicableAttributes(mainConfig, attributeConfiguration, false); 476 477 Configuration overriddenConfig = getOverridenConfiguration(); 478 if (overriddenConfig != null) 479 { 480 _getApplicableAttributes(overriddenConfig, attributeConfiguration, true); 481 } 482 483 // Then, parse own attributes 484 _parseAllMetadatas(attributeConfiguration, defParser); 485 _parseAllAttributes(attributeConfiguration); 486 487 try 488 { 489 _attributeDefinitionParser.lookupComponents(); 490// _dublinCoreAttributeDefinitionParser.lookupComponents(); 491 defParser.lookupComponents(); 492 } 493 catch (Exception e) 494 { 495 throw new ConfigurationException("Unable to lookup parameter local components", overriddenConfig, e); 496 } 497 498 } 499 500 /** 501 * Fill a map of the applicable attribute configurations. 502 * @param config the content type configuration. 503 * @param attributeConfigurations the Map of attributes {@link Configuration}, indexed by name. 504 * @param allowOverride if true, encountering an attribute which has already been declared (based on its name) will replace it. Otherwise, an exception will be thrown. 505 * @throws ConfigurationException if an error occurs. 506 */ 507 protected void _getApplicableAttributes(Configuration config, Map<String, Configuration> attributeConfigurations, boolean allowOverride) throws ConfigurationException 508 { 509 for (Configuration childConfiguration : config.getChildren()) 510 { 511 String childName = childConfiguration.getName(); 512 513 if (childName.equals("metadata") || childName.equals("repeater")) 514 { 515 String attributeName = childConfiguration.getAttribute("name", ""); 516 517 if (!allowOverride && attributeConfigurations.containsKey(attributeName)) 518 { 519 throw new ConfigurationException("Attribute with name '" + attributeName + "' is already defined", childConfiguration); 520 } 521 else if (allowOverride && attributeConfigurations.containsKey(attributeName)) 522 { 523 _checkAttributeTypes(attributeConfigurations.get(attributeName), childConfiguration); 524 } 525 526 if (allowOverride) 527 { 528 this._overriddenAttributes.add(attributeName); 529 } 530 531 attributeConfigurations.put(attributeName, childConfiguration); 532 } 533 else if (childName.equals("dublin-core")) 534 { 535 attributeConfigurations.put("dc", childConfiguration); 536 } 537 } 538 } 539 /** 540 * Get the overridden attributes list 541 * @return the overridden attributes list 542 */ 543 public List<String> getOverriddenAttributes() 544 { 545 return this._overriddenAttributes; 546 } 547 548 /** 549 * Check if all metadata's types defined in first configuration are equals to those defined in second configuration 550 * @param metadataConf1 The first configuration to compare 551 * @param metadataConf2 The second configuration to compare 552 * @throws ConfigurationException if the types are not equals 553 */ 554 protected void _checkAttributeTypes (Configuration metadataConf1, Configuration metadataConf2) throws ConfigurationException 555 { 556 String type = metadataConf1.getAttribute("type", ""); 557 String overridenType = metadataConf2.getAttribute("type", ""); 558 if (!overridenType.equals(type)) 559 { 560 throw new ConfigurationException("The type of metadata '" + metadataConf1.getAttribute("name") + " (" + type.toUpperCase() + ")" + "' can not be overriden to '" + metadataConf2.getAttribute("name") + " (" + overridenType.toUpperCase() + ")'"); 561 } 562 563 if ("composite".equals(type) || metadataConf1.getName().equals("repeater")) 564 { 565 for (Configuration childConfig1 : metadataConf1.getChildren()) 566 { 567 String childName = childConfig1.getName(); 568 if (childName.equals("metadata") || childName.equals("repeater")) 569 { 570 Configuration childConfig2 = null; 571 for (Configuration conf : metadataConf2.getChildren(childName)) 572 { 573 if (childConfig1.getAttribute("name").equals(conf.getAttribute("name"))) 574 { 575 childConfig2 = conf; 576 break; 577 } 578 } 579 580 if (childConfig2 != null) 581 { 582 _checkAttributeTypes (childConfig1, childConfig2); 583 } 584 } 585 } 586 } 587 } 588 589 /** 590 * Parse all attribute configurations. 591 * @param attributeConfigurations the attribute configurations. 592 * @throws ConfigurationException if the configuration is invalid. 593 */ 594 protected void _parseAllAttributes(Map<String, Configuration> attributeConfigurations) throws ConfigurationException 595 { 596 for (Configuration childConfiguration : attributeConfigurations.values()) 597 { 598 String childConfigName = childConfiguration.getName(); 599 600 if (childConfigName.equals("metadata") || childConfigName.equals("repeater")) 601 { 602 ModelItem child = _parseModelItem(childConfiguration, null); 603 if (child != null) 604 { 605 final String childName = child.getName(); 606 if (_modelItems.containsKey(childName)) 607 { 608 _checkAttributeTypes(_modelItems.get(childName), child); 609 } 610 611 _checkContentTypeSimplicity(child); 612 _modelItems.put(childName, child); 613 } 614 } 615 else if (childConfigName.equals("dublin-core")) 616 { 617 _parseDublinCoreAttributes(); 618 } 619 } 620 } 621 622 /** 623 * Parses a model item 624 * @param itemConfiguration configuration of the model item to parse 625 * @param parent the parent of the model item to parse. Can be <code>null</code> if the item has no parent. 626 * @return the parsed model item 627 * @throws ConfigurationException if an error occurs while the model item is parsed 628 */ 629 @SuppressWarnings("static-access") 630 protected ModelItem _parseModelItem(Configuration itemConfiguration, ModelItemGroup parent) throws ConfigurationException 631 { 632 ModelItem modelItem = null; 633 final String itemConfigName = itemConfiguration.getName(); 634 if (itemConfigName.equals("metadata")) 635 { 636 String typeId = itemConfiguration.getAttribute("type"); 637 if (ModelItemTypeConstants.COMPOSITE_TYPE_ID.equals(typeId)) 638 { 639 modelItem = _compositeDefinitionParser.parse(_manager, _pluginName, itemConfiguration, this, parent); 640 } 641 else 642 { 643 modelItem = _attributeDefinitionParser.parse(_manager, _pluginName, itemConfiguration, this, parent); 644 } 645 } 646 else if (ModelItemTypeConstants.REPEATER_TYPE_ID.equals(itemConfigName)) 647 { 648 modelItem = _repeaterDefinitionParser.parse(_manager, _pluginName, itemConfiguration, this, parent); 649 } 650 651 if (modelItem != null && modelItem instanceof ModelItemGroup) 652 { 653 for (Configuration childConfiguration : itemConfiguration.getChildren()) 654 { 655 _parseModelItem(childConfiguration, (ModelItemGroup) modelItem); 656 } 657 } 658 659 return modelItem; 660 } 661 662 /** 663 * Check if all attribute types defined in first model item are equals to those defined in second model item 664 * @param item1 The first item to compare 665 * @param item2 The second item to compare 666 * @throws ConfigurationException if the types are not equals 667 */ 668 protected void _checkAttributeTypes (ModelItem item1, ModelItem item2) throws ConfigurationException 669 { 670 if (item1 instanceof AttributeDefinition) 671 { 672 AttributeDefinition attributeDef1 = (AttributeDefinition) item1; 673 final String attibute1TypeId = attributeDef1.getType().getId(); 674 if (!(item2 instanceof AttributeDefinition) || !attibute1TypeId.equals(((AttributeDefinition) item2).getType().getId())) 675 { 676 throw new ConfigurationException("The type of attribute '" + attributeDef1 + "' defined in content type '" + attributeDef1.getModel() + "', can not be overriden to '" + item2 + "' in content type '" + ((AttributeDefinition) item2).getModel() + "'"); 677 } 678 } 679 else 680 { 681 if (!(item2 instanceof ModelItemGroup)) 682 { 683 throw new ConfigurationException("The item group '" + item1 + "' can not be overriden by the non item group '" + item2 + "' in content type '" + ((AttributeDefinition) item2).getModel() + "'"); 684 } 685 686 ModelItemGroup group1 = (ModelItemGroup) item1; 687 ModelItemGroup group2 = (ModelItemGroup) item2; 688 689 for (ModelItem subItemfromGroup1 : group1.getChildren()) 690 { 691 ModelItem subItemFromGroup2 = group2.getChild(subItemfromGroup1.getName()); 692 if (subItemFromGroup2 != null) 693 { 694 _checkAttributeTypes(subItemfromGroup1, subItemFromGroup2); 695 _checkContentTypeSimplicity(subItemfromGroup1); 696 } 697 } 698 } 699 } 700 701 /** 702 * Checks the given model item to determine if this content type is multilingual and/or simple 703 * All items of a simple content-type have to be a simple type (string, long, date, ..) 704 * A multilingual content type should contain at least an attribute of type MULTILINGUAL-STRING 705 * @param modelItem The model item to check 706 */ 707 protected void _checkContentTypeSimplicity(ModelItem modelItem) 708 { 709 if (modelItem instanceof ModelItemGroup) 710 { 711 // If the content type contains groups, it is not simple 712 _isSimple = false; 713 } 714 else if (modelItem instanceof AttributeDefinition) 715 { 716 ElementType type = ((AttributeDefinition) modelItem).getType(); 717 if (!type.isSimple()) 718 { 719 // If there is a no simple attribute, the content type is not simple 720 _isSimple = false; 721 } 722 723 if (ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(type.getId())) 724 { 725 // If there is a multilingual-string attribute, the content type is multilingual 726 _isMultilingual = true; 727 } 728 } 729 } 730 731 /** 732 * Parse DublinCore attributes 733 * @throws ConfigurationException if the configuration is invalid 734 */ 735 @SuppressWarnings("static-access") 736 protected void _parseDublinCoreAttributes() throws ConfigurationException 737 { 738 Source src = null; 739 740 try 741 { 742 src = _srcResolver.resolveURI("resource://org/ametys/cms/dublincore/dublincore.xml"); 743 744 if (src.exists()) 745 { 746 Configuration configuration = null; 747 try (InputStream is = src.getInputStream()) 748 { 749 configuration = new DefaultConfigurationBuilder(true).build(is); 750 } 751 752 ContentRestrictedCompositeDefinition definition = new ContentRestrictedCompositeDefinition(); 753 definition.setModel(this); 754 definition.setName("dc"); 755 definition.setLabel(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUBLINCORE_LABEL")); 756 definition.setDescription(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUBLINCORE_DESC")); 757 definition.setType(_contentAttributeTypeExtensionPoint.getExtension(ModelItemTypeConstants.COMPOSITE_TYPE_ID)); 758 759 for (Configuration childConfiguration : configuration.getChildren()) 760 { 761 String childName = childConfiguration.getName(); 762 763 if (childName.equals("metadata")) 764 { 765 _dublinCoreAttributeDefinitionParser.parse(_manager, _pluginName, childConfiguration, this, definition); 766 } 767 } 768 769 _modelItems.put("dc", definition); 770 } 771 } 772 catch (IOException | SAXException e) 773 { 774 throw new ConfigurationException("Unable to parse Dublin Core metadata", e); 775 } 776 finally 777 { 778 if (src != null) 779 { 780 _srcResolver.release(src); 781 } 782 } 783 } 784 785 /** 786 * Parse all metadata configurations. 787 * @param metadataConfigurations the metadata configurations. 788 * @param defParser the metadata definition parser. 789 * @throws ConfigurationException if the configuration is invalid. 790 * @deprecated use {@link #_parseAllAttributes(Map)} instead 791 */ 792 @Deprecated 793 protected void _parseAllMetadatas(Map<String, Configuration> metadataConfigurations, MetadataAndRepeaterDefinitionParser defParser) throws ConfigurationException 794 { 795 for (Configuration childConfiguration : metadataConfigurations.values()) 796 { 797 String childName = childConfiguration.getName(); 798 799 if (childName.equals("metadata") || childName.equals("repeater")) 800 { 801 _parseMetadata(childConfiguration, defParser); 802 } 803 else if (childName.equals("dublin-core")) 804 { 805 _parseDublinCoreMetadata(defParser); 806 } 807 } 808 } 809 810 /** 811 * Parse a metadata configuration. 812 * @param metadataConfiguration the metadata configuration. 813 * @param defParser the metadata definition parser. 814 * @return the created MetadataDefinition. 815 * @throws ConfigurationException if the configuration is invalid 816 * @deprecated use {@link #_parseModelItem(Configuration, ModelItemGroup)} instead 817 */ 818 @Deprecated 819 protected MetadataDefinition _parseMetadata(Configuration metadataConfiguration, MetadataAndRepeaterDefinitionParser defParser) throws ConfigurationException 820 { 821 MetadataDefinition metadataDefinition = defParser.parseParameter(_manager, _pluginName, metadataConfiguration); 822 metadataDefinition.setReferenceContentType(_id); 823 824 String metadataName = metadataDefinition.getName(); 825 826 if (_metadata.containsKey(metadataName)) 827 { 828 _checkMetadataTypes (_metadata.get(metadataName), metadataDefinition); 829 } 830 831 // Update simple and multilingual properties 832 _checkMetadataDefinition(metadataDefinition); 833 834 _metadata.put(metadataName, metadataDefinition); 835 836 return metadataDefinition; 837 } 838 839 /** 840 * Check if all metadata's types defined in first metadata definition are equals to those defined in second metadata definition 841 * @param metaDef1 The first metadata definition to compare 842 * @param metaDef2 The second metadata definition to compare 843 * @throws ConfigurationException if the types are not equals 844 * @deprecated Use {@link #_checkAttributeTypes(ModelItem, ModelItem)} instead 845 */ 846 @Deprecated 847 protected void _checkMetadataTypes (MetadataDefinition metaDef1, MetadataDefinition metaDef2) throws ConfigurationException 848 { 849 if (!metaDef1.getType().equals(metaDef2.getType())) 850 { 851 throw new ConfigurationException("The type of metadata '" + metaDef1.toString() + "' defined in content type '" + metaDef1.getReferenceContentType() + "', can not be overriden to '" + metaDef2.toString() + "' in content type '" + metaDef2.getReferenceContentType() + "'"); 852 } 853 854 if (metaDef1.getType().equals(MetadataType.COMPOSITE)) 855 { 856 for (String subMetadataName : metaDef1.getMetadataNames()) 857 { 858 if (metaDef2.getMetadataDefinition(subMetadataName) != null) 859 { 860 _checkMetadataTypes (metaDef1.getMetadataDefinition(subMetadataName), metaDef2.getMetadataDefinition(subMetadataName)); 861 862 // Update simple and multilingual properties 863 _checkMetadataDefinition(metaDef1.getMetadataDefinition(subMetadataName)); 864 } 865 } 866 } 867 } 868 869 /** 870 * Check the medatata definition to determines if this content type is multilingual and/or simple 871 * All medatata of a simple content-type have to be a simple type (string, long, date, ..) 872 * A multilingual content type should contain at least a metadata of type MULTILINGUAL_STRING 873 * @param metadataDefinition The metadata definition 874 * @return false if the medatata definition is not a valid medatata definition for a simple content-type 875 * @deprecated Use {@link #_checkContentTypeSimplicity(ModelItem)} instead 876 */ 877 @Deprecated 878 protected boolean _checkMetadataDefinition (MetadataDefinition metadataDefinition) 879 { 880 MetadataType type = metadataDefinition.getType(); 881 882 switch (type) 883 { 884 case MULTILINGUAL_STRING: 885 // The content type is a multilingual content type 886 _isMultilingual = true; 887 break; 888 889 case COMPOSITE: 890 case FILE: 891 case REFERENCE: 892 case RICH_TEXT: 893 // The content type can not be simple (complex metadata are not allowed) 894 _isSimple = false; 895 break; 896 default: 897 break; 898 } 899 900 return true; 901 } 902 903 /** 904 * Parse DublinCore metadata 905 * @param defParser The parser definition 906 * @throws ConfigurationException if the configuration is invalid 907 * @deprecated Use {@link #_parseDublinCoreAttributes()} instead 908 */ 909 @Deprecated 910 protected void _parseDublinCoreMetadata (MetadataAndRepeaterDefinitionParser defParser) throws ConfigurationException 911 { 912 Source src = null; 913 914 try 915 { 916 src = _srcResolver.resolveURI("resource://org/ametys/cms/dublincore/dublincore.xml"); 917 918 if (src.exists()) 919 { 920 Configuration configuration = null; 921 try (InputStream is = src.getInputStream()) 922 { 923 configuration = new DefaultConfigurationBuilder(true).build(is); 924 } 925 926 MetadataDefinition metadataDefinition = new MetadataDefinition(); 927 metadataDefinition.setReferenceContentType(_id); 928 metadataDefinition.setId("/dc"); // FIXME ? 929 metadataDefinition.setLabel(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUBLINCORE_LABEL")); 930 metadataDefinition.setDescription(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUBLINCORE_DESC")); 931 metadataDefinition.setName("dc"); 932 metadataDefinition.setType(MetadataType.COMPOSITE); 933 934 for (Configuration childConfiguration : configuration.getChildren()) 935 { 936 String childName = childConfiguration.getName(); 937 938 if (childName.equals("metadata")) 939 { 940 MetadataDefinition metaDef = defParser.parseParameter(_manager, _pluginName, childConfiguration); 941 metaDef.setReferenceContentType(_id); 942 String metadataName = metaDef.getName(); 943 metaDef.setId("/dc/" + metadataName); // FIXME ? 944 945 if (metaDef.getEnumerator() == null && _dcProvider.isEnumerated(metadataName)) 946 { 947 StaticEnumerator enumerator = new StaticEnumerator(); 948 949 Map<String, I18nizableText> entries = _dcProvider.getEntries(metadataName); 950 if (entries != null) 951 { 952 for (String value : entries.keySet()) 953 { 954 enumerator.add(entries.get(value), value); 955 } 956 957 } 958 metaDef.setEnumerator(enumerator); 959 } 960 961 metadataDefinition.addMetadata(metaDef); 962 } 963 } 964 965 _metadata.put("dc", metadataDefinition); 966 } 967 } 968 catch (IOException e) 969 { 970 throw new ConfigurationException("Unable to parse Dublin Core metadata", e); 971 } 972 catch (SAXException e) 973 { 974 throw new ConfigurationException("Unable to parse Dublin Core metadata", e); 975 } 976 finally 977 { 978 if (src != null) 979 { 980 _srcResolver.release(src); 981 } 982 } 983 } 984 985 /** 986 * Configure the default workflow name from the XML configuration. 987 * - From the overriden configuration 988 * - If not, from the current configuration 989 * - If not, from the supertypes 990 * - If it cannot be determined and the content type is a reference table, then "reference-table" 991 * - Otherwise "content" 992 * @param mainConfig The configuration 993 * @throws ConfigurationException if an exception occurs 994 */ 995 protected void _configureDefaultWorkflowName(Configuration mainConfig) throws ConfigurationException 996 { 997 _defaultWorkflowName = Optional.ofNullable(getOverridenConfiguration()) 998 // Override mode 999 .map(oc -> oc.getChild("default-workflow").getValue(null)) 1000 // Normal mode 1001 .or(() -> Optional.ofNullable(mainConfig.getChild("default-workflow").getValue(null))) 1002 // Inherited mode 1003 .or(this::_getDefaultWorkflowNameFromSupertypes) 1004 // Otherwise default workflow 1005 .or(() -> Optional.of(isReferenceTable() ? "reference-table" : "content")); 1006 } 1007 1008 private Optional<String> _getDefaultWorkflowNameFromSupertypes() 1009 { 1010 Set<String> defaultWorkflowNames = new HashSet<>(); 1011 1012 // Get tags from super types 1013 for (String superTypeId : _superTypeIds) 1014 { 1015 ContentType superType = _cTypeEP.getExtension(superTypeId); 1016 superType.getDefaultWorkflowName() 1017 .ifPresent(defaultWorkflowNames::add); 1018 } 1019 1020 if (defaultWorkflowNames.size() > 1) 1021 { 1022 getLogger().warn("Several default workflows are defined for content type '{}' : {}.", _id, StringUtils.join(defaultWorkflowNames)); 1023 return Optional.empty(); 1024 } 1025 1026 return defaultWorkflowNames.stream().findFirst(); 1027 } 1028 1029 /** 1030 * Configures the "parent" content type. 1031 * This must not be confounded with the super types. (See {@link AbstractContentTypeDescriptor#_configureSuperTypes(Configuration)}) 1032 * @param mainConfig The main configuration 1033 * @throws ConfigurationException if an error occurred 1034 */ 1035 protected void _configureParentContentType(Configuration mainConfig) throws ConfigurationException 1036 { 1037 Configuration parentCTypeConf = mainConfig.getChild("parent-ref", false); 1038 _parentAttributeDefinition = null; 1039 1040 if (parentCTypeConf != null) 1041 { 1042 // Check this content type is a reference table 1043 if (!isReferenceTable()) 1044 { 1045 getLogger().error("The 'parent-ref' tag for content type '{}' is defined but this feature is only enabled for reference table content types. It will be ignored.", getId()); 1046 return; 1047 } 1048 1049 String refAttributeName = parentCTypeConf.getAttribute("name"); 1050 // Check valid reference of metadata 1051 if (!_modelItems.containsKey(refAttributeName)) 1052 { 1053 getLogger().error("The 'parent-ref' tag for content type '{}' references the attribute '{}' but it does not exist. It will be ignored.", getId(), refAttributeName); 1054 return; 1055 } 1056 1057 ModelItem modelItem = _modelItems.get(refAttributeName); 1058 // Check metadata of type "content" 1059 if (!(modelItem instanceof ContentAttributeDefinition)) 1060 { 1061 getLogger().error("The 'parent-ref' tag for content type '{}' references the attribute '{}' but it is not an attribute of type CONTENT. It will be ignored.", getId(), refAttributeName); 1062 return; 1063 } 1064 1065 ContentAttributeDefinition contentAttributeDefinition = (ContentAttributeDefinition) modelItem; 1066 if (contentAttributeDefinition.isMultiple()) 1067 { 1068 getLogger().error("The 'parent-ref' tag for content type '{}' references the attribute '{}' but it is a multiple attribute. It will be ignored.", getId(), refAttributeName); 1069 return; 1070 } 1071 1072 String parentCTypeName = contentAttributeDefinition.getContentTypeId(); 1073 ContentType parentCType = _cTypeEP.getExtension(parentCTypeName); 1074 1075 if (parentCType == null) 1076 { 1077 getLogger().error("The 'parent-ref' tag for content type '{}' references the attribute '{}' with content-type '{}' but it does not exist or is not yet initialized. It will be ignored.", getId(), refAttributeName, parentCTypeName); 1078 return; 1079 } 1080 // Check parent content type is private AND simple 1081 else if (!parentCType.isPrivate()) 1082 { 1083 getLogger().error("The 'parent-ref' tag for content type '{}' references the attribute '{}' with content-type '{}' but it is not private. It will be ignored.", getId(), refAttributeName, parentCTypeName); 1084 return; 1085 } 1086 else if (!parentCType.isReferenceTable()) 1087 { 1088 getLogger().error("The 'parent-ref' tag for content type '{}' references the attribute '{}' with content-type '{}' but it is not a reference table. It will be ignored.", getId(), refAttributeName, parentCTypeName); 1089 return; 1090 } 1091 1092 if (!_hierarchicalSimpleContentsHelper.registerRelation(parentCType, this)) 1093 { 1094 getLogger().error("The content type '{}' defines a parent hierarchy which is not valid. See previous logs to know more.\nThis error can lead to UI issues.", getId()); 1095 return; 1096 } 1097 _parentAttributeDefinition = contentAttributeDefinition; 1098 } 1099 } 1100 1101 /** 1102 * Configure the content type views 1103 * @param mainConfig The content type configuration 1104 * @throws ConfigurationException if an error occurred 1105 */ 1106 protected void _configureViews(Configuration mainConfig) throws ConfigurationException 1107 { 1108 // Get applicable views configurations 1109 // At first, get the views configured with the old syntax to replace them by the ones configured with the new syntax 1110 Map<String, Configuration> viewConfigurations = _getApplicableViews(mainConfig, __VIEW_TAG_NAME_WITH_OLD_METADATA_API_SYNTAX, false); 1111 viewConfigurations.putAll(_getApplicableViews(mainConfig, __VIEW_TAG_NAME_WITH_NEW_ATTRIBUTE_API_SYNTAX, false)); 1112 1113 Configuration overriddenConfig = getOverridenConfiguration(); 1114 if (overriddenConfig != null) 1115 { 1116 viewConfigurations.putAll(_getApplicableViews(overriddenConfig, __VIEW_TAG_NAME_WITH_OLD_METADATA_API_SYNTAX, true)); 1117 viewConfigurations.putAll(_getApplicableViews(overriddenConfig, __VIEW_TAG_NAME_WITH_NEW_ATTRIBUTE_API_SYNTAX, true)); 1118 } 1119 1120 // Parse content type's own views 1121 Map<String, View> contentTypeViews = _parseViews(viewConfigurations.values()); 1122 1123 // Get views from super types that are not overriden by the content type itself 1124 Map<String, View> superTypesViews = _contentTypesHelper.getViews(_superTypeIds, new String[0], contentTypeViews.keySet()); 1125 1126 // Put in views: first the super types views, and then the content type's own views 1127 _views.putAll(superTypesViews); 1128 _views.putAll(contentTypeViews); 1129 1130 // Copy super type views in order to replace the overridden attributes 1131 for (String superTypesViewName : superTypesViews.keySet()) 1132 { 1133 View superTypesView = superTypesViews.get(superTypesViewName); 1134 if (!contentTypeViews.containsKey(superTypesViewName)) 1135 { 1136 _getViewWithOverriddenAttributes(superTypesView) 1137 .ifPresent(viewWithOverriddenAttributes -> _views.put(superTypesViewName, viewWithOverriddenAttributes)); 1138 } 1139 } 1140 } 1141 1142 private Optional<View> _getViewWithOverriddenAttributes(View originalView) 1143 { 1144 List<ModelItem> overriddenAttributes = _overriddenAttributes.stream() 1145 .map(this::getModelItem) 1146 .collect(Collectors.toList()); 1147 1148 if (_containsOneOfTheModelItems(originalView, overriddenAttributes)) 1149 { 1150 View viewWithOverriddenAttributes = new View(); 1151 originalView.copyTo(viewWithOverriddenAttributes); 1152 _copyAndReplaceOverriddenAttributes(originalView, viewWithOverriddenAttributes, overriddenAttributes, false); 1153 return Optional.of(viewWithOverriddenAttributes); 1154 } 1155 else 1156 { 1157 return Optional.empty(); 1158 } 1159 } 1160 1161 private void _copyAndReplaceOverriddenAttributes(ViewItemContainer sourceContainer, ViewItemContainer destinationContainer, Collection<? extends ModelItem> overriddenItems, boolean includeOnlyOverriddenAttributes) 1162 { 1163 for (ViewItem sourceViewItem : sourceContainer.getViewItems()) 1164 { 1165 if (sourceViewItem instanceof ModelViewItem) 1166 { 1167 Optional<ModelItem> overriddenItem = _getModelItem(overriddenItems, sourceViewItem.getName()); 1168 if (overriddenItem.isPresent()) 1169 { 1170 if (sourceViewItem instanceof ViewElement) 1171 { 1172 ViewElement destViewItem = new ViewElement(); 1173 sourceViewItem.copyTo(destViewItem); 1174 destViewItem.setDefinition((ElementDefinition) overriddenItem.get()); 1175 destinationContainer.addViewItem(destViewItem); 1176 } 1177 else 1178 { 1179 ModelViewItemGroup destViewItem = new ModelViewItemGroup(); 1180 sourceViewItem.copyTo(destViewItem); 1181 destViewItem.setDefinition((ModelItemGroup) overriddenItem.get()); 1182 _copyAndReplaceOverriddenAttributes((ModelViewItemGroup) sourceViewItem, destViewItem, ((ModelItemGroup) overriddenItem.get()).getChildren(), true); 1183 destinationContainer.addViewItem(destViewItem); 1184 } 1185 } 1186 else if (!includeOnlyOverriddenAttributes) 1187 { 1188 destinationContainer.addViewItem(sourceViewItem); 1189 } 1190 } 1191 else if (sourceViewItem instanceof SimpleViewItemGroup) 1192 { 1193 if (_containsOneOfTheModelItems((SimpleViewItemGroup) sourceViewItem, overriddenItems)) 1194 { 1195 SimpleViewItemGroup destViewItem = new SimpleViewItemGroup(); 1196 sourceViewItem.copyTo(destViewItem); 1197 _copyAndReplaceOverriddenAttributes((SimpleViewItemGroup) sourceViewItem, destViewItem, overriddenItems, false); 1198 destinationContainer.addViewItem(destViewItem); 1199 } 1200 } 1201 } 1202 } 1203 1204 private Optional<ModelItem> _getModelItem(Collection<? extends ModelItem> modelItems, String name) 1205 { 1206 for (ModelItem modelItem : modelItems) 1207 { 1208 if (modelItem.getName().equals(name)) 1209 { 1210 return Optional.of(modelItem); 1211 } 1212 } 1213 1214 return Optional.empty(); 1215 } 1216 1217 private boolean _containsOneOfTheModelItems(ViewItemContainer viewItemContainer, Collection<? extends ModelItem> modelItems) 1218 { 1219 for (ModelItem modelItem : modelItems) 1220 { 1221 if (viewItemContainer.hasModelViewItem(modelItem.getName())) 1222 { 1223 return true; 1224 } 1225 } 1226 1227 return false; 1228 } 1229 1230 /** 1231 * Get the overridden views list 1232 * @return the overridden views list 1233 */ 1234 public List<String> getOverriddenViews() 1235 { 1236 return this._overriddenViews; 1237 } 1238 1239 /** 1240 * Compute the applicable views from their configurations. 1241 * @param configuration The content type configuration 1242 * @param viewTagName The name of the tag containing the view 1243 * @param allowOverride if <code>true</code>, encountering a view which has already been declared (based on its name) will replace it. Otherwise, an exception will be thrown. 1244 * @return the applicable views, indexed by their names. For each view, indicates if the view is configured 1245 * @throws ConfigurationException if the configuration is invalid 1246 */ 1247 protected Map<String, Configuration> _getApplicableViews(Configuration configuration, String viewTagName, boolean allowOverride) throws ConfigurationException 1248 { 1249 Map<String, Configuration> applicableViews = new LinkedHashMap<>(); 1250 1251 for (Configuration viewConfig : configuration.getChildren(viewTagName)) 1252 { 1253 String name = viewConfig.getAttribute("name"); 1254 if (allowOverride) 1255 { 1256 this._overriddenViews.add(name); 1257 } 1258 if (!allowOverride && applicableViews.containsKey(name)) 1259 { 1260 // TODO NEWATTRIBUTEAPI_CONTENT: Review the unicity of views due to edition / view differences : 1261 // In old API, we made the difference between view and edition metadata set. 1262 // We decided remove this notion. But all existing content types have for example 2 "main" metadata-set, one for view and the other for edition 1263 // So we can't check here that there is onl one view called "main" 1264 // Really remove the edition / view notion? or allow the creation of a view without type and put it in edition and view mode? 1265// throw new ConfigurationException("The view named '" + name + "' is already defined.", viewConfig); 1266 getLogger().info("The view named '" + name + "' is already defined.", viewConfig); 1267 } 1268 else 1269 { 1270 applicableViews.put(name, viewConfig); 1271 } 1272 } 1273 1274 return applicableViews; 1275 } 1276 1277 /** 1278 * Parses the own content type's views 1279 * @param viewConfigurations the configuration that contains the views 1280 * @throws ConfigurationException if the configuration is invalid 1281 * @return the views as a <code>Map</code> with a boolean that determines if the view is internal or not 1282 */ 1283 protected Map<String, View> _parseViews(Collection<Configuration> viewConfigurations) throws ConfigurationException 1284 { 1285 ViewParser metadataSetParser = new ContentTypeViewParser(this, Optional.of(__GROUP_TAG_NAME_WITH_OLD_METADATA_API_SYNTAX), Optional.of(__ATTRIBUTE_REF_TAG_NAME_WITH_OLD_METADATA_API_SYNTAX), false); 1286 ViewParser viewParser = new ContentTypeViewParser(this); 1287 1288 try 1289 { 1290 Map<String, View> views = new LinkedHashMap<>(); 1291 1292 LifecycleHelper.setupComponent(metadataSetParser, new SLF4JLoggerAdapter(getLogger()), _context, _manager, null); 1293 LifecycleHelper.setupComponent(viewParser, new SLF4JLoggerAdapter(getLogger()), _context, _manager, null); 1294 1295 for (Configuration viewConfiguration : viewConfigurations) 1296 { 1297 ViewParser parser = __VIEW_TAG_NAME_WITH_NEW_ATTRIBUTE_API_SYNTAX.equals(viewConfiguration.getName()) ? viewParser : metadataSetParser; 1298 View view = parser.parseView(viewConfiguration); 1299 views.put(view.getName(), view); 1300 } 1301 1302 return views; 1303 } 1304 catch (Exception e) 1305 { 1306 throw new ConfigurationException("Unable to initialize the content view parser", e); 1307 } 1308 finally 1309 { 1310 LifecycleHelper.dispose(metadataSetParser); 1311 LifecycleHelper.dispose(viewParser); 1312 } 1313 } 1314 1315 /** 1316 * Configure the global validators for content type 1317 * @param config The content type configuration 1318 * @throws ConfigurationException if an error occurs 1319 */ 1320 protected void _configureGlobalValidators (Configuration config) throws ConfigurationException 1321 { 1322 _globalValidators = new ArrayList<>(); 1323 List<String> globalValidatorsToLookup = new ArrayList<>(); 1324 1325 Configuration globalValidatorsConfig = config.getChild("global-validators", true); 1326 globalValidatorsToLookup.addAll(_parseGlobalValidators(globalValidatorsConfig, config.getAttributeAsBoolean("include-from-supertype", true))); 1327 1328 Configuration overriddenConfig = getOverridenConfiguration(); 1329 if (overriddenConfig != null) 1330 { 1331 // Global validators into an overriden configuration are added to the original global validators 1332 globalValidatorsToLookup.addAll(_parseGlobalValidators(overriddenConfig.getChild("global-validators", true), false)); 1333 } 1334 1335 try 1336 { 1337 _globalValidatorsManager.initialize(); 1338 } 1339 catch (Exception e) 1340 { 1341 throw new ConfigurationException("Unable to initialize global validator manager", e); 1342 } 1343 1344 for (String validatorRole : globalValidatorsToLookup) 1345 { 1346 try 1347 { 1348 ContentValidator contentValidator = _globalValidatorsManager.lookup(validatorRole); 1349 contentValidator.setContentType(this); 1350 1351 _globalValidators.add(contentValidator); 1352 } 1353 catch (ComponentException e) 1354 { 1355 throw new ConfigurationException("Unable to lookup global validator role: '" + validatorRole + "' for content type: " + this.getId(), e); 1356 } 1357 } 1358 1359 } 1360 1361 /** 1362 * Parse the global validators 1363 * @param config the configuration 1364 * @param includeSuperTypeValidators true to include validators of super types 1365 * @return the role of global validators to be lookuped 1366 * @throws ConfigurationException if configuration is incorrect 1367 */ 1368 @SuppressWarnings("unchecked") 1369 protected List<String> _parseGlobalValidators(Configuration config, boolean includeSuperTypeValidators) throws ConfigurationException 1370 { 1371 List<String> gvRoles = new ArrayList<>(); 1372 1373 if (includeSuperTypeValidators) 1374 { 1375 for (String superTypeId : _superTypeIds) 1376 { 1377 ContentType cType = _cTypeEP.getExtension(superTypeId); 1378 _globalValidators.addAll(cType.getGlobalValidators()); 1379 } 1380 } 1381 1382 for (Configuration globalValidatorConfig : config.getChildren("global-validator")) 1383 { 1384 String globalValidatorId = __GLOBAL_VALIDATOR_ROLE_PREFIX + RandomStringUtils.randomAlphanumeric(10); 1385 String validatorClassName = globalValidatorConfig.getAttribute("class"); 1386 1387 try 1388 { 1389 Class validatorClass = Class.forName(validatorClassName); 1390 _globalValidatorsManager.addComponent(_pluginName, null, globalValidatorId, validatorClass, globalValidatorConfig); 1391 } 1392 catch (Exception e) 1393 { 1394 throw new ConfigurationException("Unable to instantiate global validator for class: " + validatorClassName, e); 1395 } 1396 1397 gvRoles.add(globalValidatorId); 1398 } 1399 1400 return gvRoles; 1401 } 1402 1403 /** 1404 * Configure the indexing model 1405 * @param config The main configuration 1406 * @throws ConfigurationException if an error occurred 1407 */ 1408 protected void _configureIndexingModel (Configuration config) throws ConfigurationException 1409 { 1410 Configuration indexConf = config.getChild("indexing-model", true); 1411 1412 Configuration overriddenConfig = getOverridenConfiguration(); 1413 if (overriddenConfig != null && overriddenConfig.getChild("indexing-model", false) != null) 1414 { 1415 // Indexing model is overriden (original configuration is ignored) 1416 indexConf = overriddenConfig.getChild("indexing-model", true); 1417 } 1418 1419 _indexingModel = new IndexingModel(); 1420 1421 boolean includeFromSuperType = indexConf.getAttributeAsBoolean("include-from-supertype", true); 1422 if (includeFromSuperType) 1423 { 1424 _indexingModel = _contentTypesHelper.getIndexingModel(_superTypeIds, new String[0]); 1425 } 1426 1427 boolean includeAll = indexConf.getAttributeAsBoolean("include-all", true); 1428 if (includeAll) 1429 { 1430 for (String metadataName : _metadata.keySet()) 1431 { 1432 MetadataDefinition definition = _metadata.get(metadataName); 1433 _indexingModel.addIndexingField(new DefaultMetadataIndexingField(metadataName, definition, metadataName)); 1434 } 1435 } 1436 1437 // Optionally add the semantic annotations to the indexing model. 1438 boolean includeSemanticAnnotations = indexConf.getAttributeAsBoolean("include-semantic-annotations", true); 1439 if (includeSemanticAnnotations) 1440 { 1441 _addSemanticAnnotations(indexConf, this); 1442 } 1443 1444 // Metadata fields. 1445 _configureMetadataIndexingFields(indexConf); 1446 1447 // Custom fields. 1448 _configureCustomIndexingFields(indexConf); 1449 1450 // Custom metadata fields. 1451 _configureCustomMetadataIndexingFields(indexConf); 1452 } 1453 1454 /** 1455 * Add semantic annotations as indexing fields to the indexing model. 1456 * @param indexConf the indexing model configuration. 1457 * @param holder the metadata holder (ContentType or MetadataDefinition) to scan for annotable metadata. 1458 */ 1459 protected void _addSemanticAnnotations(Configuration indexConf, MetadataDefinitionHolder holder) 1460 { 1461 // Get the semantic annotations in a multimap. 1462 Multimap<SemanticAnnotation, String> metadatas = HashMultimap.create(); 1463 _getSemanticAnnotations(holder, metadatas, ""); 1464 1465 // Add a custom indexing field for each annotation to the indexing model. 1466 for (SemanticAnnotation annotation : metadatas.keySet()) 1467 { 1468 Collection<String> metaPaths = metadatas.get(annotation); 1469 _indexingModel.addIndexingField(new SemanticAnnotationIndexingField(annotation, metaPaths, this)); 1470 } 1471 } 1472 1473 /** 1474 * Get the semantic annotations and their paths from the given metadata definition holder's sub-tree. 1475 * @param holder The current metadata definition holder. 1476 * @param annotations The map of semantic annotations and the corresponding metadata paths. 1477 * @param prefix The current prefix in the metadata tree (with a trailing slash, if applicable). 1478 */ 1479 protected void _getSemanticAnnotations(MetadataDefinitionHolder holder, Multimap<SemanticAnnotation, String> annotations, String prefix) 1480 { 1481 for (String metadataName : holder.getMetadataNames()) 1482 { 1483 String metaPath = prefix + metadataName; 1484 MetadataDefinition metaDef = holder.getMetadataDefinition(metadataName); 1485 if (metaDef instanceof AnnotableDefinition) 1486 { 1487 List<SemanticAnnotation> metaAnnotations = ((AnnotableDefinition) metaDef).getSemanticAnnotations(); 1488 for (SemanticAnnotation annotation : metaAnnotations) 1489 { 1490 annotations.put(annotation, metaPath); 1491 } 1492 } 1493 1494 _getSemanticAnnotations(metaDef, annotations, metaPath + ContentConstants.METADATA_PATH_SEPARATOR); 1495 } 1496 } 1497 1498 /** 1499 * Configure the metadata indexing fields. 1500 * @param indexConf the indexing model configuration. 1501 * @throws ConfigurationException if an error occurs. 1502 */ 1503 protected void _configureMetadataIndexingFields(Configuration indexConf) throws ConfigurationException 1504 { 1505 Configuration[] fieldsConf = indexConf.getChildren("metadata-field"); 1506 for (Configuration fieldConf : fieldsConf) 1507 { 1508 String metadataPath = fieldConf.getAttribute("path"); 1509 1510 MetadataDefinition metadataDef = _contentTypesHelper.getMetadataDefinition(metadataPath, this); 1511 if (metadataDef != null) 1512 { 1513 String fieldName = fieldConf.getAttribute("name", null); 1514 if (fieldName == null) 1515 { 1516 fieldName = StringUtils.substringBeforeLast(metadataPath, ContentConstants.METADATA_PATH_SEPARATOR); 1517 } 1518 1519 // TODO check if metadataDef.getType() is a primitive type (string, long, double, ..) ?? 1520 /*if (MetadataType.COMPOSITE == metadataDef.getType()) 1521 { 1522 throw new ConfigurationException("Indexing field of path '" + metadataPath + "' defined in content type '" + this.getId() + "' is not a valid path."); 1523 }*/ 1524 _indexingModel.addIndexingField(new DefaultMetadataIndexingField(fieldName, metadataDef, metadataPath)); 1525 } 1526 else 1527 { 1528 throw new ConfigurationException("Indexing field of path '" + metadataPath + "' defined in content type '" + this.getId() + "' is not a valid path.", fieldConf); 1529 } 1530 } 1531 } 1532 1533 /** 1534 * Configure the custom indexing fields. 1535 * @param indexConf the indexing model configuration. 1536 * @throws ConfigurationException if an error occurs. 1537 */ 1538 @SuppressWarnings("unchecked") 1539 protected void _configureCustomIndexingFields(Configuration indexConf) throws ConfigurationException 1540 { 1541 List<String> customFieldRoles = new ArrayList<>(); 1542 1543 Configuration[] customsConf = indexConf.getChildren("custom-field"); 1544 for (Configuration customConf : customsConf) 1545 { 1546 String className = customConf.getAttribute("class", null); 1547 if (className == null) 1548 { 1549 throw new ConfigurationException("A custom index field defined in content type '" + this.getId() + "' does not specifiy a class.", customConf); 1550 } 1551 1552 try 1553 { 1554 Class<CustomIndexingField> customFieldClass = (Class<CustomIndexingField>) Class.forName(className); 1555 String fieldRole = getId() + "-" + className; 1556 _customFieldManager.addComponent("cms", null, getId() + "-" + className, customFieldClass, customConf); 1557 1558 customFieldRoles.add(fieldRole); 1559 } 1560 catch (Exception e) 1561 { 1562 throw new ConfigurationException("Unable to instanciate custom indexing field for class: " + className, customConf, e); 1563 } 1564 } 1565 1566 try 1567 { 1568 _customFieldManager.initialize(); 1569 } 1570 catch (Exception e) 1571 { 1572 throw new ConfigurationException("Unable to initialize custom indexing field manager", e); 1573 } 1574 1575 for (String customFieldRole : customFieldRoles) 1576 { 1577 try 1578 { 1579 CustomIndexingField field = _customFieldManager.lookup(customFieldRole); 1580 _indexingModel.addIndexingField(field); 1581 } 1582 catch (ComponentException e) 1583 { 1584 throw new ConfigurationException("Unable to lookup custom indexing field with role: '" + customFieldRole + "' for content type: " + this.getId(), e); 1585 } 1586 } 1587 } 1588 1589 /** 1590 * Configure the custom metadata indexing fields. 1591 * @param indexConf the indexing model configuration. 1592 * @throws ConfigurationException if an error occurs. 1593 */ 1594 @SuppressWarnings("unchecked") 1595 protected void _configureCustomMetadataIndexingFields(Configuration indexConf) throws ConfigurationException 1596 { 1597 List<String> customMetadataFieldRoles = new ArrayList<>(); 1598 1599 int index = 0; 1600 Configuration[] customMetaConfs = indexConf.getChildren("custom-metadata-field"); 1601 for (Configuration customMetaConf : customMetaConfs) 1602 { 1603 String className = customMetaConf.getAttribute("class", null); 1604 if (className == null) 1605 { 1606 throw new ConfigurationException("A custom indexing field defined in content type '" + this.getId() + "' does not specifiy a class.", customMetaConf); 1607 } 1608 1609 try 1610 { 1611 DefaultConfiguration localConf = new DefaultConfiguration(customMetaConf, true); 1612 DefaultConfiguration cTypeConf = new DefaultConfiguration("contentType"); 1613 cTypeConf.setAttribute("id", this.getId()); 1614 localConf.addChild(cTypeConf); 1615 1616 // Use an index in the role to be able to use several times the same class in a content-type. 1617 String fieldRole = getId() + "-" + className + "-" + index; 1618 Class<CustomMetadataIndexingField> customMetaFieldClass = (Class<CustomMetadataIndexingField>) Class.forName(className); 1619 _customMetadataIndexingFieldManager.addComponent("cms", null, fieldRole, customMetaFieldClass, localConf); 1620 1621 customMetadataFieldRoles.add(fieldRole); 1622 1623 index++; 1624 } 1625 catch (Exception e) 1626 { 1627 throw new ConfigurationException("Unable to instanciate custom metadata indexing field for class: " + className, customMetaConf, e); 1628 } 1629 } 1630 1631 try 1632 { 1633 _customMetadataIndexingFieldManager.initialize(); 1634 } 1635 catch (Exception e) 1636 { 1637 throw new ConfigurationException("Unable to initialize custom metadata indexing field manager", e); 1638 } 1639 1640 for (String customMetaFieldRole : customMetadataFieldRoles) 1641 { 1642 try 1643 { 1644 CustomMetadataIndexingField field = _customMetadataIndexingFieldManager.lookup(customMetaFieldRole); 1645 _indexingModel.addIndexingField(field); 1646 } 1647 catch (ComponentException e) 1648 { 1649 throw new ConfigurationException("Unable to lookup custom indexing field with role: '" + customMetaFieldRole + "' for content type: " + this.getId(), e); 1650 } 1651 } 1652 } 1653 1654 /** 1655 * Parse the tags 1656 * @param configuration the configuration to use 1657 * @return the tags 1658 * @throws ConfigurationException if the configuration is not valid. 1659 */ 1660 protected Set<String> _parseTags (Configuration configuration) throws ConfigurationException 1661 { 1662 Set<String> tags = new HashSet<>(); 1663 1664 Configuration[] children = configuration.getChildren("tag"); 1665 for (Configuration tagConfig : children) 1666 { 1667 tags.add(tagConfig.getValue()); 1668 } 1669 1670 return tags; 1671 } 1672 1673 @Override 1674 public void postInitialize() throws Exception 1675 { 1676 _checkTitleAttribute(); 1677 1678 _checkContentAttributes(this, 0); 1679 1680 _resolveViewReferences(); 1681 1682 _computeIndexingModelReferences(); 1683 } 1684 1685 @SuppressWarnings("static-access") 1686 private void _checkTitleAttribute() throws ConfigurationException 1687 { 1688 if (!isAbstract() && !isMixin()) 1689 { 1690 ModelItem titleItem = null; 1691 if (hasModelItem(Content.ATTRIBUTE_TITLE)) 1692 { 1693 titleItem = getModelItem(Content.ATTRIBUTE_TITLE); 1694 } 1695 else 1696 { 1697 // The title attribute is mandatory for non abstract content types 1698 throw new ConfigurationException("An attribute 'title' in content type '" + getId() + "' should be defined."); 1699 } 1700 1701 // The title attribute should'nt be a group item 1702 if (!(titleItem instanceof ElementDefinition)) 1703 { 1704 throw new ConfigurationException("An attribute 'title' in content type '" + getId() + "' should be defined."); 1705 } 1706 1707 // The title attribute should be a string or a multilingual string 1708 ElementDefinition titleAttribute = (ElementDefinition) titleItem; 1709 String typeId = titleAttribute.getType().getId(); 1710 if (!ModelItemTypeConstants.STRING_TYPE_ID.equals(typeId) && !ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(typeId)) 1711 { 1712 throw new ConfigurationException("The attribute 'title' in content type '" + getId() + "' is defined with the type '" + typeId + "'. Only '" + ModelItemTypeConstants.STRING_TYPE_ID + "' and '" + ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID + "' are allowed"); 1713 } 1714 1715 // The title attribute should not be multiple 1716 if (titleAttribute.isMultiple()) 1717 { 1718 throw new ConfigurationException("The attribute 'title' in content type '" + getId() + "' should not be multiple."); 1719 } 1720 1721 // The title attribute must be mandatory 1722 Validator titleValidator = titleAttribute.getValidator(); 1723 if (titleValidator == null || !(boolean) titleValidator.getConfiguration().get("mandatory")) 1724 { 1725 throw new ConfigurationException("The attribute 'title' in content type '" + getId() + "' should be mandatory."); 1726 } 1727 } 1728 } 1729 1730 /** 1731 * Check for each content attribute: the content type id, the mutual references and the default values 1732 * @param modelItemContainer the {@link ModelItemContainer} to check 1733 * @param repeaterLevel the current nested level of repeaters, 0 if the current attribute isn't in a repeater. 1734 * @throws ConfigurationException if a content attribute has an invalid configuration 1735 */ 1736 protected void _checkContentAttributes(ModelItemContainer modelItemContainer, int repeaterLevel) throws ConfigurationException 1737 { 1738 for (ModelItem modelItem : modelItemContainer.getModelItems()) 1739 { 1740 if (ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(modelItem.getType().getId())) 1741 { 1742 ContentAttributeDefinition definition = (ContentAttributeDefinition) modelItem; 1743 _checkContentTypeId(definition); 1744 _checkMutualReferences(definition, repeaterLevel); 1745 definition.checkDefaultValue(); 1746 } 1747 1748 // Check sub-attributes 1749 if (modelItem instanceof ModelItemContainer) 1750 { 1751 // Increment the repeater level if the current attribute is a repeater. 1752 int newRepeaterLevel = (modelItem instanceof org.ametys.plugins.repository.model.RepeaterDefinition) ? repeaterLevel + 1 : repeaterLevel; 1753 _checkContentAttributes((ModelItemContainer) modelItem, newRepeaterLevel); 1754 } 1755 } 1756 } 1757 1758 /** 1759 * Check the content type id of the given content attribute definition 1760 * @param definition the definition to check 1761 * @throws ConfigurationException if the given content attribute references an invalid or non-existing content type. 1762 */ 1763 protected void _checkContentTypeId(ContentAttributeDefinition definition) throws ConfigurationException 1764 { 1765 String contentTypeId = definition.getContentTypeId(); 1766 1767 if (StringUtils.isNotBlank(contentTypeId) && !_cTypeEP.hasExtension(contentTypeId)) 1768 { 1769 throw new ConfigurationException("The content attribute of path " + definition.getPath() + " in content type " + getId() + " references a nonexistent content-type: '" + contentTypeId + "'"); 1770 } 1771 } 1772 1773 /** 1774 * Check the mutual reference declaration of the given content attribute definition 1775 * @param definition the definition to check 1776 * @param repeaterLevel the current nested level of repeaters, 0 if the current attribute isn't in a repeater. 1777 * @throws ConfigurationException if there is a problem with mutual reference declaration 1778 */ 1779 protected void _checkMutualReferences(ContentAttributeDefinition definition, int repeaterLevel) throws ConfigurationException 1780 { 1781 String contentTypeId = definition.getContentTypeId(); 1782 String invertRelationPath = definition.getInvertRelationPath(); 1783 1784 if (StringUtils.isNotEmpty(invertRelationPath)) 1785 { 1786 String currentAttributePath = definition.getPath(); 1787 1788 if (StringUtils.isEmpty(contentTypeId)) 1789 { 1790 throw new ConfigurationException("The attribute at path '" + currentAttributePath + "' in content type '" + getId() + "' is declared as a mutual relationship, a content type is required."); 1791 } 1792 1793 // Do not check the extension existence, this is already done in _checkContentAttribute method 1794 ContentType contentType = _cTypeEP.getExtension(contentTypeId); 1795 1796 if (repeaterLevel > 0 && definition.isMultiple()) 1797 { 1798 throw new ConfigurationException("The attribute at path '" + currentAttributePath + "' in content type '" + getId() + "' is declared as a mutual relationship, it can't be multiple AND in a repeater."); 1799 } 1800 else if (repeaterLevel >= 2) 1801 { 1802 throw new ConfigurationException("The attribute at path '" + currentAttributePath + "' in content type '" + getId() + "' is declared as a mutual relationship, it can't be in two levels of repeaters."); 1803 } 1804 1805 try 1806 { 1807 ModelItem invertRelationDefinition = contentType.getModelItem(invertRelationPath); 1808 1809 // Ensure that the referenced attribute is of type content. 1810 if (!ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(invertRelationDefinition.getType().getId())) 1811 { 1812 throw new ConfigurationException("Mutual relationship: the attribute at path '" + invertRelationPath + "' of type " + contentTypeId + " is not of type Content."); 1813 } 1814 1815 String invertCTypeId = ((ContentAttributeDefinition) invertRelationDefinition).getContentTypeId(); 1816 String invertPath = ((ContentAttributeDefinition) invertRelationDefinition).getInvertRelationPath(); 1817 1818 // Ensure that the referenced attribute's content type is compatible with the current type. 1819 if (!_cTypeEP.isSameOrDescendant(getId(), invertCTypeId)) 1820 { 1821 throw new ConfigurationException("Mutual relationship: the attribute at path " + invertRelationPath + " of type " + contentTypeId + " references an incompatible type: " + StringUtils.defaultString(invertCTypeId, "<null>")); 1822 } 1823 1824 // Ensure that the referenced attribute references this attribute. 1825 if (!currentAttributePath.equals(invertPath)) 1826 { 1827 throw new ConfigurationException("Mutual relationship: the attribute at path " + currentAttributePath + " of type " + getId() + " references the attribute '" + invertRelationPath + "' of type " + contentTypeId + " but the latter does not reference it back."); 1828 } 1829 } 1830 catch (UndefinedItemPathException e) 1831 { 1832 // Ensure the referenced attribute presence. 1833 throw new ConfigurationException("Mutual relationship: the attribute at path '" + invertRelationPath + "' doesn't exist for type " + contentTypeId); 1834 } 1835 } 1836 } 1837 1838 /** 1839 * Resolve the temporary view references 1840 * @throws ConfigurationException if a view reference has a configuration error (the content attribute nesting the reference does not specify any content type, the view does not exist, ...) 1841 */ 1842 protected void _resolveViewReferences() throws ConfigurationException 1843 { 1844 for (String viewName : getViewNames()) 1845 { 1846 View view = getView(viewName); 1847 _resolveViewReferences(view, viewName); 1848 } 1849 } 1850 1851 /** 1852 * Resolve the temporary view references in the given {@link ViewItemAccessor} 1853 * @param viewItemAccessor the {@link ViewItemAccessor} 1854 * @param currentViewName the name of the current view (to avoid views referencing themselves) 1855 * @throws ConfigurationException if a view reference has a configuration error (the content attribute nesting the reference does not specify any content type, the view does not exist, ...) 1856 */ 1857 protected void _resolveViewReferences(ViewItemAccessor viewItemAccessor, String currentViewName) throws ConfigurationException 1858 { 1859 boolean hasResolvedReferences = false; 1860 List<ViewItem> viewItemsWithResolvedReferences = new ArrayList<>(); 1861 for (ViewItem viewItem : viewItemAccessor.getViewItems()) 1862 { 1863 if (viewItem instanceof TemporaryViewReference) 1864 { 1865 assert viewItemAccessor instanceof ViewElement; 1866 1867 ElementDefinition definition = ((ViewElement) viewItemAccessor).getDefinition(); 1868 assert definition instanceof ContentAttributeDefinition; 1869 1870 String contentTypeId = ((ContentAttributeDefinition) definition).getContentTypeId(); 1871 if (contentTypeId != null) 1872 { 1873 Set<String> ancestors = _contentTypesHelper.getAncestors(this.getId()); 1874 if (!(ancestors.contains(contentTypeId) && currentViewName.equals(viewItem.getName()))) 1875 { 1876 ContentType contentType = _cTypeEP.getExtension(contentTypeId); 1877 View view = contentType.getView(viewItem.getName()); 1878 if (view != null) 1879 { 1880 hasResolvedReferences = true; 1881 viewItemsWithResolvedReferences.addAll(view.getViewItems()); 1882 } 1883 else 1884 { 1885 throw new ConfigurationException("The view '" + viewItem.getName() + "' does not exist in content type '" + contentTypeId + "' referenced by the attribute named '" + definition.getName() + "'."); 1886 } 1887 } 1888 else 1889 { 1890 throw new ConfigurationException("The view '" + viewItem.getName() + "' cannot make a reference to itself."); 1891 } 1892 } 1893 else 1894 { 1895 throw new ConfigurationException("The attribute '" + definition.getName() + "' doesn't reference any content type. It is not possible to add the items of the view '" + viewItem.getName() + "'."); 1896 } 1897 } 1898 else 1899 { 1900 if (viewItem instanceof ViewItemAccessor) 1901 { 1902 _resolveViewReferences((ViewItemAccessor) viewItem, currentViewName); 1903 } 1904 viewItemsWithResolvedReferences.add(viewItem); 1905 } 1906 } 1907 1908 if (hasResolvedReferences) 1909 { 1910 viewItemAccessor.clear(); 1911 viewItemAccessor.addViewItems(viewItemsWithResolvedReferences); 1912 } 1913 } 1914 1915 /** 1916 * Browse the indexing model and compute indexing field references. 1917 */ 1918 protected void _computeIndexingModelReferences() 1919 { 1920 // Impacted ContentType -> local IndexingField name -> path to impacted content. 1921 Map<String, Map<String, List<String>>> references = new HashMap<>(); 1922 1923 for (IndexingField field : _indexingModel.getFields()) 1924 { 1925 if (field instanceof MetadataIndexingField) 1926 { 1927 String metadataPath = ((MetadataIndexingField) field).getMetadataPath(); 1928 1929 List<MetadataDefinition> definitions = getIndexingFieldDefinitions(this, metadataPath); 1930 1931 List<String> joinPaths = new ArrayList<>(); 1932 boolean localContentType = true; 1933 StringBuilder currentContentPath = new StringBuilder(); 1934 for (MetadataDefinition definition : definitions) 1935 { 1936 if (currentContentPath.length() > 0) 1937 { 1938 currentContentPath.append(ContentConstants.METADATA_PATH_SEPARATOR); 1939 } 1940 currentContentPath.append(definition.getName()); 1941 1942 if (definition.getType() == MetadataType.CONTENT || definition.getType() == MetadataType.SUB_CONTENT) 1943 { 1944 if (!localContentType) 1945 { 1946 joinPaths.add(currentContentPath.toString()); 1947 currentContentPath.setLength(0); 1948 1949 String cTypeId = definition.getContentType(); 1950 1951 Map<String, List<String>> cTypeRefs; 1952 if (references.containsKey(cTypeId)) 1953 { 1954 cTypeRefs = references.get(cTypeId); 1955 } 1956 else 1957 { 1958 cTypeRefs = new LinkedHashMap<>(); 1959 references.put(cTypeId, cTypeRefs); 1960 } 1961 1962 cTypeRefs.put(field.getName(), new ArrayList<>(joinPaths)); 1963 } 1964 1965 localContentType = false; 1966 } 1967 } 1968 } 1969 } 1970 1971 _indexingModel.setReferences(references); 1972 } 1973 1974 /** 1975 * Get the list of metadata definitions "traversed" from the initial content type to the given metadata. 1976 * @param initialContentType the initial content type. 1977 * @param metadataPath the compound metadata path. 1978 * @return the list of metadata definitions. 1979 */ 1980 protected List<MetadataDefinition> getIndexingFieldDefinitions(ContentType initialContentType, String metadataPath) 1981 { 1982 List<MetadataDefinition> definitions = new ArrayList<>(); 1983 1984 String[] pathSegments = StringUtils.split(metadataPath, ContentConstants.METADATA_PATH_SEPARATOR); 1985 1986 if (pathSegments.length > 0) 1987 { 1988 IndexingModel indexingModel = _contentTypesHelper.getIndexingModel(new String[] {initialContentType.getId()}, new String[0]); 1989 1990 IndexingField refField = indexingModel.getField(pathSegments[0]); 1991 1992 MetadataDefinition metadataDef = null; 1993 if (refField != null && refField instanceof CustomMetadataIndexingField) 1994 { 1995 metadataDef = ((CustomMetadataIndexingField) refField).getMetadataDefinition(); 1996 } 1997 else 1998 { 1999 metadataDef = initialContentType.getMetadataDefinition(pathSegments[0]); 2000 } 2001 2002 if (metadataDef != null) 2003 { 2004 definitions.add(metadataDef); 2005 } 2006 2007 for (int i = 1; i < pathSegments.length && metadataDef != null; i++) 2008 { 2009 if (metadataDef.getType() == MetadataType.CONTENT || metadataDef.getType() == MetadataType.SUB_CONTENT) 2010 { 2011 String refCTypeId = metadataDef.getContentType(); 2012 if (refCTypeId != null && _cTypeEP.hasExtension(refCTypeId)) 2013 { 2014 ContentType refCType = _cTypeEP.getExtension(refCTypeId); 2015 2016 List<MetadataDefinition> followingDefs = getIndexingFieldDefinitions(refCType, StringUtils.join(pathSegments, '/', i, pathSegments.length)); 2017 definitions.addAll(followingDefs); 2018 2019 return definitions; 2020 } 2021 } 2022 else 2023 { 2024 refField = indexingModel.getField(pathSegments[i]); 2025 if (refField != null && refField instanceof CustomMetadataIndexingField) 2026 { 2027 metadataDef = ((CustomMetadataIndexingField) refField).getMetadataDefinition(); 2028 } 2029 else 2030 { 2031 metadataDef = metadataDef.getMetadataDefinition(pathSegments[i]); 2032 } 2033 definitions.add(metadataDef); 2034 } 2035 } 2036 } 2037 2038 return definitions; 2039 } 2040 2041 @Override 2042 public List<ContentValidator> getGlobalValidators() 2043 { 2044 return Collections.unmodifiableList(_globalValidators); 2045 } 2046 2047 @Override 2048 public RichTextUpdater getRichTextUpdater() 2049 { 2050 return _richTextUpdater; 2051 } 2052 2053 @Override 2054 public Set<String> getMetadataNames() 2055 { 2056 return Collections.unmodifiableSet(_metadata.keySet()); 2057 } 2058 2059 @Override 2060 public MetadataDefinition getMetadataDefinition(String metadataName) 2061 { 2062 return _metadata.get(metadataName); 2063 } 2064 2065 @Override 2066 public boolean hasMetadataDefinition(String metadataName) 2067 { 2068 return _metadata.containsKey(metadataName); 2069 } 2070 2071 @Override 2072 public MetadataDefinition getMetadataDefinitionByPath(String metadataPath) 2073 { 2074 String[] pathSegments = StringUtils.split(metadataPath, ContentConstants.METADATA_PATH_SEPARATOR); 2075 2076 if (pathSegments.length == 0) 2077 { 2078 return null; 2079 } 2080 2081 MetadataDefinition metadataDef = _metadata.get(pathSegments[0]); 2082 2083 for (int i = 1; i < pathSegments.length && metadataDef != null; i++) 2084 { 2085 metadataDef = metadataDef.getMetadataDefinition(pathSegments[i]); 2086 } 2087 2088 return metadataDef; 2089 } 2090 2091 @Override 2092 public IndexingModel getIndexingModel() 2093 { 2094 return _indexingModel; 2095 } 2096 2097 @Override 2098 public boolean canRead(Content content, MetadataDefinition metadataDef) throws AmetysRepositoryException 2099 { 2100 Restrictions restrictions = _getRestrictionsForPath(metadataDef); 2101 2102 FirstRestrictionsChecksState state = _restrictedModelItemHelper._doFirstRestrictionsChecks(content, restrictions, true); 2103 if (!FirstRestrictionsChecksState.UNKNOWN.equals(state)) 2104 { 2105 return FirstRestrictionsChecksState.TRUE.equals(state); 2106 } 2107 2108 String[] pathSegments = StringUtils.split(metadataDef.getId(), ContentConstants.METADATA_PATH_SEPARATOR); 2109 if (pathSegments.length > 1) 2110 { 2111 // Check read access on parent metadata definition 2112 String parentMetadataPath = metadataDef.getId().substring(0, metadataDef.getId().lastIndexOf(ContentConstants.METADATA_PATH_SEPARATOR)); 2113 MetadataDefinition parentMetadataDef = _contentTypesHelper.getMetadataDefinition(parentMetadataPath, content); 2114 return canRead(content, parentMetadataDef); 2115 } 2116 2117 return true; 2118 } 2119 2120 @Override 2121 public boolean canWrite(Content content, MetadataDefinition metadataDef) throws AmetysRepositoryException 2122 { 2123 Restrictions restrictions = _getRestrictionsForPath(metadataDef); 2124 2125 FirstRestrictionsChecksState state = _restrictedModelItemHelper._doFirstRestrictionsChecks(content, restrictions, false); 2126 if (!FirstRestrictionsChecksState.UNKNOWN.equals(state)) 2127 { 2128 return FirstRestrictionsChecksState.TRUE.equals(state); 2129 } 2130 2131 String[] pathSegments = StringUtils.split(metadataDef.getId(), ContentConstants.METADATA_PATH_SEPARATOR); 2132 if (pathSegments.length > 1) 2133 { 2134 // Check write access on parent metadata definition 2135 String parentMetadataPath = metadataDef.getId().substring(0, metadataDef.getId().lastIndexOf(ContentConstants.METADATA_PATH_SEPARATOR)); 2136 MetadataDefinition parentMetadataDef = _contentTypesHelper.getMetadataDefinition(parentMetadataPath, content); 2137 return canWrite(content, parentMetadataDef); 2138 } 2139 2140 return canRead(content, metadataDef); 2141 } 2142 2143 @Override 2144 public Set<String> getTags() 2145 { 2146 return Collections.unmodifiableSet(_tags); 2147 } 2148 2149 @Override 2150 public boolean hasTag(String tagName) 2151 { 2152 return _tags.contains(tagName); 2153 } 2154 2155 @Override 2156 public boolean isPrivate() 2157 { 2158 return hasTag(TAG_PRIVATE); 2159 } 2160 2161 @Override 2162 public boolean isAbstract() 2163 { 2164 return _abstract; 2165 } 2166 2167 @Override 2168 public boolean isSimple() 2169 { 2170 return _isSimple; 2171 } 2172 2173 @Override 2174 public boolean isReferenceTable() 2175 { 2176 return hasTag(TAG_REFERENCE_TABLE) || hasTag(TAG_RENDERABLE_FERENCE_TABLE); 2177 } 2178 2179 @Override 2180 public boolean isMultilingual() 2181 { 2182 return _isMultilingual; 2183 } 2184 2185 @Override 2186 public boolean isMixin() 2187 { 2188 return hasTag(TAG_MIXIN); 2189 } 2190 2191 @Override 2192 public Optional<String> getDefaultWorkflowName() 2193 { 2194 return _defaultWorkflowName; 2195 } 2196 2197 @Override 2198 public String getRight() 2199 { 2200 return _right; 2201 } 2202 2203 @Override 2204 public void saxContentTypeAdditionalData(org.xml.sax.ContentHandler contentHandler, Content content) throws AmetysRepositoryException, SAXException 2205 { 2206 // Nothing 2207 } 2208 2209 @Override 2210 public Map<String, Object> getAdditionalData(Content content) 2211 { 2212 return new HashMap<>(); 2213 } 2214 2215 /** 2216 * Retrieves the restrictions for a given path. 2217 * @param metadataDef the metadata definition. 2218 * @return the restrictions or <code>null</code> if not found. 2219 */ 2220 protected Restrictions _getRestrictionsForPath(MetadataDefinition metadataDef) 2221 { 2222 if (metadataDef != null && metadataDef instanceof RestrictedDefinition) 2223 { 2224 return ((RestrictedDefinition) metadataDef).getRestrictions(); 2225 } 2226 2227 // Not found 2228 return null; 2229 } 2230 2231 @Override 2232 public String toString() 2233 { 2234 return "'" + getId() + "'"; 2235 } 2236 2237 /** 2238 * Restricted definition. 2239 * @deprecated use {@link RestrictedModelItem} instead 2240 */ 2241 @Deprecated 2242 protected interface RestrictedDefinition 2243 { 2244 /** 2245 * Provides the restrictions. 2246 * @return the restrictions. 2247 */ 2248 Restrictions getRestrictions(); 2249 } 2250 2251 /** 2252 * Definition with semantic annotations 2253 */ 2254 protected interface AnnotableDefinition 2255 { 2256 /** 2257 * Provides the semantic annotations 2258 * @return the semantic annotations 2259 */ 2260 List<SemanticAnnotation> getSemanticAnnotations(); 2261 2262 /** 2263 * Set the semantic annotations 2264 * @param annotations the semantic annotations to set 2265 */ 2266 void setSemanticAnnotations(List<SemanticAnnotation> annotations); 2267 } 2268 2269 /** 2270 * Internal {@link MetadataDefinition} storage contains instances of this class. 2271 * @deprecated Use {@link AttributeDefinition} instead 2272 */ 2273 @Deprecated 2274 protected static class RestrictedMetadataDefinition extends MetadataDefinition implements RestrictedDefinition 2275 { 2276 /** Restrictions. */ 2277 protected Restrictions _restrictions = new Restrictions(); 2278 2279 @Override 2280 public Restrictions getRestrictions() 2281 { 2282 return _restrictions; 2283 } 2284 } 2285 2286 /** 2287 * Internal {@link MetadataDefinition} storage contains instances of this class. 2288 * @deprecated Use {@link RichTextAttributeDefinition} instead 2289 */ 2290 @Deprecated 2291 protected static class RestrictedRichTextDefinition extends RichTextMetadataDefinition implements RestrictedDefinition 2292 { 2293 /** Restrictions. */ 2294 protected Restrictions _restrictions = new Restrictions(); 2295 2296 @Override 2297 public Restrictions getRestrictions() 2298 { 2299 return _restrictions; 2300 } 2301 } 2302 2303 /** 2304 * Internal {@link org.ametys.cms.contenttype.RepeaterDefinition} storage contains instances of this class. 2305 * @deprecated Use {@link ContentRestrictedRepeaterDefinition} instead 2306 */ 2307 @Deprecated 2308 protected static class RestrictedRepeaterDefinition extends org.ametys.cms.contenttype.RepeaterDefinition implements RestrictedDefinition 2309 { 2310 /** Restrictions. */ 2311 protected Restrictions _restrictions = new Restrictions(); 2312 2313 @Override 2314 public Restrictions getRestrictions() 2315 { 2316 return _restrictions; 2317 } 2318 } 2319 2320 /** 2321 * {@link RestrictedMetadataDefinition} and {@link RestrictedRepeaterDefinition} parser. 2322 * @deprecated Use {@link ContentAttributeDefinitionParser} instead 2323 */ 2324 @Deprecated 2325 protected class MetadataAndRepeaterDefinitionParser extends AbstractParameterParser<MetadataDefinition, MetadataType> 2326 { 2327 /** Parent prefix. */ 2328 protected String _parentPrefix = ""; 2329 2330 /** 2331 * Creates an {@link MetadataAndRepeaterDefinitionParser}. 2332 * @param enumeratorManager the enumerator component manager. 2333 * @param validatorManager the validator component manager. 2334 */ 2335 public MetadataAndRepeaterDefinitionParser(ThreadSafeComponentManager<org.ametys.runtime.parameter.Enumerator> enumeratorManager, ThreadSafeComponentManager<Validator> validatorManager) 2336 { 2337 super(enumeratorManager, validatorManager); 2338 } 2339 2340 @Override 2341 protected MetadataDefinition _createParameter(Configuration metadataConfiguration) throws ConfigurationException 2342 { 2343 String defName = metadataConfiguration.getName(); 2344 2345 if (defName.equals("metadata")) 2346 { 2347 String defType = metadataConfiguration.getAttribute("type"); 2348 if (defType.equals("rich-text") || defType.equals("html-rich-text")) 2349 { 2350 return new RestrictedRichTextDefinition(); 2351 } 2352 else 2353 { 2354 return new RestrictedMetadataDefinition(); 2355 } 2356 2357 } 2358 else if (defName.equals("repeater")) 2359 { 2360 return new RestrictedRepeaterDefinition(); 2361 } 2362 2363 throw new ConfigurationException("Unsupported metadata or repeater configuration", metadataConfiguration); 2364 } 2365 2366 @Override 2367 protected String _parseId(Configuration metadataConfiguration) throws ConfigurationException 2368 { 2369 String metadataName = metadataConfiguration.getAttribute("name"); 2370 2371 if (!metadataName.matches("^[a-zA-Z]((?!__)[a-zA-Z0-9_-])*$")) 2372 { 2373 throw new ConfigurationException("Invalid metadata name: " + metadataName + ". The metadata name must start with a letter and must contain only letters, digits, underscore or dash characters.", metadataConfiguration); 2374 } 2375 2376 return _parentPrefix + (_parentPrefix.length() > 0 ? ContentConstants.METADATA_PATH_SEPARATOR : "") + metadataName; 2377 } 2378 2379 @Override 2380 protected MetadataType _parseType(Configuration metadataConfiguration) throws ConfigurationException 2381 { 2382 if (metadataConfiguration.getName().equals("repeater")) 2383 { 2384 // A repeater is a composite 2385 return MetadataType.COMPOSITE; 2386 } 2387 else if (metadataConfiguration.getAttribute("type").equals("html-rich-text")) 2388 { 2389 return MetadataType.RICH_TEXT; 2390 } 2391 2392 try 2393 { 2394 return MetadataType.valueOf(metadataConfiguration.getAttribute("type").toUpperCase().replaceAll("-", "_")); 2395 } 2396 catch (IllegalArgumentException e) 2397 { 2398 throw new ConfigurationException("Invalid type", metadataConfiguration, e); 2399 } 2400 } 2401 2402 @Override 2403 protected I18nizableText _parseI18nizableText(Configuration config, String pluginName, String name) throws ConfigurationException 2404 { 2405 // Override i18n parsing to use the default catalog (which can be application for automatic content types) 2406 return I18nizableText.parseI18nizableText(config.getChild(name), getDefaultCatalog()); 2407 } 2408 2409 @Override 2410 protected Object _parseDefaultValue(Configuration metadataConfiguration, MetadataDefinition metadataDef) 2411 { 2412 String value; 2413 2414 Configuration childNode = metadataConfiguration.getChild("default-value", false); 2415 if (childNode == null) 2416 { 2417 value = null; 2418 } 2419 else 2420 { 2421 value = childNode.getValue(""); 2422 } 2423 2424 return value; 2425 } 2426 2427 @Override 2428 protected void _additionalParsing(ServiceManager manager, String pluginName, Configuration metadataConfiguration, String metadataId, MetadataDefinition metadataDefinition) throws ConfigurationException 2429 { 2430 super._additionalParsing(manager, pluginName, metadataConfiguration, metadataId, metadataDefinition); 2431 String metadataName = metadataConfiguration.getAttribute("name"); 2432 2433 metadataDefinition.setReferenceContentType(_id); 2434 metadataDefinition.setName(metadataName); 2435 metadataDefinition.setMultiple(metadataConfiguration.getAttributeAsBoolean("multiple", false)); 2436 // Use default transformer (docbook) 2437 metadataDefinition.setRichTextTransformer(_richTextTransformer); 2438 // Use docbook outgoing consistency extractor 2439 metadataDefinition.setRichTextOutgoingReferencesExtractor(_richTextOutgoingReferencesExtractor); 2440 2441 if (metadataDefinition instanceof org.ametys.cms.contenttype.RepeaterDefinition) 2442 { 2443 org.ametys.cms.contenttype.RepeaterDefinition repeaterDefinition = (org.ametys.cms.contenttype.RepeaterDefinition) metadataDefinition; 2444 2445 _parseRepeaterDefinition(manager, pluginName, metadataConfiguration, repeaterDefinition); 2446 } 2447 2448 if (metadataDefinition instanceof AnnotableDefinition) 2449 { 2450 AnnotableDefinition definitionWithSemAnnotations = (AnnotableDefinition) metadataDefinition; 2451 2452 _parseDefinitionWithAnnotations(manager, pluginName, metadataConfiguration, definitionWithSemAnnotations); 2453 } 2454 2455 _restrictedModelItemHelper.populateRestrictions(metadataConfiguration, ((RestrictedDefinition) metadataDefinition).getRestrictions()); 2456 2457 if (metadataDefinition.getType() == MetadataType.CONTENT || metadataDefinition.getType() == MetadataType.SUB_CONTENT) 2458 { 2459 // Content metadata: parse and set the content type restriction. 2460 String contentType = metadataConfiguration.getAttribute("contentType", null); 2461 if (StringUtils.isNotEmpty(contentType)) 2462 { 2463 metadataDefinition.setContentType(contentType); 2464 } 2465 2466 if (metadataDefinition.getType() == MetadataType.CONTENT) 2467 { 2468 _parseContentRelations(metadataConfiguration, metadataDefinition, contentType); 2469 } 2470 } 2471 else if (metadataDefinition.getType() == MetadataType.COMPOSITE) 2472 { 2473 for (Configuration childConfig : metadataConfiguration.getChildren()) 2474 { 2475 String childName = childConfig.getName(); 2476 2477 if (childName.equals("metadata") || childName.equals("repeater")) 2478 { 2479 String oldParentPrefix = _parentPrefix; 2480 // Stack new parent prefix 2481 _parentPrefix = _parentPrefix + (_parentPrefix.length() > 0 ? ContentConstants.METADATA_PATH_SEPARATOR : "") + metadataName; 2482 MetadataDefinition subMetaDef = parseParameter(manager, pluginName, childConfig); 2483 // Restore parent prefix 2484 _parentPrefix = oldParentPrefix; 2485 2486 if (!metadataDefinition.addMetadata(subMetaDef)) 2487 { 2488 throw new ConfigurationException("Metadata with name " + subMetaDef.getName() + " is already defined", childConfig); 2489 } 2490 } 2491 } 2492 } 2493 } 2494 2495 /** 2496 * Parse content mutual relations. 2497 * @param metadataConfiguration the metadata configuration. 2498 * @param metadataDefinition the metadata definition to fill. 2499 * @param contentType the content type. 2500 */ 2501 protected void _parseContentRelations(Configuration metadataConfiguration, MetadataDefinition metadataDefinition, String contentType) 2502 { 2503 String invert = metadataConfiguration.getAttribute("invert", null); 2504 if (StringUtils.isNotEmpty(invert)) 2505 { 2506 metadataDefinition.setInvertRelationPath(invert); 2507 2508 boolean forceInvert = metadataConfiguration.getAttributeAsBoolean("forceInvert", false); 2509 metadataDefinition.setForceInvert(forceInvert); 2510 } 2511 } 2512 2513 /** 2514 * Parses the repeater definition. 2515 * @param manager the service manager. 2516 * @param pluginName the plugin name declaring this parameter. 2517 * @param metadataConfiguration the metadata configuration to use. 2518 * @param repeaterDefinition the repeater definition. 2519 * @throws ConfigurationException if the configuration is not valid. 2520 */ 2521 protected void _parseRepeaterDefinition(ServiceManager manager, String pluginName, Configuration metadataConfiguration, org.ametys.cms.contenttype.RepeaterDefinition repeaterDefinition) throws ConfigurationException 2522 { 2523 repeaterDefinition.setAddLabel(_parseI18nizableText(metadataConfiguration, pluginName, "add-label")); 2524 repeaterDefinition.setDeleteLabel(_parseI18nizableText(metadataConfiguration, pluginName, "del-label")); 2525 repeaterDefinition.setHeaderLabel(metadataConfiguration.getChild("header-label").getValue(null)); 2526 repeaterDefinition.setInitialSize(metadataConfiguration.getAttributeAsInteger("initial-size", 0)); 2527 repeaterDefinition.setMinSize(metadataConfiguration.getAttributeAsInteger("min-size", 0)); 2528 repeaterDefinition.setMaxSize(metadataConfiguration.getAttributeAsInteger("max-size", -1)); 2529 } 2530 2531 /** 2532 * Parses the definition with semantic annotations. 2533 * @param manager the service manager. 2534 * @param pluginName the plugin name declaring this parameter. 2535 * @param metadataConfiguration the metadata configuration to use. 2536 * @param annotableDefinition the metadata definition 2537 * @throws ConfigurationException if the configuration is not valid. 2538 */ 2539 protected void _parseDefinitionWithAnnotations(ServiceManager manager, String pluginName, Configuration metadataConfiguration, AnnotableDefinition annotableDefinition) throws ConfigurationException 2540 { 2541 Configuration annotationsConfiguration = metadataConfiguration.getChild("annotations"); 2542 List<SemanticAnnotation> semAnnotations = _parseSemAnnotations(pluginName, annotationsConfiguration); 2543 annotableDefinition.setSemanticAnnotations(semAnnotations); 2544 } 2545 2546 /** 2547 * Extract the list of the declared annotations 2548 * @param pluginName the plugin name declaring this parameter. 2549 * @param annotationsConfiguration the annotations configuration to use. 2550 * @return the list of the declared annotations 2551 * @throws ConfigurationException if the configuration is not valid. 2552 */ 2553 protected List<SemanticAnnotation> _parseSemAnnotations(String pluginName, Configuration annotationsConfiguration) throws ConfigurationException 2554 { 2555 List<SemanticAnnotation> annotations = new ArrayList<>(); 2556 2557 for (Configuration annotationConfig : annotationsConfiguration.getChildren("annotation")) 2558 { 2559 String id = annotationConfig.getAttribute("name"); 2560 2561 if (!_getAnnotationNamePattern().matcher(id).matches()) 2562 { 2563 throw new ConfigurationException("Invalid annonation name '" + id + "'. This value is not permitted: only [a-zA-Z][a-zA-Z0-9-_]* are allowed."); 2564 } 2565 2566 I18nizableText label = _parseI18nizableText(annotationConfig, pluginName, "label"); 2567 I18nizableText description = _parseI18nizableText(annotationConfig, pluginName, "description"); 2568 annotations.add(new SemanticAnnotation(id, label, description)); 2569 } 2570 2571 return annotations; 2572 } 2573 2574 /** 2575 * Get the annotation name pattern to test validity. 2576 * @return The annotation name pattern. 2577 */ 2578 protected Pattern _getAnnotationNamePattern() 2579 { 2580 if (__annotationNamePattern == null) 2581 { 2582 // [a-zA-Z][a-zA-Z0-9_]* 2583 __annotationNamePattern = Pattern.compile("[a-z][a-z0-9-_]*", Pattern.CASE_INSENSITIVE); 2584 } 2585 2586 return __annotationNamePattern; 2587 } 2588 } 2589 2590 public Optional<ContentAttributeDefinition> getParentAttributeDefinition() 2591 { 2592 return Optional.ofNullable(_parentAttributeDefinition); 2593 } 2594 2595 public Collection<ModelItem> getModelItems() 2596 { 2597 return Collections.unmodifiableCollection(_modelItems.values()); 2598 } 2599 2600 public Set<String> getViewNames(boolean includeInternals) 2601 { 2602 if (includeInternals) 2603 { 2604 return Collections.unmodifiableSet(_views.keySet()); 2605 } 2606 else 2607 { 2608 return _views.entrySet() 2609 .stream() 2610 .filter(entry -> !entry.getValue().isInternal()) 2611 .map(Map.Entry::getKey) 2612 .collect(Collectors.toSet()); 2613 } 2614 } 2615 2616 public View getView(String viewName) 2617 { 2618 return _views.get(viewName); 2619 } 2620 2621 public String getFamilyId() 2622 { 2623 return ContentTypeExtensionPoint.ROLE; 2624 } 2625}