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.odfsync.apogee.scc; 017 018import java.io.ByteArrayInputStream; 019import java.io.File; 020import java.io.FileInputStream; 021import java.io.IOException; 022import java.io.InputStream; 023import java.math.BigDecimal; 024import java.sql.Clob; 025import java.sql.SQLException; 026import java.util.ArrayList; 027import java.util.Arrays; 028import java.util.Date; 029import java.util.HashMap; 030import java.util.HashSet; 031import java.util.LinkedHashMap; 032import java.util.LinkedHashSet; 033import java.util.LinkedList; 034import java.util.List; 035import java.util.Map; 036import java.util.Set; 037 038import org.apache.avalon.framework.configuration.Configuration; 039import org.apache.avalon.framework.configuration.ConfigurationException; 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.cocoon.Constants; 047import org.apache.cocoon.components.ContextHelper; 048import org.apache.cocoon.environment.Request; 049import org.apache.commons.io.IOUtils; 050import org.apache.commons.lang.StringUtils; 051import org.slf4j.Logger; 052 053import org.ametys.cms.content.external.ExternalizableMetadataHelper; 054import org.ametys.cms.content.external.ExternalizableMetadataProvider.ExternalizableMetadataStatus; 055import org.ametys.cms.repository.ModifiableDefaultContent; 056import org.ametys.core.util.JSONUtils; 057import org.ametys.plugins.contentio.ContentImporterHelper; 058import org.ametys.plugins.contentio.synchronize.AbstractSimpleSynchronizableContentsCollection; 059import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollection; 060import org.ametys.plugins.odfsync.apogee.ApogeeDAO; 061import org.ametys.plugins.odfsync.apogee.scc.impl.OrgUnitSynchronizableContentsCollection; 062import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 063import org.ametys.plugins.repository.metadata.ModifiableRichText; 064import org.ametys.runtime.config.Config; 065import org.ametys.runtime.i18n.I18nizableText; 066 067import com.google.common.base.CharMatcher; 068import com.google.common.collect.ImmutableList; 069import com.google.common.collect.ImmutableMap; 070 071/** 072 * Abstract class for Apogee synchronization 073 */ 074public abstract class AbstractApogeeSynchronizableContentsCollection extends AbstractSimpleSynchronizableContentsCollection implements Contextualizable, ApogeeSynchronizableContentsCollection 075{ 076 /** 077 * Request attribute name to store handle contents during import or synchronization. 078 */ 079 public static final String HANDLE_CONTENTS = AbstractApogeeSynchronizableContentsCollection.class.getName() + "$handleContents"; 080 081 /** Name of parameter holding the field ID column */ 082 protected static final String __PARAM_ID_COLUMN = "idColumn"; 083 /** Name of parameter holding the fields mapping */ 084 protected static final String __PARAM_MAPPING = "mapping"; 085 /** Name of parameter into mapping holding the synchronized property */ 086 protected static final String __PARAM_MAPPING_SYNCHRO = "synchro"; 087 /** Name of parameter into mapping holding the path of metadata */ 088 protected static final String __PARAM_MAPPING_METADATA_REF = "metadata-ref"; 089 /** Name of parameter into mapping holding the remote attribute */ 090 protected static final String __PARAM_MAPPING_ATTRIBUTE = "attribute"; 091 /** Name of parameter holding the criteria */ 092 protected static final String __PARAM_CRITERIA = "criteria"; 093 /** Name of parameter into criteria holding a criterion */ 094 protected static final String __PARAM_CRITERIA_CRITERION = "criterion"; 095 /** Name of parameter into criterion holding the id */ 096 protected static final String __PARAM_CRITERIA_CRITERION_ID = "id"; 097 /** Name of parameter into criterion holding the label */ 098 protected static final String __PARAM_CRITERIA_CRITERION_LABEL = "label"; 099 /** Name of parameter into criterion holding the type */ 100 protected static final String __PARAM_CRITERIA_CRITERION_TYPE = "type"; 101 /** Name of paramter holding columns */ 102 protected static final String __PARAM_COLUMNS = "columns"; 103 /** Name of paramter into columns holding column */ 104 protected static final String __PARAM_COLUMNS_COLUMN = "column"; 105 106 /** Default language configured for ODF */ 107 protected String _odfLang; 108 109 /** Name of the Apogée column which contains the ID */ 110 protected String _idColumn; 111 112 /** Mapping between metadata and columns */ 113 protected Map<String, List<String>> _mapping; 114 115 /** External fields */ 116 protected Set<String> _extFields; 117 118 /** Synchronized fields */ 119 protected Set<String> _syncFields; 120 121 /** Synchronized fields */ 122 protected Set<String> _columns; 123 124 /** Synchronized fields */ 125 protected Set<ApogeeCriterion> _criteria; 126 127 /** Context */ 128 protected Context _context; 129 130 /** The DAO for remote DB Apogee */ 131 protected ApogeeDAO _apogeeDAO; 132 133 /** The JSON utils */ 134 protected JSONUtils _jsonUtils; 135 136 @Override 137 public void service(ServiceManager manager) throws ServiceException 138 { 139 super.service(manager); 140 _apogeeDAO = (ApogeeDAO) manager.lookup(ApogeeDAO.ROLE); 141 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 142 } 143 144 @Override 145 public void contextualize(Context context) throws ContextException 146 { 147 _context = context; 148 } 149 150 @Override 151 protected void configureDataSource(Configuration configuration) throws ConfigurationException 152 { 153 @SuppressWarnings("resource") 154 InputStream is = null; 155 _odfLang = Config.getInstance().getValueAsString("odf.programs.lang"); 156 try 157 { 158 org.apache.cocoon.environment.Context ctx = (org.apache.cocoon.environment.Context) _context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 159 File apogeeMapping = new File(ctx.getRealPath("/WEB-INF/param/odf/apogee-mapping.xml")); 160 if (!apogeeMapping.isFile()) 161 { 162 is = getClass().getResourceAsStream("/org/ametys/plugins/odfsync/apogee/apogee-mapping.xml"); 163 } 164 else 165 { 166 is = new FileInputStream(apogeeMapping); 167 } 168 Configuration cfg = new DefaultConfigurationBuilder().build(is); 169 Configuration child = cfg.getChild(getMappingName()); 170 if (child != null) 171 { 172 _criteria = new LinkedHashSet<>(); 173 _columns = new LinkedHashSet<>(); 174 _idColumn = child.getChild(__PARAM_ID_COLUMN).getValue(); 175 _mapping = new HashMap<>(); 176 _extFields = new HashSet<>(); 177 _syncFields = new HashSet<>(); 178 String mappingAsString = child.getChild(__PARAM_MAPPING).getValue(); 179 _mapping.put(getIdField(), ImmutableList.of(getIdColumn())); 180 if (StringUtils.isNotEmpty(mappingAsString)) 181 { 182 List<Object> mappingAsList = _jsonUtils.convertJsonToList(mappingAsString); 183 for (Object object : mappingAsList) 184 { 185 @SuppressWarnings("unchecked") 186 Map<String, Object> field = (Map<String, Object>) object; 187 188 String metadataRef = (String) field.get(__PARAM_MAPPING_METADATA_REF); 189 190 String[] attributes = ((String) field.get(__PARAM_MAPPING_ATTRIBUTE)).split(","); 191 _mapping.put(metadataRef, Arrays.asList(attributes)); 192 193 boolean isSynchronized = field.containsKey(__PARAM_MAPPING_SYNCHRO) ? (Boolean) field.get(__PARAM_MAPPING_SYNCHRO) : false; 194 if (isSynchronized) 195 { 196 _syncFields.add(metadataRef); 197 } 198 else 199 { 200 _extFields.add(metadataRef); 201 } 202 } 203 } 204 205 Configuration[] criteria = child.getChild(__PARAM_CRITERIA).getChildren(__PARAM_CRITERIA_CRITERION); 206 for (Configuration criterion : criteria) 207 { 208 String id = criterion.getChild(__PARAM_CRITERIA_CRITERION_ID).getValue(); 209 I18nizableText label = _getCriterionLabel(criterion.getChild(__PARAM_CRITERIA_CRITERION_LABEL), id); 210 String type = criterion.getChild(__PARAM_CRITERIA_CRITERION_TYPE).getValue("STRING"); 211 212 _criteria.add(new ApogeeCriterion(id, label, type)); 213 } 214 215 Configuration[] columns = child.getChild(__PARAM_COLUMNS).getChildren(__PARAM_COLUMNS_COLUMN); 216 for (Configuration column : columns) 217 { 218 _columns.add(column.getValue()); 219 } 220 } 221 } 222 catch (Exception e) 223 { 224 throw new ConfigurationException("Error while parsing apogee-mapping.xml", e); 225 } 226 finally 227 { 228 IOUtils.closeQuietly(is); 229 } 230 } 231 232 private I18nizableText _getCriterionLabel(Configuration configuration, String defaultValue) 233 { 234 if (configuration.getAttributeAsBoolean("i18n", false)) 235 { 236 return new I18nizableText("plugin.odf-sync", configuration.getValue(defaultValue)); 237 } 238 else 239 { 240 return new I18nizableText(configuration.getValue(defaultValue)); 241 } 242 } 243 244 @Override 245 public List<ModifiableDefaultContent> populate(Logger logger) 246 { 247 boolean isRequestAttributeOwner = false; 248 249 Request request = ContextHelper.getRequest(_context); 250 if (request.getAttribute(HANDLE_CONTENTS) == null) 251 { 252 request.setAttribute(HANDLE_CONTENTS, new HashSet<String>()); 253 isRequestAttributeOwner = true; 254 } 255 256 List<ModifiableDefaultContent> populatedContents = super.populate(logger); 257 258 if (isRequestAttributeOwner) 259 { 260 request.removeAttribute(HANDLE_CONTENTS); 261 } 262 263 return populatedContents; 264 } 265 266 @Override 267 protected Map<String, Map<String, Object>> internalSearch(Map<String, Object> parameters, int offset, int limit, List<Object> sort, Logger logger) 268 { 269 Map<String, Object> searchParams = new HashMap<>(parameters); 270 if (offset > 0) 271 { 272 searchParams.put("__offset", offset); 273 } 274 if (limit < Integer.MAX_VALUE) 275 { 276 searchParams.put("__limit", offset + limit); 277 } 278 searchParams.put("__order", getSort(sort)); 279 280 // We don't use session.selectMap which reorder data 281 List<Map<String, Object>> requestValues = _search(searchParams, logger); 282 283 String idColumn = getIdColumn(); 284 Map<String, Map<String, Object>> results = new LinkedHashMap<>(); 285 for (Map<String, Object> contentValues : requestValues) 286 { 287 results.put(contentValues.get(idColumn).toString(), contentValues); 288 } 289 290 for (Map<String, Object> result : results.values()) 291 { 292 result.put(SCC_UNIQUE_ID, result.get(getIdColumn())); 293 } 294 295 return results; 296 } 297 298 @Override 299 protected Map<String, Map<String, List<Object>>> getRemoteValues(Map<String, Object> parameters, Logger logger) 300 { 301 Map<String, Map<String, List<Object>>> remoteValues = new HashMap<>(); 302 303 Map<String, Map<String, Object>> results = internalSearch(parameters, 0, Integer.MAX_VALUE, null, logger); 304 305 if (results != null) 306 { 307 remoteValues = _sccHelper.organizeRemoteValuesByMetadata(results, _mapping); 308 } 309 310 return remoteValues; 311 } 312 313 @Override 314 protected List<ModifiableDefaultContent> _importOrSynchronizeContent(String idValue, Map<String, List<Object>> remoteValues, boolean forceImport, Logger logger) 315 { 316 return _importOrSynchronizeContent(idValue, _odfLang, remoteValues, forceImport, logger); 317 } 318 319 @Override 320 public List<ModifiableDefaultContent> importContent(String idValue, Map<String, Object> importParams, Logger logger) throws Exception 321 { 322 boolean isRequestAttributeOwner = false; 323 324 Request request = ContextHelper.getRequest(_context); 325 if (request.getAttribute(HANDLE_CONTENTS) == null) 326 { 327 request.setAttribute(HANDLE_CONTENTS, new HashSet<String>()); 328 isRequestAttributeOwner = true; 329 } 330 331 List<ModifiableDefaultContent> createdContents = new ArrayList<>(); 332 333 Map<String, Object> parameters = putIdParameter(idValue); 334 Map<String, Map<String, List<Object>>> results = getTransformedRemoteValues(parameters, logger); 335 if (!results.isEmpty()) 336 { 337 try 338 { 339 createdContents.add(_importContent(idValue, importParams, _odfLang, results.get(idValue), logger)); 340 } 341 catch (Exception e) 342 { 343 _nbError++; 344 logger.error("An error occurred while importing or synchronizing content", e); 345 } 346 } 347 348 if (isRequestAttributeOwner) 349 { 350 request.removeAttribute(HANDLE_CONTENTS); 351 } 352 353 return createdContents; 354 } 355 356 @Override 357 public void synchronizeContent(ModifiableDefaultContent content, Logger logger) throws Exception 358 { 359 boolean isRequestAttributeOwner = false; 360 361 Request request = ContextHelper.getRequest(_context); 362 if (request.getAttribute(HANDLE_CONTENTS) == null) 363 { 364 request.setAttribute(HANDLE_CONTENTS, new HashSet<String>()); 365 isRequestAttributeOwner = true; 366 } 367 368 super.synchronizeContent(content, logger); 369 370 if (isRequestAttributeOwner) 371 { 372 request.removeAttribute(HANDLE_CONTENTS); 373 } 374 } 375 376 @Override 377 protected Map<String, Object> putIdParameter(String idValue) 378 { 379 Map<String, Object> parameters = new HashMap<>(); 380 parameters.put(getIdField(), idValue); 381 return parameters; 382 } 383 384 /** 385 * Search the contents with the search parameters. Use id parameter to search an unique content. 386 * @param searchParams Search parameters 387 * @param logger The logger 388 * @return A Map of mapped metadatas extract from Apogée database ordered by content unique Apogée ID 389 */ 390 protected abstract List<Map<String, Object>> _search(Map<String, Object> searchParams, Logger logger); 391 392 /** 393 * Convert the {@link BigDecimal} values retrieved from database into long values 394 * @param searchResults The initial search results from database 395 * @return The converted search results 396 */ 397 protected List<Map<String, Object>> _convertBigDecimal(List<Map<String, Object>> searchResults) 398 { 399 List<Map<String, Object>> convertedSearchResults = new ArrayList<>(); 400 401 for (Map<String, Object> searchResult : searchResults) 402 { 403 for (String key : searchResult.keySet()) 404 { 405 if (searchResult.get(key) instanceof BigDecimal) 406 { 407 searchResult.put(key, ((BigDecimal) searchResult.get(key)).longValue()); 408 } 409 } 410 411 convertedSearchResults.add(searchResult); 412 } 413 414 return convertedSearchResults; 415 } 416 417 /** 418 * Transform CLOB value to String value. 419 * @param value The input value 420 * @param idValue The identifier of the program 421 * @param logger The logger 422 * @return the same value, with CLOB transformed to String. 423 */ 424 protected Object _transformClobToString(Object value, String idValue, Logger logger) 425 { 426 if (value instanceof Clob) 427 { 428 Clob clob = (Clob) value; 429 try 430 { 431 String strValue = IOUtils.toString(clob.getCharacterStream()); 432 return CharMatcher.javaIsoControl().and(CharMatcher.anyOf("\r\n\t").negate()).removeFrom(strValue); 433 } 434 catch (SQLException | IOException e) 435 { 436 logger.error("Unable to get education add elements from the program '{}'.", idValue, e); 437 return null; 438 } 439 finally 440 { 441 try 442 { 443 clob.free(); 444 } 445 catch (SQLException e) 446 { 447 // Ignore the exception. 448 } 449 } 450 } 451 452 return value; 453 } 454 455 /** 456 * Get the name of the mapping. 457 * @return the mapping name 458 */ 459 protected abstract String getMappingName(); 460 461 /** 462 * Get the identifier column (can be a concatened column). 463 * @return the column id 464 */ 465 protected String getIdColumn() 466 { 467 return _idColumn; 468 } 469 470 @Override 471 public String getIdField() 472 { 473 return "apogeeSyncCode"; 474 } 475 476 @Override 477 public Set<String> getLocalAndExternalFields(Map<String, Object> additionalParameters) 478 { 479 return _syncFields; 480 } 481 482 @Override 483 public Set<String> getExternalOnlyFields(Map<String, Object> additionalParameters) 484 { 485 return _extFields; 486 } 487 488 @Override 489 protected boolean _fillContent(Map<String, List<Object>> remoteValues, ModifiableDefaultContent content, boolean create, Logger logger) 490 { 491 Map<String, Object> params = ImmutableMap.of("contentType", getContentType()); 492 ModifiableCompositeMetadata holder = content.getMetadataHolder(); 493 494 for (String metadataName : getRichTextFields()) 495 { 496 if (remoteValues.containsKey(metadataName)) 497 { 498 List<String> lines = new LinkedList<>(); 499 for (Object remoteText : remoteValues.get(metadataName)) 500 { 501 lines.add(remoteText.toString()); 502 } 503 504 String docbook = ContentImporterHelper.textToDocbook(lines.toArray(new String[lines.size()])); 505 boolean synchronize = getLocalAndExternalFields(params).contains(metadataName); 506 507 try (ByteArrayInputStream is = new ByteArrayInputStream(docbook.getBytes("UTF-8"))) 508 { 509 ModifiableRichText richText = ExternalizableMetadataHelper.getRichText(holder, metadataName, synchronize ? ExternalizableMetadataStatus.EXTERNAL : ExternalizableMetadataStatus.LOCAL, true); 510 richText.setInputStream(is); 511 richText.setMimeType("text/xml"); 512 richText.setLastModified(new Date()); 513 } 514 catch (IOException e) 515 { 516 logger.error("An error occured while parsing the rich text '{}' of the content '{}'", metadataName, content.getTitle(), e); 517 } 518 519 remoteValues.remove(metadataName); 520 } 521 } 522 523 boolean hasChanges = super._fillContent(remoteValues, content, create, logger); 524 hasChanges = _handleAdditionalMetadata(holder, create) || hasChanges; 525 526 if (!holder.hasMetadata(getIdField())) 527 { 528 holder.setMetadata(getIdField(), remoteValues.get(getIdField()).get(0).toString()); 529 hasChanges |= create; 530 } 531 532 return hasChanges; 533 } 534 535 /** 536 * Method to add additional metadata on import or synchronize. 537 * @param holder The holder of the content to update 538 * @param create If we are on creation mode 539 * @return <code>true</code> if changes has been made 540 */ 541 protected boolean _handleAdditionalMetadata(ModifiableCompositeMetadata holder, boolean create) 542 { 543 // Do nothing 544 return false; 545 } 546 547 /** 548 * Get the list of rich text fields of the imported content. 549 * @return The list of the rich text fields metadata name 550 */ 551 protected Set<String> getRichTextFields() 552 { 553 return new HashSet<>(); 554 } 555 556 @Override 557 protected void configureSearchModel() 558 { 559 for (ApogeeCriterion criterion : _criteria) 560 { 561 _searchModelConfiguration.addCriterion(criterion.getId(), criterion.getLabel(), criterion.getType()); 562 } 563 for (String columnName : _columns) 564 { 565 _searchModelConfiguration.addColumn(columnName); 566 } 567 } 568 569 @Override 570 protected boolean additionalImportOperations(ModifiableDefaultContent content, Map<String, List<Object>> remoteValues, Map<String, Object> importParams, Logger logger) 571 { 572 boolean hasChanges = super.additionalImportOperations(content, remoteValues, importParams, logger); 573 574 Object parentId = importParams != null && importParams.containsKey("parentId") ? importParams.get("parentId") : null; 575 ModifiableDefaultContent parentContent = parentId != null ? _resolver.resolveById(parentId.toString()) : null; 576 577 hasChanges = handleParent(content, parentContent, logger) || hasChanges; 578 hasChanges = handleChildren(content, logger) || hasChanges; 579 hasChanges = setAdditionalMetadata(content, remoteValues, logger) || hasChanges; 580 581 return hasChanges; 582 } 583 584 @Override 585 protected boolean additionalSynchronizeOperations(ModifiableDefaultContent content, Map<String, List<Object>> remoteValues, Logger logger) 586 { 587 boolean hasChanges = super.additionalSynchronizeOperations(content, remoteValues, logger); 588 hasChanges = additionalImportOperations(content, remoteValues, null, logger) || hasChanges; 589 return hasChanges; 590 } 591 592 /** 593 * Set the parent metadata and invert relation. 594 * @param currentContent Current content 595 * @param parentContent Parent content to set 596 * @param logger The logger 597 * @return <code>true</code> if there are changes 598 */ 599 protected boolean handleParent(ModifiableDefaultContent currentContent, ModifiableDefaultContent parentContent, Logger logger) 600 { 601 // Nothing to do by default 602 return false; 603 } 604 605 /** 606 * Set the children metadata and invert relation, import and synchronize the children too. 607 * @param content Current content 608 * @param logger The logger 609 * @return <code>true</code> if there are changes 610 */ 611 protected boolean handleChildren(ModifiableDefaultContent content, Logger logger) 612 { 613 // Nothing to do by default 614 return false; 615 } 616 617 /** 618 * Set the additional metadata. 619 * @param content Current content 620 * @param remoteValues Values of the content 621 * @param logger The logger 622 * @return <code>true</code> if there are changes 623 */ 624 protected boolean setAdditionalMetadata(ModifiableDefaultContent content, Map<String, List<Object>> remoteValues, Logger logger) 625 { 626 // Nothing to do by default 627 return false; 628 } 629 630 @SuppressWarnings("unchecked") 631 private String getSort(List<Object> sortList) 632 { 633 if (sortList != null) 634 { 635 StringBuilder sort = new StringBuilder(); 636 637 for (Object sortValueObj : sortList) 638 { 639 Map<String, Object> sortValue = (Map<String, Object>) sortValueObj; 640 641 sort.append(sortValue.get("property")); 642 if (sortValue.containsKey("direction")) 643 { 644 sort.append(" "); 645 sort.append(sortValue.get("direction")); 646 sort.append(","); 647 } 648 else 649 { 650 sort.append(" ASC,"); 651 } 652 } 653 654 sort.deleteCharAt(sort.length() - 1); 655 656 return sort.toString(); 657 } 658 659 return null; 660 } 661 662 @Override 663 public int getTotalCount(Map<String, Object> parameters, Logger logger) 664 { 665 // Remove empty parameters 666 Map<String, Object> searchParams = new HashMap<>(); 667 for (String parameterName : parameters.keySet()) 668 { 669 Object parameterValue = parameters.get(parameterName); 670 if (parameterValue != null && !parameterValue.toString().isEmpty()) 671 { 672 searchParams.put(parameterName, parameterValue); 673 } 674 } 675 676 searchParams.put("__count", true); 677 678 List<Map<String, Object>> results = _search(searchParams, logger); 679 if (results != null && !results.isEmpty()) 680 { 681 return Integer.valueOf(results.get(0).get("COUNT(*)").toString()).intValue(); 682 } 683 684 return 0; 685 } 686 687 @Override 688 protected ModifiableDefaultContent _importContent(String idValue, Map<String, Object> importParams, String lang, Map<String, List<Object>> remoteValues, Logger logger) throws Exception 689 { 690 ModifiableDefaultContent content = super._importContent(idValue, importParams, lang, _transformOrgUnitMetadata(remoteValues, logger), logger); 691 if (content != null) 692 { 693 addToHandleContents(content.getId()); 694 } 695 return content; 696 } 697 698 @Override 699 protected ModifiableDefaultContent _synchronizeContent(ModifiableDefaultContent content, Map<String, List<Object>> remoteValues, Logger logger) throws Exception 700 { 701 if (!addToHandleContents(content.getId())) 702 { 703 return content; 704 } 705 return super._synchronizeContent(content, _transformOrgUnitMetadata(remoteValues, logger), logger); 706 } 707 708 /** 709 * Import and synchronize children of the given content, then edit the structure of the content and its children. 710 * @param content Parent content 711 * @param sccModelId SCC model ID 712 * @param metadataName Metadata name to set 713 * @param invertMetadataName Metadata name of the invert relation 714 * @param logger The logger 715 * @return <code>true</code> if there are changes 716 */ 717 protected boolean importOrSynchronizeChildren(ModifiableDefaultContent content, String sccModelId, String metadataName, String invertMetadataName, Logger logger) 718 { 719 boolean hasChanges = false; 720 721 ModifiableCompositeMetadata cm = content.getMetadataHolder(); 722 723 if (removalSync() && cm.hasMetadata(metadataName)) 724 { 725 String[] children = cm.getStringArray(metadataName); 726 hasChanges = ExternalizableMetadataHelper.removeMetadataIfExists(cm, metadataName); 727 728 for (String child : children) 729 { 730 ModifiableDefaultContent childContent = _resolver.resolveById(child); 731 if (child != null) 732 { 733 hasChanges = _updateRelation(childContent.getMetadataHolder(), invertMetadataName, content, true) || hasChanges; 734 } 735 } 736 } 737 738 // Get the SCC for children 739 SynchronizableContentsCollection scc = _sccHelper.getSCCFromModelId(sccModelId); 740 741 // Search for children 742 if (scc != null && scc instanceof ApogeeSynchronizableContentsCollection) 743 { 744 String syncCode = cm.getString(getIdField()); 745 List<ModifiableDefaultContent> children = ((ApogeeSynchronizableContentsCollection) scc).importOrSynchronizeContents(ImmutableMap.of("parentCode", syncCode), logger); 746 747 hasChanges = ExternalizableMetadataHelper.setMetadata(cm, metadataName, children.toArray(new ModifiableDefaultContent[children.size()])) || hasChanges; 748 for (ModifiableDefaultContent child : children) 749 { 750 hasChanges = _updateRelation(child.getMetadataHolder(), invertMetadataName, content) || hasChanges; 751 } 752 } 753 754 return hasChanges; 755 } 756 757 @Override 758 public List<ModifiableDefaultContent> importOrSynchronizeContents(Map<String, Object> searchParams, Logger logger) 759 { 760 return _importOrSynchronizeContents(searchParams, true, logger); 761 } 762 763 /** 764 * Add the content ID to the handle contents list. 765 * @param contentId Content ID 766 * @return <code>true</code> if the content ID have been added, <code>false</code> is returned if the content ID already exists in the handle contents list. 767 */ 768 protected boolean addToHandleContents(String contentId) 769 { 770 Request request = ContextHelper.getRequest(_context); 771 @SuppressWarnings("unchecked") 772 Set<String> handleContents = (Set<String>) request.getAttribute(HANDLE_CONTENTS); 773 boolean added = handleContents.add(contentId); 774 request.setAttribute(HANDLE_CONTENTS, handleContents); 775 return added; 776 } 777 778 @SuppressWarnings("unchecked") 779 private Map<String, List<Object>> _transformOrgUnitMetadata(Map<String, List<Object>> remoteValues, Logger logger) 780 { 781 // Transform orgUnit values and import content if necessary (useful for Course and SubProgram) 782 SynchronizableContentsCollection scc = _sccHelper.getSCCFromModelId(OrgUnitSynchronizableContentsCollection.MODEL_ID); 783 784 List<Object> orgUnitCodes = remoteValues.get("orgUnit"); 785 if (orgUnitCodes != null && !orgUnitCodes.isEmpty()) 786 { 787 List<?> orgUnitContents = null; 788 String orgUnitCode = orgUnitCodes.get(0).toString(); 789 790 if (scc != null) 791 { 792 try 793 { 794 ModifiableDefaultContent orgUnitContent = scc.getContent(_odfLang, orgUnitCode); 795 if (orgUnitContent == null) 796 { 797 orgUnitContents = scc.importContent(orgUnitCode, null, logger); 798 } 799 else 800 { 801 orgUnitContents = ImmutableList.of(orgUnitContent); 802 } 803 } 804 catch (Exception e) 805 { 806 logger.error("An error occured during the import of the OrgUnit identified by the synchronization code '{}'", orgUnitCode, e); 807 } 808 } 809 810 if (orgUnitContents == null) 811 { 812 // Impossible link to orgUnit 813 remoteValues.remove("orgUnit"); 814 logger.warn("Impossible to import the OrgUnit with the synchronization code '{}', check if you set the OrgUnit SCC with the following model ID: '{}'", orgUnitCode, OrgUnitSynchronizableContentsCollection.MODEL_ID); 815 } 816 else 817 { 818 remoteValues.put("orgUnit", (List<Object>) orgUnitContents); 819 } 820 } 821 822 return remoteValues; 823 } 824 825 @Override 826 public boolean handleRightAssignmentContext() 827 { 828 // Rights on ODF contents are handled by ODFRightAssignmentContext 829 return false; 830 } 831}