001/* 002 * Copyright 2017 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.plugins.contenttypeseditor; 017 018import java.util.ArrayList; 019import java.util.Collection; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.Map.Entry; 024 025import org.apache.avalon.framework.component.Component; 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; 030 031import org.ametys.cms.contenttype.AbstractMetadataSetElement; 032import org.ametys.cms.contenttype.ContentType; 033import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 034import org.ametys.cms.contenttype.Fieldset; 035import org.ametys.cms.contenttype.MetadataDefinition; 036import org.ametys.cms.contenttype.MetadataDefinitionReference; 037import org.ametys.cms.contenttype.MetadataSet; 038import org.ametys.cms.contenttype.RepeaterDefinition; 039import org.ametys.cms.contenttype.RichTextMetadataDefinition; 040import org.ametys.cms.contenttype.SemanticAnnotation; 041import org.ametys.cms.contenttype.indexing.CustomIndexingField; 042import org.ametys.cms.contenttype.indexing.IndexingField; 043import org.ametys.cms.contenttype.indexing.IndexingModel; 044import org.ametys.cms.contenttype.indexing.MetadataIndexingField; 045import org.ametys.core.right.Right; 046import org.ametys.core.right.RightsExtensionPoint; 047import org.ametys.core.util.I18nUtils; 048import org.ametys.runtime.i18n.I18nizableText; 049import org.ametys.runtime.parameter.DefaultValidator; 050import org.ametys.runtime.parameter.Enumerator; 051import org.ametys.runtime.parameter.StaticEnumerator; 052import org.ametys.runtime.parameter.Validator; 053import org.ametys.runtime.plugin.component.PluginAware; 054 055/** 056 * Helper to retrieve content type infos 057 */ 058public class ContentTypeInformationsHelper extends AbstractLogEnabled implements Component, Serviceable, PluginAware 059{ 060 /** The Avalon role name */ 061 public static final String ROLE = ContentTypeInformationsHelper.class.getName(); 062 063 /** The content type extension point instance */ 064 protected ContentTypeExtensionPoint _contentTypeExtensionPoint; 065 066 /** The rights extension point instance */ 067 protected RightsExtensionPoint _rightsExtensionPoint; 068 069 /** Utility methods helping the management of internationalizable text */ 070 protected I18nUtils _i18nUtils; 071 072 private Map<String, I18nizableText> _allMetadataLabels = new HashMap<>(); 073 074 private String _pluginName; 075 076 enum ContentTypeAttributeDataType 077 { 078 /** Metadata type */ 079 METADATA, 080 081 /** Metadata set type */ 082 METADATA_SET, 083 084 /** Fieldset type */ 085 FIELDSET, 086 087 /** Metadata reference type */ 088 METADATA_REF, 089 090 /** Indexing field type */ 091 INDEXING_FIELD 092 } 093 094 @Override 095 public void service(ServiceManager serviceManager) throws ServiceException 096 { 097 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE); 098 _rightsExtensionPoint = (RightsExtensionPoint) serviceManager.lookup(RightsExtensionPoint.ROLE); 099 _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE); 100 } 101 102 public void setPluginInfo(String pluginName, String featureName, String id) 103 { 104 _pluginName = pluginName; 105 } 106 107 /** 108 * retrieve content type informations 109 * @param contentTypeId - Content type's identifier 110 * @param hideInheritedMetadata - True to hide inherited metadata of a content type according to contentTypeId parameter 111 * @return a <code>Map</code> containing all informations about the content type 112 */ 113 public Map<String, Object> getContentTypeInfos(String contentTypeId, boolean hideInheritedMetadata) 114 { 115 ContentType cType = _contentTypeExtensionPoint.getExtension(contentTypeId); 116 if (cType == null) 117 { 118 throw new IllegalStateException("Unknown content type '" + contentTypeId + "'"); 119 } 120 121 Map<String, Object> contentTypeInfos = new HashMap<>(); 122 contentTypeInfos.put("id", cType.getId()); 123 contentTypeInfos.put("label", cType.getLabel()); 124 contentTypeInfos.put("description", cType.getDescription()); 125 contentTypeInfos.put("iconGlyph", cType.getIconGlyph()); 126 contentTypeInfos.put("iconDecorator", cType.getIconDecorator()); 127 contentTypeInfos.put("largeIcon", cType.getLargeIcon()); 128 contentTypeInfos.put("mediumIcon", cType.getMediumIcon()); 129 contentTypeInfos.put("smallIcon", cType.getSmallIcon()); 130 contentTypeInfos.put("category", cType.getCategory()); 131 contentTypeInfos.put("pluginName", cType.getPluginName()); 132 contentTypeInfos.put("defaultTitle", cType.getDefaultTitle()); 133 contentTypeInfos.put("right", _getRightLabel(cType.getRight())); 134 contentTypeInfos.put("private", cType.isPrivate()); 135 contentTypeInfos.put("abstract", cType.isAbstract()); 136 contentTypeInfos.put("multilingual", cType.isMultilingual()); 137 contentTypeInfos.put("mixin", cType.isMixin()); 138 contentTypeInfos.put("referencetable", cType.isReferenceTable()); 139 contentTypeInfos.put("simple", cType.isSimple()); 140 contentTypeInfos.put("superTypes", _getSuperTypesInfos(cType)); 141 contentTypeInfos.put("metadata", _getMetadata(cType, hideInheritedMetadata)); 142 contentTypeInfos.put("metadataSets", _getMetadataSets(cType, hideInheritedMetadata)); 143 contentTypeInfos.put("indexingModel", _getIndexingModel(cType)); 144 return contentTypeInfos; 145 } 146 147 private I18nizableText _getRightLabel(String rightId) 148 { 149 if (rightId != null) 150 { 151 Right right = _rightsExtensionPoint.getExtension(rightId); 152 if (right != null) 153 { 154 return right.getLabel(); 155 } 156 } 157 return null; 158 } 159 160 private List<Map<String, Object>> _getSuperTypesInfos(ContentType cType) 161 { 162 List<Map<String, Object>> superTypesInfos = new ArrayList<>(); 163 for (String superTypeId : cType.getSupertypeIds()) 164 { 165 Map<String, Object> superTypeInfos = new HashMap<>(); 166 ContentType superType = _contentTypeExtensionPoint.getExtension(superTypeId); 167 superTypeInfos.put("id", superTypeId); 168 superTypeInfos.put("label", superType.getLabel()); 169 superTypeInfos.put("isMixin", superType.isMixin()); 170 superTypesInfos.add(superTypeInfos); 171 172 } 173 return superTypesInfos; 174 } 175 176 private List<Map<String, Object>> _getMetadata(ContentType cType, boolean hideInheritedMetadata) 177 { 178 List<Map<String, Object>> metadata = new ArrayList<>(); 179 for (String name : cType.getMetadataNames()) 180 { 181 MetadataDefinition definition = cType.getMetadataDefinition(name); 182 String referenceContentTypeId = definition.getReferenceContentType(); 183 if (hideInheritedMetadata) 184 { 185 if (referenceContentTypeId.equals(cType.getId())) 186 { 187 metadata.add(_getMetadataValues(cType, definition, null)); 188 } 189 } 190 else 191 { 192 metadata.add(_getMetadataValues(cType, definition, null)); 193 } 194 } 195 return metadata; 196 } 197 198 private Map<String, Object> _getMetadataValues(ContentType cType, MetadataDefinition definition, String parentPath) 199 { 200 this._allMetadataLabels.put(definition.getName(), definition.getLabel()); 201 // TODO manage transformer and richTextOutgoingReferenceExtractor 202 Map<String, Object> values = new HashMap<>(); 203 values.put("dataType", ContentTypeAttributeDataType.METADATA.name().toLowerCase()); 204 values.put("id", definition.getId()); 205 values.put("pluginName", definition.getPluginName()); 206 values.put("label", definition.getLabel()); 207 values.put("description", definition.getDescription()); 208 values.put("type", definition.getType().name().toLowerCase()); 209 values.put("widget", definition.getWidget()); 210 values.put("widgetParams", definition.getWidgetParameters()); 211 values.put("defaultValue", definition.getDefaultValue()); 212 values.put("name", definition.getName()); 213 values.put("path", parentPath == null ? definition.getName() : parentPath + "/" + definition.getName()); 214 values.put("multiple", definition.isMultiple()); 215 values.put("mandatory", _isMandatoryMetadata(definition)); 216 values.put("enumerated", definition.getEnumerator() != null); 217 //values.put("iconGlyph", _getMetadataIconGlyph(definition, definition.getEnumerator() != null)); 218 //values.put("iconCls", _getMetadataIconGlyph(definition, definition.getEnumerator() != null)); 219 String linkedContentType = definition.getContentType(); 220 if (linkedContentType != null) 221 { 222 Map<String, Object> linkedCTypeInfos = new HashMap<>(); 223 linkedCTypeInfos.put("id", definition.getContentType()); 224 ContentType linkedCType = _contentTypeExtensionPoint.getExtension(definition.getContentType()); 225 linkedCTypeInfos.put("label", linkedCType.getLabel()); 226 linkedCTypeInfos.put("iconGlyph", linkedCType.getIconGlyph()); 227 228 values.put("linkedContentType", linkedCTypeInfos); 229 } 230 values.put("invertRelationPath", definition.getInvertRelationPath()); 231 values.put("forceInvert", definition.getForceInvert()); 232 values.put("contentTypeId", cType.getId()); 233 ContentType referenceContentType = _contentTypeExtensionPoint.getExtension(definition.getReferenceContentType()); 234 values.put("referenceContentTypeId", referenceContentType.getId()); 235 values.put("referenceContentTypeLabel", referenceContentType.getLabel()); 236 if (definition instanceof RepeaterDefinition) 237 { 238 values.putAll(_getRepeaterValues((RepeaterDefinition) definition)); 239 } 240 else if (definition instanceof RichTextMetadataDefinition) 241 { 242 values.putAll(_getRichTextValues((RichTextMetadataDefinition) definition)); 243 } 244 else if (definition.getEnumerator() != null) 245 { 246 Enumerator enumerator = definition.getEnumerator(); 247 if (enumerator instanceof StaticEnumerator) 248 { 249 values.put("enumerator", _getEnumerator(definition)); 250 } 251 else 252 { 253 values.put("enumeratorName", enumerator.getClass().getName()); 254 } 255 } 256 Validator validator = definition.getValidator(); 257 if (validator != null) 258 { 259 if (validator.getClass().equals(DefaultValidator.class)) 260 { 261 values.put("validator", _getValidatorParameters(definition)); 262 } 263 else 264 { 265 values.put("validatorName", validator.getClass().getName()); 266 } 267 } 268 269 List<Map<String, Object>> children = new ArrayList<>(); 270 for (String childName : definition.getMetadataNames()) 271 { 272 MetadataDefinition childDefinition = definition.getMetadataDefinition(childName); 273 children.add(_getMetadataValues(cType, childDefinition, parentPath == null ? definition.getName() : parentPath + "/" + definition.getName())); 274 } 275 values.put("leaf", children.isEmpty()); 276 values.put("children", children); 277 278 return values; 279 } 280 281 private Map<String, Object> _getRepeaterValues(RepeaterDefinition definition) 282 { 283 Map<String, Object> values = new HashMap<>(); 284 values.put("initializeSize", definition.getInitialSize()); 285 values.put("minSize", definition.getMinSize()); 286 values.put("maxSize", definition.getMaxSize()); 287 values.put("addLabel", definition.getAddLabel()); 288 values.put("deleteLabel", definition.getDeleteLabel()); 289 values.put("headerLabel", definition.getHeaderLabel()); 290 values.put("type", "repeater"); 291 return values; 292 } 293 294 private Map<String, Object> _getRichTextValues(RichTextMetadataDefinition definition) 295 { 296 Map<String, Object> values = new HashMap<>(); 297 List<SemanticAnnotation> annotations = definition.getSemanticAnnotations(); 298 List<Object> annotationsValues = new ArrayList<>(); 299 for (SemanticAnnotation annotation : annotations) 300 { 301 Map<String, Object> annotationValues = new HashMap<>(); 302 annotationValues.put("id", annotation.getId()); 303 annotationValues.put("label", annotation.getLabel()); 304 annotationValues.put("description", annotation.getDescription()); 305 annotationsValues.add(annotationValues); 306 } 307 values.put("semanticAnnotations", annotationsValues); 308 return values; 309 } 310 311 private List<Map<String, Object>> _getMetadataSets(ContentType cType, boolean hideInheritedMetadata) 312 { 313 List<Map<String, Object>> metadataSets = _getEditionMetadataSets(cType, hideInheritedMetadata); 314 metadataSets.addAll(_getViewMetadataSets(cType, hideInheritedMetadata)); 315 return metadataSets; 316 } 317 318 private List<Map<String, Object>> _getEditionMetadataSets(ContentType cType, boolean hideInheritedMetadata) 319 { 320 List<Map<String, Object>> metadataSets = new ArrayList<>(); 321 for (String name : cType.getEditionMetadataSetNames(true)) 322 { 323 MetadataSet metadataSet = cType.getMetadataSetForEdition(name); 324 metadataSets.add(_getMetadataSetValues(cType, metadataSet, null, hideInheritedMetadata)); 325 } 326 return metadataSets; 327 } 328 329 private List<Map<String, Object>> _getViewMetadataSets(ContentType cType, boolean hideInheritedMetadata) 330 { 331 List<Map<String, Object>> metadataSets = new ArrayList<>(); 332 for (String name : cType.getViewMetadataSetNames(true)) 333 { 334 MetadataSet metadataSet = cType.getMetadataSetForView(name); 335 metadataSets.add(_getMetadataSetValues(cType, metadataSet, null, hideInheritedMetadata)); 336 } 337 return metadataSets; 338 } 339 340 private Map<String, Object> _getMetadataSetValues(ContentType cType, MetadataSet metadataSet, String parentPath, boolean hideInheritedMetadata) 341 { 342 Map<String, Object> values = new HashMap<>(); 343 values.put("dataType", ContentTypeAttributeDataType.METADATA_SET.name().toLowerCase()); 344 values.put("name", metadataSet.getName()); 345 values.put("label", metadataSet.getLabel()); 346 values.put("description", metadataSet.getDescription()); 347 values.put("isEdition", metadataSet.isEdition()); 348 values.put("iconGlyph", metadataSet.getIconGlyph()); 349 values.put("iconDecorator", metadataSet.getIconDecorator()); 350 values.put("smallIcon", metadataSet.getSmallIcon()); 351 values.put("mediumIcon", metadataSet.getMediumIcon()); 352 values.put("largeIcon", metadataSet.getLargeIcon()); 353 if (!metadataSet.getIconGlyph().isEmpty()) 354 { 355 values.put("iconCls", metadataSet.getIconGlyph()); 356 } 357 else 358 { 359 values.put("icon", metadataSet.getSmallIcon()); 360 } 361 _processMetadataSetElementChildren(cType, metadataSet, values, parentPath, hideInheritedMetadata); 362 return values; 363 } 364 365 private Map<String, Object> _getFieldsetValues(ContentType cType, Fieldset fieldset, String parentPath, boolean hideInheritedMetadata) 366 { 367 Map<String, Object> values = new HashMap<>(); 368 values.put("dataType", ContentTypeAttributeDataType.FIELDSET.name().toLowerCase()); 369 values.put("label", fieldset.getLabel()); 370 values.put("role", fieldset.getRole()); 371 _processMetadataSetElementChildren(cType, fieldset, values, parentPath, hideInheritedMetadata); 372 return values; 373 } 374 375 private Map<String, Object> _getMetadataDefinitionReferenceValues(ContentType cType, MetadataDefinitionReference metadataRef, String parentPath, boolean hideInheritedMetadata) 376 { 377 Map<String, Object> values = new HashMap<>(); 378 values.put("dataType", ContentTypeAttributeDataType.METADATA_REF.name().toLowerCase()); 379 String metadataName = metadataRef.getMetadataName(); 380 String metadataPath = parentPath != null ? parentPath + "/" + metadataName : metadataName; 381 I18nizableText metadataLabel = this._allMetadataLabels.get(metadataName); 382 if (metadataLabel != null) 383 { 384 values.put("label", metadataLabel); 385 } 386 values.put("name", metadataName); 387 values.put("path", metadataPath); 388 _processMetadataSetElementChildren(cType, metadataRef, values, parentPath != null ? parentPath + "/" + metadataName : metadataName, hideInheritedMetadata); 389 return values; 390 } 391 392 private void _processMetadataSetElementChildren(ContentType cType, AbstractMetadataSetElement metadataSetElement, Map<String, Object> values, String parentPath, boolean hideInheritedMetadata) 393 { 394 List<Map<String, Object>> children = new ArrayList<>(); 395 for (AbstractMetadataSetElement element : metadataSetElement.getElements()) 396 { 397 if (element instanceof MetadataSet) 398 { 399 children.add(_getMetadataSetValues(cType, (MetadataSet) element, parentPath, hideInheritedMetadata)); 400 } 401 else if (element instanceof Fieldset) 402 { 403 children.add(_getFieldsetValues(cType, (Fieldset) element, parentPath, hideInheritedMetadata)); 404 } 405 else 406 { 407 MetadataDefinitionReference metadataSet = (MetadataDefinitionReference) element; 408 MetadataDefinition metadata = cType.getMetadataDefinitionByPath(metadataSet.getMetadataName()); 409 if ((metadata != null && hideInheritedMetadata && cType.getId().equals(metadata.getReferenceContentType())) || !hideInheritedMetadata || metadata == null) 410 { 411 children.add(_getMetadataDefinitionReferenceValues(cType, (MetadataDefinitionReference) element, parentPath, hideInheritedMetadata)); 412 } 413 } 414 } 415 values.put("leaf", children.isEmpty()); 416 values.put("children", children); 417 } 418 419 private List<Map<String, Object>> _getIndexingModel(ContentType cType) 420 { 421 List<Map<String, Object>> result = new ArrayList<>(); 422 IndexingModel indexingModel = cType.getIndexingModel(); 423 Collection<IndexingField> fields = indexingModel.getFields(); 424 for (IndexingField indexingField : fields) 425 { 426 result.add(_getIndexingFieldDetails(indexingField)); 427 } 428 return result; 429 } 430 431 private List<String> _getIndexingFieldPath(IndexingField indexingField) 432 { 433 List<String> indexingFieldPath = new ArrayList<>(); 434 if (indexingField instanceof CustomIndexingField) 435 { 436 indexingFieldPath = null; 437 } 438 else if (indexingField instanceof MetadataIndexingField) 439 { 440 MetadataIndexingField metadataIndexingField = (MetadataIndexingField) indexingField; 441 String metadataPath = metadataIndexingField.getMetadataPath(); 442 indexingFieldPath.add(metadataPath); 443 } 444 return indexingFieldPath; 445 } 446 447 private I18nizableText _getIndexingFieldType(IndexingField indexingField) 448 { 449 I18nizableText type = new I18nizableText("plugin." + _pluginName, "PLUGINS_CONTENTTYPESEDITOR_EDITOR_TOOL_INDEXING_MODEL_METADATA_TYPE"); 450 if (indexingField instanceof CustomIndexingField) 451 { 452 type = new I18nizableText("plugin." + _pluginName, "PLUGINS_CONTENTTYPESEDITOR_EDITOR_TOOL_INDEXING_MODEL_CUSTOM_METADATA_TYPE"); 453 } 454 return type; 455 } 456 457 private Map<String, Object> _getIndexingFieldDetails(IndexingField indexingField) 458 { 459 Map<String, Object> values = new HashMap<>(); 460 values.put("label", indexingField.getLabel()); 461 values.put("name", indexingField.getName()); 462 values.put("description", indexingField.getDescription()); 463 values.put("dataType", ContentTypeAttributeDataType.INDEXING_FIELD.name().toLowerCase()); 464 465 List<String> paths = _getIndexingFieldPath(indexingField); 466 values.put("path", paths); 467 if (indexingField instanceof CustomIndexingField) 468 { 469 values.put("class", indexingField.getClass()); 470 } 471 I18nizableText type = _getIndexingFieldType(indexingField); 472 values.put("type", type); 473 values.put("leaf", true); 474 return values; 475 } 476 477 private List<String> _getEnumerator(MetadataDefinition definition) 478 { 479 List<String> enumerators = new ArrayList<>(); 480 Enumerator enumerator = definition.getEnumerator(); 481 try 482 { 483 Map<Object, I18nizableText> entries = enumerator.getEntries(); 484 for (Entry<Object, I18nizableText> entry : entries.entrySet()) 485 { 486 I18nizableText enumeratorLabel = entry.getValue(); 487 String translatedEnumeratorLabel = _i18nUtils.translate(enumeratorLabel); 488 String label = translatedEnumeratorLabel == null ? enumeratorLabel.toString() : translatedEnumeratorLabel; 489 enumerators.add(label); 490 } 491 } 492 catch (Exception e) 493 { 494 getLogger().error("Unable to set values for enumerator " + enumerator.getClass().getName(), e); 495 } 496 return enumerators; 497 } 498 499 private Map<Object, Object> _getValidatorParameters(MetadataDefinition definition) 500 { 501 Map<Object, Object> validatorParameters = new HashMap<>(); 502 Validator validator = definition.getValidator(); 503 Map<String, Object> configuration = validator.getConfiguration(); 504 Object mandatory = configuration.get("mandatory"); 505 if (mandatory.toString().equals("true")) 506 { 507 Map<Object, Object> mandatoryInfo = new HashMap<>(); 508 mandatoryInfo.put("key", new I18nizableText("plugin." + _pluginName, "PLUGINS_CONTENTTYPESEDITOR_EDITOR_TOOL_METADATA_VALIDATOR_MANDATORY_LABEL")); 509 mandatoryInfo.put("value", new I18nizableText("")); 510 validatorParameters.put("mandatory", mandatoryInfo); 511 } 512 Object regexp = configuration.get("regexp"); 513 if (regexp != null) 514 { 515 Map<Object, Object> regexpInfo = new HashMap<>(); 516 regexpInfo.put("key", new I18nizableText("plugin." + _pluginName, "PLUGINS_CONTENTTYPESEDITOR_EDITOR_TOOL_METADATA_VALIDATOR_REGEXP_LABEL")); 517 regexpInfo.put("value", regexp.toString()); 518 validatorParameters.put("regexp", regexpInfo); 519 } 520 Object invalidText = configuration.get("invalidText"); 521 if (invalidText != null) 522 { 523 Map<Object, Object> invalidTextInfo = new HashMap<>(); 524 invalidTextInfo.put("key", new I18nizableText("plugin." + _pluginName, "PLUGINS_CONTENTTYPESEDITOR_EDITOR_TOOL_METADATA_VALIDATOR_INVALIDTEXT_LABEL")); 525 invalidTextInfo.put("value", invalidText); 526 validatorParameters.put("invalidText", invalidTextInfo); 527 } 528 return validatorParameters; 529 } 530 531 private boolean _isMandatoryMetadata(MetadataDefinition metadataDefinition) 532 { 533 boolean isMandatory = false; 534 Validator validator = metadataDefinition.getValidator(); 535 if (validator != null) 536 { 537 Map<String, Object> configuration = validator.getConfiguration(); 538 if (configuration != null) 539 { 540 Object object = configuration.get("mandatory"); 541 if (object instanceof Boolean) 542 { 543 isMandatory = (boolean) object; 544 } 545 } 546 } 547 return isMandatory; 548 } 549 550 void setContentTypeExtensionPoint(ContentTypeExtensionPoint contentTypeExtensionPoint) 551 { 552 this._contentTypeExtensionPoint = contentTypeExtensionPoint; 553 } 554 555 void setRightsExtensionPoint(RightsExtensionPoint rightsExtensionPoint) 556 { 557 this._rightsExtensionPoint = rightsExtensionPoint; 558 } 559 560}