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