001/* 002 * Copyright 2020 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.IOException; 019import java.io.InputStream; 020import java.util.Optional; 021 022import org.apache.avalon.framework.component.Component; 023import org.apache.avalon.framework.configuration.Configuration; 024import org.apache.avalon.framework.configuration.ConfigurationException; 025import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 026import org.apache.avalon.framework.logger.AbstractLogEnabled; 027import org.apache.avalon.framework.service.ServiceException; 028import org.apache.avalon.framework.service.ServiceManager; 029import org.apache.avalon.framework.service.Serviceable; 030import org.apache.commons.lang3.StringUtils; 031import org.apache.excalibur.source.Source; 032import org.apache.excalibur.source.SourceResolver; 033import org.xml.sax.SAXException; 034 035import org.ametys.runtime.model.ElementDefinition; 036import org.ametys.runtime.model.ModelItem; 037import org.ametys.runtime.model.ModelItemGroup; 038import org.ametys.runtime.model.ModelViewItem; 039import org.ametys.runtime.model.ModelViewItemGroup; 040import org.ametys.runtime.model.SimpleViewItemGroup; 041import org.ametys.runtime.model.TemporaryViewReference; 042import org.ametys.runtime.model.View; 043import org.ametys.runtime.model.ViewElement; 044import org.ametys.runtime.model.ViewElementAccessor; 045import org.ametys.runtime.model.ViewItemAccessor; 046import org.ametys.runtime.model.ViewItemGroup; 047import org.ametys.runtime.model.ViewParser; 048 049/** 050 * Component that parses the configuration of a content type's view 051 */ 052public class ContentTypeViewParser extends AbstractLogEnabled implements ViewParser, Component, Serviceable 053{ 054 /** Default tag name for references to attributes */ 055 public static final String DEFAULT_ATTRIBUTE_REF_TAG_NAME = "attribute-ref"; 056 057 /** The content type extension point */ 058 protected ContentTypeExtensionPoint _contentTypeExtensionPoint; 059 /** The content types helper */ 060 protected ContentTypesHelper _contentTypesHelper; 061 /** The content types parser helper */ 062 protected ContentTypesParserHelper _contentTypesParserHelper; 063 /** The source resolver */ 064 protected SourceResolver _srcResolver; 065 066 /** The content type containing view */ 067 protected ContentType _contentType; 068 069 /** The tag name for simple groups */ 070 protected String _groupTagName; 071 /** The tag name for attribute references */ 072 protected String _attributeRefTagName; 073 /** Determine if this parser supports attribute nested in an attribute of type content */ 074 protected boolean _supportsNestedAttributes; 075 076 /** 077 * Creates a view parser for content types 078 * @param contentType the content type containing the view 079 */ 080 public ContentTypeViewParser(ContentType contentType) 081 { 082 this(contentType, Optional.empty(), Optional.empty(), true); 083 } 084 085 /** 086 * Creates a view parser for content types 087 * @param contentType the content type containing the view 088 * @param groupTagName the tag name for simple groups 089 * @param attributeRefTagName the tag name for attribute references 090 * @param supportsNestedAttributes <code>false</code> to avoid this parser to support nested attributes 091 */ 092 public ContentTypeViewParser(ContentType contentType, Optional<String> groupTagName, Optional<String> attributeRefTagName, boolean supportsNestedAttributes) 093 { 094 _contentType = contentType; 095 _groupTagName = groupTagName.orElse(DEFAULT_GROUP_TAG_NAME); 096 _attributeRefTagName = attributeRefTagName.orElse(DEFAULT_ATTRIBUTE_REF_TAG_NAME); 097 _supportsNestedAttributes = supportsNestedAttributes; 098 } 099 100 public void service(ServiceManager manager) throws ServiceException 101 { 102 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 103 _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 104 _contentTypesParserHelper = (ContentTypesParserHelper) manager.lookup(ContentTypesParserHelper.ROLE); 105 _srcResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 106 } 107 108 public View parseView(Configuration viewConfiguration) throws ConfigurationException 109 { 110 View view = new View(); 111 112 String name = viewConfiguration.getAttribute("name"); 113 view.setName(name); 114 view.setInternal(viewConfiguration.getAttributeAsBoolean("internal", false)); 115 view.setLabel(_contentTypesParserHelper.parseI18nizableText(_contentType, viewConfiguration, "label", name)); 116 view.setDescription(_contentTypesParserHelper.parseI18nizableText(_contentType, viewConfiguration, "description")); 117 118 Configuration iconConf = viewConfiguration.getChild("icons"); 119 120 view.setSmallIcon(_contentTypesParserHelper.parseIcon(_contentType, iconConf, "small", null)); 121 String mediumIcon = _contentTypesParserHelper.parseIcon(_contentType, iconConf, "medium", null); 122 view.setMediumIcon(mediumIcon); 123 view.setLargeIcon(_contentTypesParserHelper.parseIcon(_contentType, iconConf, "large", null)); 124 125 view.setIconGlyph(_contentTypesParserHelper.parseIconGlyph(iconConf, name, mediumIcon == null ? "ametysicon-column3" : null)); 126 view.setIconDecorator(iconConf.getChild("decorator").getValue(null)); 127 128 _fillViewItems(viewConfiguration, view); 129 130 return view; 131 } 132 133 /** 134 * Fill the items of the given view 135 * @param viewConfiguration the configuration of the view to fill 136 * @param view the view to fill 137 * @throws ConfigurationException if the configuration is not valid. 138 */ 139 protected void _fillViewItems(Configuration viewConfiguration, View view) throws ConfigurationException 140 { 141 for (Configuration itemConfiguration : viewConfiguration.getChildren()) 142 { 143 String itemConfigurationName = itemConfiguration.getName(); 144 145 if (_attributeRefTagName.equals(itemConfigurationName)) 146 { 147 ModelViewItem viewItem = _parseModelViewItem(itemConfiguration, StringUtils.EMPTY, view); 148 149 if (getLogger().isWarnEnabled() && view.hasModelViewItem(viewItem)) 150 { 151 String itemName = viewItem.getDefinition().getName(); 152 getLogger().warn("The item '" + itemName + "' is already referenced by a super-view or by the view '" + view.getName() + "' itself."); 153 } 154 155 view.addViewItem(viewItem); 156 157 } 158 else if (_groupTagName.equals(itemConfigurationName)) 159 { 160 view.addViewItem(_parseSimpleViewItemGroup(itemConfiguration, StringUtils.EMPTY, ViewItemGroup.TAB_ROLE, view)); 161 } 162 else if ("dublin-core".equals(itemConfigurationName)) 163 { 164 view.addViewItem(_parseDublinCoreViewItems()); 165 } 166 else if ("include".equals(itemConfigurationName)) 167 { 168 view.includeView(_getViewToInclude(itemConfiguration, view.getName())); 169 } 170 } 171 } 172 173 /** 174 * Parses a model view item 175 * @param itemConfiguration configuration of the model view item 176 * @param parentPath path of the parent of the model view item 177 * @param referenceView view that references the item 178 * @return the model view item 179 * @throws ConfigurationException if the configuration is not valid. 180 */ 181 protected ModelViewItem _parseModelViewItem(Configuration itemConfiguration, String parentPath, View referenceView) throws ConfigurationException 182 { 183 String modelItemName = itemConfiguration.getAttribute("name"); 184 String path = StringUtils.isEmpty(parentPath) ? modelItemName : parentPath + ModelItem.ITEM_PATH_SEPARATOR + modelItemName; 185 186 if (!_contentType.hasModelItem(path)) 187 { 188 throw new ConfigurationException("The item '" + path + "' in the view '" + referenceView.getName() + "' is not defined in content type '" + _contentType.getId() + "'", itemConfiguration); 189 } 190 191 ModelItem modelItem = _contentType.getModelItem(path); 192 ModelViewItem viewItem; 193 if (modelItem instanceof ModelItemGroup) 194 { 195 if (_shouldInsertAllAccessorItems(itemConfiguration)) 196 { 197 viewItem = ModelViewItemGroup.of((ModelItemGroup) modelItem); 198 } 199 else 200 { 201 viewItem = new ModelViewItemGroup(); 202 _parseViewItemAccessorChildren(itemConfiguration, (ViewItemAccessor) viewItem, path, referenceView); 203 } 204 205 ((ModelViewItemGroup) viewItem).setDefinition((ModelItemGroup) modelItem); 206 } 207 else if (modelItem instanceof ContentAttributeDefinition) 208 { 209 viewItem = _parseContentAttributeViewItem(itemConfiguration, (ContentAttributeDefinition) modelItem, path, referenceView); 210 ((ViewElement) viewItem).setDefinition((ElementDefinition) modelItem); 211 } 212 else 213 { 214 viewItem = new ViewElement(); 215 ((ViewElement) viewItem).setDefinition((ElementDefinition) modelItem); 216 } 217 218 if (itemConfiguration.getChild("label", false) != null) 219 { 220 viewItem.setLabel(_contentTypesParserHelper.parseI18nizableText(_contentType, itemConfiguration, "label")); 221 } 222 223 if (itemConfiguration.getChild("description", false) != null) 224 { 225 viewItem.setDescription(_contentTypesParserHelper.parseI18nizableText(_contentType, itemConfiguration, "description")); 226 } 227 228 return viewItem; 229 } 230 231 /** 232 * Parses the view item referencing an attribute of type content 233 * @param itemConfiguration configuration of the view item 234 * @param definition definition of the attribute 235 * @param path path of the item in the view 236 * @param referenceView view that references the item 237 * @return the view item 238 * @throws ConfigurationException if the configuration is not valid 239 */ 240 protected ViewElement _parseContentAttributeViewItem(Configuration itemConfiguration, ContentAttributeDefinition definition, String path, View referenceView) throws ConfigurationException 241 { 242 if (_supportsNestedAttributes) 243 { 244 ViewElementAccessor viewItem = new ViewElementAccessor(); 245 246 if (_shouldInsertAllAccessorItems(itemConfiguration)) 247 { 248 String contentTypeId = definition.getContentTypeId(); 249 if (contentTypeId != null) 250 { 251 ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId); 252 // Create a view with all content type's items 253 View view = View.of(contentType); 254 // And add all items to the current accessor 255 viewItem.addViewItems(view.getViewItems()); 256 } 257 else 258 { 259 // No content type is defined on the attribute, add a view item for title 260 ViewElement titleViewElement = new ViewElement(); 261 titleViewElement.setDefinition(_contentTypesHelper.getTitleAttributeDefinition()); 262 viewItem.addViewItem(titleViewElement); 263 } 264 } 265 else 266 { 267 // Parse children 268 _parseViewItemAccessorChildren(itemConfiguration, viewItem, path, referenceView); 269 } 270 271 return viewItem; 272 } 273 else 274 { 275 return new ViewElement(); 276 } 277 } 278 279 /** 280 * Checks if all the items of the current accessor should be inserted in the view 281 * @param accessorConfiguration configuration of the current accessor 282 * @return <code>true</code> if all items of the current accessor should be inserted in the view, <code>false</code> otherwise 283 * @throws ConfigurationException if the configuration is not valid 284 */ 285 protected boolean _shouldInsertAllAccessorItems(Configuration accessorConfiguration) throws ConfigurationException 286 { 287 Configuration child = accessorConfiguration.getChild(_attributeRefTagName, false); 288 if (child != null) 289 { 290 String name = child.getAttribute("name"); 291 return ALL_ITEMS_REFERENCE.equals(name); 292 } 293 else 294 { 295 return false; 296 } 297 } 298 299 /** 300 * Parses the items of the given {@link ViewItemAccessor} 301 * @param itemConfiguration configuration of the model view item 302 * @param viewItemAccessor the {@link ViewItemAccessor} that will access to the parsed items 303 * @param modelItemPath path of the model view item. 304 * @param referenceView The view that references the item 305 * @throws ConfigurationException if the configuration is not valid 306 */ 307 protected void _parseViewItemAccessorChildren(Configuration itemConfiguration, ViewItemAccessor viewItemAccessor, String modelItemPath, View referenceView) throws ConfigurationException 308 { 309 for (Configuration childConfiguration : itemConfiguration.getChildren()) 310 { 311 String childConfigurationName = childConfiguration.getName(); 312 313 if (_attributeRefTagName.equals(childConfigurationName)) 314 { 315 ModelViewItem viewItem = _parseModelViewItem(childConfiguration, modelItemPath, referenceView); 316 317 if (getLogger().isWarnEnabled() && (viewItemAccessor.hasModelViewItem(viewItem) || referenceView.hasModelViewItem(viewItem))) 318 { 319 String itemPath = viewItem.getDefinition().getPath(); 320 getLogger().warn("The item '" + itemPath + "' is already referenced by the accessor '" + itemConfiguration.getAttribute("name") + "' or by the view '" + referenceView.getName() + "."); 321 } 322 323 viewItemAccessor.addViewItem(viewItem); 324 325 } 326 else if (_groupTagName.equals(childConfigurationName)) 327 { 328 viewItemAccessor.addViewItem(_parseSimpleViewItemGroup(childConfiguration, modelItemPath, ViewItemGroup.FIELDSET_ROLE, referenceView)); 329 } 330 else if ("view".equals(childConfigurationName) && viewItemAccessor instanceof ViewElement) 331 { 332 // Insert items of the given view 333 String viewName = childConfiguration.getAttribute("name"); 334 TemporaryViewReference viewReference = new TemporaryViewReference(); 335 viewReference.setName(viewName); 336 viewItemAccessor.addViewItem(viewReference); 337 } 338 else 339 { 340 getLogger().error("The group '" + itemConfiguration.getAttribute("name") + "' contains forbidden items @ " + childConfiguration.getLocation()); 341 } 342 } 343 } 344 345 /** 346 * Parses a simple view item group 347 * @param itemConfiguration configuration of the simple view item group 348 * @param parentPath path of the model item parent of the group. 349 * @param role role of the view group 350 * @param referenceView the reference view for includes 351 * @return the simple view item group 352 * @throws ConfigurationException if the configuration is not valid. 353 */ 354 protected SimpleViewItemGroup _parseSimpleViewItemGroup(Configuration itemConfiguration, String parentPath, String role, View referenceView) throws ConfigurationException 355 { 356 SimpleViewItemGroup group = new SimpleViewItemGroup(); 357 group.setRole(itemConfiguration.getAttribute("role", role)); 358 group.setName(itemConfiguration.getAttribute("name", null)); 359 360 group.setLabel(_contentTypesParserHelper.parseI18nizableText(_contentType, itemConfiguration, "label")); 361 group.setDescription(_contentTypesParserHelper.parseI18nizableText(_contentType, itemConfiguration, "description")); 362 363 for (Configuration childConfiguration : itemConfiguration.getChildren()) 364 { 365 String childConfigurationName = childConfiguration.getName(); 366 367 if (_attributeRefTagName.equals(childConfigurationName)) 368 { 369 ModelViewItem viewItem = _parseModelViewItem(childConfiguration, parentPath, referenceView); 370 371 if (getLogger().isWarnEnabled() && (group.hasModelViewItem(viewItem) || referenceView.hasModelViewItem(viewItem))) 372 { 373 String itemPath = viewItem.getDefinition().getPath(); 374 String groupName = itemConfiguration.getAttribute("name", null); 375 String groupDesignation = groupName != null ? "group named '" + groupName + "'" : "unnamed group"; 376 getLogger().warn("The item '" + itemPath + "' is already referenced by the current '" + groupDesignation + "' or by the view '" + referenceView.getName() + "."); 377 } 378 379 group.addViewItem(viewItem); 380 } 381 else if (_groupTagName.equals(childConfigurationName)) 382 { 383 group.addViewItem(_parseSimpleViewItemGroup(childConfiguration, parentPath, ViewItemGroup.FIELDSET_ROLE, referenceView)); 384 } 385 else if ("dublin-core".equals(childConfigurationName)) 386 { 387 group.addViewItem(_parseDublinCoreViewItems()); 388 } 389 else if ("include".equals(childConfigurationName)) 390 { 391 group.includeView(_getViewToInclude(childConfiguration, referenceView.getName()), referenceView); 392 } 393 } 394 395 return group; 396 } 397 398 /** 399 * Retrieves the view included by the given configuration 400 * @param itemConfiguration configuration of the item that includes the view 401 * @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) 402 * @return the view included by the given configuration 403 * @throws ConfigurationException if the configuration is not valid 404 */ 405 protected View _getViewToInclude(Configuration itemConfiguration, String viewName) throws ConfigurationException 406 { 407 String superTypeId = itemConfiguration.getAttribute("from-supertype"); 408 if ("true".equals(superTypeId)) 409 { 410 View viewToInclude = _contentTypesHelper.getView(viewName, _contentType.getSupertypeIds(), new String[0]); 411 if (viewToInclude == null) 412 { 413 throw new ConfigurationException("The view '" + viewName + "' in content type '" + _contentType.getId() + "' includes a super-view not found in its super-types.", itemConfiguration); 414 } 415 return viewToInclude; 416 } 417 else 418 { 419 if (!_contentTypesHelper.getAncestors(_contentType.getId()).contains(superTypeId)) 420 { 421 throw new ConfigurationException("The view '" + viewName + "' in content type '" + _contentType.getId() + "' includes the super-view of the type '" + superTypeId + "' that is not a super-type.", itemConfiguration); 422 } 423 424 ContentType superType = _contentTypeExtensionPoint.getExtension(superTypeId); 425 if (superType == null) 426 { 427 throw new ConfigurationException("The view '" + viewName + "' in content type '" + _contentType.getId() + "' includes the super-view of an unknown super-type '" + superTypeId + "'.", itemConfiguration); 428 } 429 430 View viewToInclude = superType.getView(viewName); 431 if (viewToInclude == null) 432 { 433 throw new ConfigurationException("The view '" + viewName + "' in content type '" + _contentType.getId() + "' includes the super-view not found in its super-type '" + superTypeId + "'.", itemConfiguration); 434 } 435 return viewToInclude; 436 } 437 } 438 439 /** 440 * Parses the DublinCore view items. 441 * @return the group containing the dublin core view items 442 * @throws ConfigurationException if the configuration is not valid. 443 */ 444 protected ModelViewItemGroup _parseDublinCoreViewItems() throws ConfigurationException 445 { 446 ModelViewItemGroup dublinCoreGroup = new ModelViewItemGroup(); 447 448 ModelItemGroup dublinCoreRootDef = null; 449 if (_contentType.hasModelItem("/dc")) 450 { 451 ModelItem modelItem = _contentType.getModelItem("/dc"); 452 if (modelItem instanceof ModelItemGroup) 453 { 454 dublinCoreRootDef = (ModelItemGroup) modelItem; 455 } 456 } 457 458 if (dublinCoreRootDef != null) 459 { 460 dublinCoreGroup.setDefinition(dublinCoreRootDef); 461 } 462 else 463 { 464 throw new ConfigurationException("Unable to include Dublin Core view in a view of the content type '" + _contentType.getId() + "'. The Dublin Core attributes have not been defined."); 465 } 466 467 468 Source src = null; 469 470 try 471 { 472 src = _srcResolver.resolveURI("resource://org/ametys/cms/dublincore/dublincore.xml"); 473 474 if (src.exists()) 475 { 476 try (InputStream is = src.getInputStream()) 477 { 478 Configuration configuration = new DefaultConfigurationBuilder(true).build(is); 479 Configuration viewConfiguration = configuration.getChild("metadata-set"); 480 481 for (Configuration childConfiguration : viewConfiguration.getChildren("metadata-ref")) 482 { 483 ViewElement viewElement = new ViewElement(); 484 String childAttributeName = "/dc" + ModelItem.ITEM_PATH_SEPARATOR + childConfiguration.getAttribute("name"); 485 if (_contentType.hasModelItem(childAttributeName)) 486 { 487 ModelItem definition = _contentType.getModelItem(childAttributeName); 488 if (definition instanceof ElementDefinition) 489 { 490 viewElement.setDefinition((ElementDefinition) definition); 491 dublinCoreGroup.addViewItem(viewElement); 492 } 493 else 494 { 495 throw new ConfigurationException("Unable to include Dublin Core view in a view of the content type '" + _contentType.getId() + "'. The Dublin Core attribute '" + childAttributeName + "' has not been defined."); 496 } 497 } 498 else 499 { 500 throw new ConfigurationException("Unable to include Dublin Core view in a view of the content type '" + _contentType.getId() + "'. The Dublin Core attribute '" + childAttributeName + "' has not been defined."); 501 } 502 } 503 } 504 } 505 } 506 catch (IOException | SAXException e) 507 { 508 throw new ConfigurationException("Unable to parse Dublin Core view", e); 509 } 510 finally 511 { 512 if (src != null) 513 { 514 _srcResolver.release(src); 515 } 516 } 517 518 return dublinCoreGroup; 519 } 520}