001/* 002 * Copyright 2018 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.data.holder.group.impl; 017 018import java.io.IOException; 019import java.util.ArrayList; 020import java.util.Collections; 021import java.util.Comparator; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.Optional; 026import java.util.SortedSet; 027import java.util.TreeSet; 028 029import org.apache.cocoon.xml.AttributesImpl; 030import org.apache.cocoon.xml.XMLUtils; 031import org.apache.commons.lang3.StringUtils; 032import org.apache.solr.common.SolrInputDocument; 033import org.slf4j.Logger; 034import org.slf4j.LoggerFactory; 035import org.xml.sax.Attributes; 036import org.xml.sax.ContentHandler; 037import org.xml.sax.SAXException; 038 039import org.ametys.cms.content.indexing.solr.SolrFieldNames; 040import org.ametys.cms.data.holder.IndexableDataHolder; 041import org.ametys.cms.data.holder.group.IndexableRepeater; 042import org.ametys.cms.data.holder.group.IndexableRepeaterEntry; 043import org.ametys.cms.data.holder.impl.IndexableDataHolderHelper; 044import org.ametys.cms.data.type.indexing.IndexableDataContext; 045import org.ametys.plugins.repository.data.holder.DataHolder; 046import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 047import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry; 048import org.ametys.plugins.repository.data.holder.group.ModifiableRepeater; 049import org.ametys.plugins.repository.data.holder.group.ModifiableRepeaterEntry; 050import org.ametys.plugins.repository.data.holder.group.Repeater; 051import org.ametys.plugins.repository.data.holder.group.RepeaterEntry; 052import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater; 053import org.ametys.plugins.repository.data.holder.values.SynchronizationContext; 054import org.ametys.plugins.repository.data.repositorydata.RepositoryData; 055import org.ametys.plugins.repository.model.RepeaterDefinition; 056import org.ametys.runtime.model.ModelViewItem; 057import org.ametys.runtime.model.ModelViewItemGroup; 058import org.ametys.runtime.model.ViewHelper; 059import org.ametys.runtime.model.ViewItemAccessor; 060import org.ametys.runtime.model.ViewItemContainer; 061import org.ametys.runtime.model.exception.BadItemTypeException; 062import org.ametys.runtime.model.exception.NotUniqueTypeException; 063import org.ametys.runtime.model.exception.UndefinedItemPathException; 064import org.ametys.runtime.model.exception.UnknownTypeException; 065import org.ametys.runtime.model.type.DataContext; 066 067/** 068 * Class for model aware repeaters 069 */ 070public class DefaultModelAwareRepeater implements IndexableRepeater 071{ 072 private static final Logger __LOGGER = LoggerFactory.getLogger(ModelAwareDataHolder.class); 073 074 /** Definition of this repeater */ 075 protected RepeaterDefinition _definition; 076 077 /** Parent of the current {@link Repeater} */ 078 protected IndexableDataHolder _parent; 079 080 /** Root {@link DataHolder} */ 081 protected IndexableDataHolder _root; 082 083 /** Repository data to use to store entries in the repository */ 084 protected RepositoryData _repositoryData; 085 086 /** 087 * Creates a model aware repeater 088 * @param repositoryData the repository data of the repeater 089 * @param parent the parent of the created {@link Repeater} 090 * @param root the root {@link DataHolder} 091 * @param definition the definition of the repeater 092 */ 093 public DefaultModelAwareRepeater(RepositoryData repositoryData, IndexableDataHolder parent, IndexableDataHolder root, RepeaterDefinition definition) 094 { 095 _repositoryData = repositoryData; 096 _definition = definition; 097 _parent = parent; 098 _root = root; 099 } 100 101 @Override 102 public List<? extends IndexableRepeaterEntry> getEntries() 103 { 104 SortedSet<IndexableRepeaterEntry> entries = new TreeSet<>(new Comparator<ModelAwareRepeaterEntry>() 105 { 106 public int compare(ModelAwareRepeaterEntry entry1, ModelAwareRepeaterEntry entry2) 107 { 108 return Integer.compare(entry1.getPosition(), entry2.getPosition()); 109 } 110 }); 111 112 for (String entryName : _repositoryData.getDataNames()) 113 { 114 IndexableRepeaterEntry entry = getEntry(Integer.parseInt(entryName)); 115 entries.add(entry); 116 } 117 118 return Collections.unmodifiableList(new ArrayList<>(entries)); 119 } 120 121 public IndexableRepeaterEntry getEntry(int position) 122 { 123 if (1 <= position && position <= getSize()) 124 { 125 RepositoryData entryRepositoryData = _repositoryData.getRepositoryData(String.valueOf(position)); 126 return new DefaultModelAwareRepeaterEntry(entryRepositoryData, this, _definition); 127 } 128 else if (-getSize() < position && position <= 0) 129 { 130 // Find the positive equivalent position and call the getEntry method with this position 131 return getEntry(getSize() + position); 132 } 133 else 134 { 135 return null; 136 } 137 } 138 139 public int getSize() 140 { 141 return _repositoryData.getDataNames().size(); 142 } 143 144 public boolean hasEntry(int position) 145 { 146 if (1 <= position) 147 { 148 return _repositoryData.hasValue(String.valueOf(position)); 149 } 150 else 151 { 152 return _repositoryData.hasValue(String.valueOf(getSize() + position)); 153 } 154 } 155 156 /** 157 * Retrieves the repeater's model 158 * @return the repeater's model 159 */ 160 public RepeaterDefinition getModel() 161 { 162 return _definition; 163 } 164 165 public void dataToSAX(ContentHandler contentHandler, String dataPath, DataContext context) throws SAXException 166 { 167 for (ModelAwareRepeaterEntry entry : getEntries()) 168 { 169 XMLUtils.startElement(contentHandler, "entry", _getEntryAttributes(entry)); 170 entry.dataToSAX(contentHandler, dataPath, context); 171 XMLUtils.endElement(contentHandler, "entry"); 172 } 173 } 174 175 public void dataToSAX(ContentHandler contentHandler, DataContext context) throws SAXException, BadItemTypeException 176 { 177 ModelViewItemGroup viewItemGroup = ModelViewItemGroup.of(_definition); 178 dataToSAX(contentHandler, viewItemGroup, context); 179 } 180 181 public void dataToSAX(ContentHandler contentHandler, ViewItemAccessor viewItemAccessor, DataContext context) throws SAXException, BadItemTypeException 182 { 183 for (ModelAwareRepeaterEntry entry : getEntries()) 184 { 185 XMLUtils.startElement(contentHandler, "entry", _getEntryAttributes(entry)); 186 entry.dataToSAX(contentHandler, viewItemAccessor, context); 187 XMLUtils.endElement(contentHandler, "entry"); 188 } 189 } 190 191 private Attributes _getEntryAttributes(ModelAwareRepeaterEntry entry) 192 { 193 AttributesImpl entryAttrs = new AttributesImpl(); 194 String entryName = Integer.toString(entry.getPosition()); 195 entryAttrs.addCDATAAttribute("name", entryName); 196 return entryAttrs; 197 } 198 199 public Map<String, Object> dataToJSON(String dataPath, DataContext context) throws IOException 200 { 201 return _dataToJSON(Optional.of(dataPath), Optional.empty(), context); 202 } 203 204 public Map<String, Object> dataToJSON(DataContext context) throws BadItemTypeException 205 { 206 ModelViewItemGroup viewItemGroup = ModelViewItemGroup.of(_definition); 207 return dataToJSON(viewItemGroup, context); 208 } 209 210 public Map<String, Object> dataToJSON(ViewItemAccessor viewItemAccessor, DataContext context) throws BadItemTypeException 211 { 212 return _dataToJSON(Optional.empty(), Optional.of(viewItemAccessor), context); 213 } 214 215 @SuppressWarnings("unchecked") 216 private Map<String, Object> _dataToJSON(Optional<String> dataPath, Optional<ViewItemAccessor> viewItemAccessor, DataContext context) throws BadItemTypeException 217 { 218 List<Map<String, Object>> entriesValues = new ArrayList<>(); 219 for (ModelAwareRepeaterEntry entry : getEntries()) 220 { 221 DataContext entryContext = context.cloneContext(); 222 if (StringUtils.isNotEmpty(context.getDataPath())) 223 { 224 entryContext.addSuffixToLastSegment("[" + entry.getPosition() + "]"); 225 } 226 227 Map<String, Object> entryValues = null; 228 if (dataPath.isPresent()) 229 { 230 entryValues = (Map<String, Object>) entry.dataToJSON(dataPath.get(), entryContext); 231 } 232 else if (viewItemAccessor.isPresent()) 233 { 234 entryValues = entry.dataToJSON(viewItemAccessor.get(), entryContext); 235 } 236 237 entriesValues.add(entryValues); 238 } 239 240 Map<String, Object> result = new HashMap<>(); 241 result.put("entryCount", getSize()); 242 result.put("entries", entriesValues); 243 return result; 244 } 245 246 /** 247 * Generates SAX events for the comments of the data in the given view in the current {@link DataHolder} 248 * @param contentHandler the {@link ContentHandler} that will receive the SAX events 249 * @param viewItemAccessor the {@link ViewItemAccessor} referencing the items for which generate SAX events 250 * @throws SAXException if an error occurs during the SAX events generation 251 */ 252 public void commentsToSAX(ContentHandler contentHandler, ViewItemAccessor viewItemAccessor) throws SAXException 253 { 254 for (ModelAwareRepeaterEntry entry : getEntries()) 255 { 256 entry.commentsToSAX(contentHandler, viewItemAccessor); 257 } 258 } 259 260 public void copyTo(ModifiableRepeater repeater) throws UndefinedItemPathException, BadItemTypeException, UnknownTypeException, NotUniqueTypeException 261 { 262 for (RepeaterEntry entry : getEntries()) 263 { 264 ModifiableRepeaterEntry entryDestination = repeater.addEntry(entry.getPosition()); 265 entry.copyTo(entryDestination); 266 } 267 } 268 269 public List<SolrInputDocument> indexData(SolrInputDocument document, SolrInputDocument rootDocument, String solrFieldPrefix, IndexableDataContext context) throws BadItemTypeException 270 { 271 List<SolrInputDocument> additionalDocuments = new ArrayList<>(); 272 String solrFieldName = solrFieldPrefix + context.getDataPathLastSegment(); 273 274 for (IndexableRepeaterEntry entry : getEntries()) 275 { 276 // Update the context with entry position 277 IndexableDataContext newContext = context.cloneContext() 278 .addSuffixToLastSegment("[" + entry.getPosition() + "]"); 279 280 SolrInputDocument repeaterEntryDoc = new SolrInputDocument(); 281 282 if (!context.indexForFullTextField()) 283 { 284 // Creates a new Solr document for each entry 285 String repeaterEntryDocId = document.getField("id").getFirstValue().toString() + "/" + solrFieldName + "/" + entry.getPosition(); 286 repeaterEntryDoc.addField("id", repeaterEntryDocId); 287 repeaterEntryDoc.addField(SolrFieldNames.DOCUMENT_TYPE, SolrFieldNames.TYPE_REPEATER); 288 repeaterEntryDoc.addField(SolrFieldNames.REPEATER_ENTRY_POSITION, entry.getPosition()); 289 290 document.addField(solrFieldName + "_s_dv", repeaterEntryDocId); 291 } 292 293 // Add the created document to additional documents 294 additionalDocuments.add(repeaterEntryDoc); 295 296 ViewItemAccessor viewItemAccessor = context.getViewItem() 297 .map(ViewItemAccessor.class::cast) 298 .orElse(ViewHelper.createViewItemAccessor(entry.getModel())); 299 additionalDocuments.addAll(IndexableDataHolderHelper.indexData(entry, viewItemAccessor, repeaterEntryDoc, rootDocument, StringUtils.EMPTY, newContext)); 300 } 301 302 return additionalDocuments; 303 } 304 305 public boolean hasDifferences(ViewItemContainer viewItemContainer, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 306 { 307 return SynchronizableRepeater.Mode.APPEND.equals(repeaterValues.getMode()) 308 ? _hasDifferencesInAppendMode(viewItemContainer, repeaterValues, context) 309 : SynchronizableRepeater.Mode.REPLACE.equals(repeaterValues.getMode()) 310 ? _hasDifferencesInReplaceMode(viewItemContainer, repeaterValues, context) 311 : _hasDifferencesInReplaceAllMode(viewItemContainer, repeaterValues, context); 312 } 313 314 /** 315 * Check if there are differences between the given values and the repeater's entries if {@link SynchronizableRepeater#getMode()} is APPEND 316 * @param viewItemContainer The {@link ViewItemContainer} containing all items to check 317 * @param repeaterValues the values of the repeater to check 318 * @param context the context of the synchronization 319 * @return <code>true</code> if there are differences, <code>false</code> otherwise 320 * @throws UndefinedItemPathException if an entry's key refers to a data that is not defined by the model 321 * @throws BadItemTypeException if the type defined by the model of one of the entry's key doesn't match the corresponding value 322 */ 323 protected boolean _hasDifferencesInAppendMode(ViewItemContainer viewItemContainer, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 324 { 325 boolean hasEntriesToAppend = !repeaterValues.getRemovedEntries().isEmpty() || !repeaterValues.getEntries().isEmpty(); 326 327 if (__LOGGER.isDebugEnabled()) 328 { 329 String viewItemPath = viewItemContainer instanceof ModelViewItem modelViewItem 330 ? ViewHelper.getModelViewItemPath(modelViewItem) 331 : StringUtils.EMPTY; 332 if (hasEntriesToAppend) 333 { 334 __LOGGER.debug("#hasDifferences[{}] differences detected: some entries will be appended", viewItemPath); 335 } 336 else 337 { 338 __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath); 339 } 340 } 341 342 return hasEntriesToAppend; 343 } 344 345 /** 346 * Check if there are differences between the given values and the repeater's entries if {@link SynchronizableRepeater#getMode()} is REPLACE 347 * @param viewItemContainer The {@link ViewItemContainer} containing all items to check 348 * @param repeaterValues the values of the repeater to check 349 * @param context the context of the synchronization 350 * @return <code>true</code> if there are differences, <code>false</code> otherwise 351 * @throws UndefinedItemPathException if an entry's key refers to a data that is not defined by the model 352 * @throws BadItemTypeException if the type defined by the model of one of the entry's key doesn't match the corresponding value 353 */ 354 protected boolean _hasDifferencesInReplaceMode(ViewItemContainer viewItemContainer, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 355 { 356 List<Integer> positions = repeaterValues.getReplacePositions(); 357 List<Map<String, Object>> entriesValues = repeaterValues.getEntries(); 358 359 for (int i = 0; i < positions.size(); i++) 360 { 361 int position = positions.get(i); 362 ModelAwareRepeaterEntry repeaterEntry = getEntry(position); 363 if (repeaterEntry.hasDifferences(viewItemContainer, entriesValues.get(i), context)) 364 { 365 return true; 366 } 367 } 368 369 // No differences has been found in entries 370 if (__LOGGER.isDebugEnabled()) 371 { 372 String viewItemPath = viewItemContainer instanceof ModelViewItem modelViewItem 373 ? ViewHelper.getModelViewItemPath(modelViewItem) 374 : StringUtils.EMPTY; 375 __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath); 376 } 377 return false; 378 } 379 380 /** 381 * Check if there are differences between the given values and the repeater's entries if {@link SynchronizableRepeater#getMode()} is REPLACE_ALL 382 * @param viewItemContainer The {@link ViewItemContainer} containing all items to check 383 * @param repeaterValues the values of the repeater to check 384 * @param context the context of the synchronization 385 * @return <code>true</code> if there are differences, <code>false</code> otherwise 386 * @throws UndefinedItemPathException if an entry's key refers to a data that is not defined by the model 387 * @throws BadItemTypeException if the type defined by the model of one of the entry's key doesn't match the corresponding value 388 */ 389 protected boolean _hasDifferencesInReplaceAllMode(ViewItemContainer viewItemContainer, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 390 { 391 List<Map<String, Object>> entriesValues = repeaterValues.getEntries(); 392 if (hasToMoveEntries(repeaterValues.getPositionsMapping(), entriesValues.size())) 393 { 394 if (__LOGGER.isDebugEnabled()) 395 { 396 String viewItemPath = viewItemContainer instanceof ModelViewItem modelViewItem 397 ? ViewHelper.getModelViewItemPath(modelViewItem) 398 : StringUtils.EMPTY; 399 __LOGGER.debug("#hasDifferences[{}] differences detected: some entries will be moved", viewItemPath); 400 } 401 return true; 402 } 403 404 for (ModelAwareRepeaterEntry repeaterEntry : getEntries()) 405 { 406 int entryIndex = repeaterEntry.getPosition() - 1; 407 Map<String, Object> entryValues = entriesValues.get(entryIndex); 408 if (repeaterEntry.hasDifferences(viewItemContainer, entryValues, context)) 409 { 410 return true; 411 } 412 } 413 414 // No differences has been found in entries 415 if (__LOGGER.isDebugEnabled()) 416 { 417 String viewItemPath = viewItemContainer instanceof ModelViewItem modelViewItem 418 ? ViewHelper.getModelViewItemPath(modelViewItem) 419 : StringUtils.EMPTY; 420 __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath); 421 } 422 return false; 423 } 424 425 public boolean hasToMoveEntries(Map<Integer, Integer> positionsMapping, int targetSize) 426 { 427 int initialSize = getSize(); 428 429 if (targetSize != initialSize) 430 { 431 return true; 432 } 433 434 for (Map.Entry<Integer, Integer> mapping : positionsMapping.entrySet()) 435 { 436 if (!mapping.getKey().equals(mapping.getValue())) 437 { 438 return true; 439 } 440 } 441 442 return false; 443 } 444 445 public RepositoryData getRepositoryData() 446 { 447 return _repositoryData; 448 } 449 450 public IndexableDataHolder getParentDataHolder() 451 { 452 return _parent; 453 } 454 455 public IndexableDataHolder getRootDataHolder() 456 { 457 return _root; 458 } 459}