001/* 002 * Copyright 2021 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.pegase; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.nio.charset.StandardCharsets; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.HashMap; 024import java.util.HashSet; 025import java.util.LinkedHashMap; 026import java.util.LinkedHashSet; 027import java.util.List; 028import java.util.Map; 029import java.util.Objects; 030import java.util.Optional; 031import java.util.Set; 032import java.util.function.Predicate; 033import java.util.stream.Collectors; 034import java.util.stream.Stream; 035 036import org.apache.avalon.framework.configuration.Configuration; 037import org.apache.avalon.framework.configuration.ConfigurationException; 038import org.apache.avalon.framework.service.ServiceException; 039import org.apache.avalon.framework.service.ServiceManager; 040import org.apache.commons.io.IOUtils; 041import org.apache.commons.lang3.StringUtils; 042import org.apache.http.HttpEntity; 043import org.apache.http.NameValuePair; 044import org.apache.http.StatusLine; 045import org.apache.http.client.HttpResponseException; 046import org.apache.http.client.config.RequestConfig; 047import org.apache.http.client.entity.UrlEncodedFormEntity; 048import org.apache.http.client.methods.CloseableHttpResponse; 049import org.apache.http.client.methods.HttpGet; 050import org.apache.http.client.methods.HttpPost; 051import org.apache.http.client.methods.HttpUriRequest; 052import org.apache.http.impl.client.CloseableHttpClient; 053import org.apache.http.impl.client.HttpClientBuilder; 054import org.apache.http.message.BasicNameValuePair; 055import org.slf4j.Logger; 056 057import org.ametys.cms.FilterNameHelper; 058import org.ametys.cms.data.ContentValue; 059import org.ametys.cms.repository.Content; 060import org.ametys.cms.repository.ContentQueryHelper; 061import org.ametys.cms.repository.LanguageExpression; 062import org.ametys.cms.repository.ModifiableContent; 063import org.ametys.cms.repository.WorkflowAwareContent; 064import org.ametys.core.util.JSONUtils; 065import org.ametys.core.util.URIUtils; 066import org.ametys.odf.ProgramItem; 067import org.ametys.odf.catalog.CatalogsManager; 068import org.ametys.odf.course.Course; 069import org.ametys.odf.course.CourseFactory; 070import org.ametys.odf.courselist.CourseList; 071import org.ametys.odf.courselist.CourseListFactory; 072import org.ametys.odf.program.ContainerFactory; 073import org.ametys.odf.program.ProgramFactory; 074import org.ametys.odf.program.ProgramPart; 075import org.ametys.odf.program.SubProgramFactory; 076import org.ametys.odf.program.TraversableProgramPart; 077import org.ametys.odf.workflow.AbstractCreateODFContentFunction; 078import org.ametys.plugins.contentio.synchronize.AbstractSimpleSynchronizableContentsCollection; 079import org.ametys.plugins.odfsync.utils.ContentWorkflowDescription; 080import org.ametys.plugins.repository.AmetysObjectIterable; 081import org.ametys.plugins.repository.query.expression.AndExpression; 082import org.ametys.plugins.repository.query.expression.Expression; 083import org.ametys.plugins.repository.query.expression.Expression.Operator; 084import org.ametys.plugins.repository.query.expression.StringExpression; 085import org.ametys.runtime.config.Config; 086import org.ametys.runtime.i18n.I18nizableText; 087import org.ametys.runtime.model.View; 088 089import com.google.common.collect.ImmutableList; 090import com.opensymphony.workflow.WorkflowException; 091 092/** 093 * SCC for Pegase (COF). 094 */ 095public class PegaseSynchronizableContentsCollection extends AbstractSimpleSynchronizableContentsCollection 096{ 097 /** The JSON utils */ 098 protected JSONUtils _jsonUtils; 099 100 /** The catalogs manager */ 101 protected CatalogsManager _catalogsManager; 102 103 /** Mapping between metadata and columns */ 104 protected Map<String, Map<String, List<String>>> _mappingByContentType; 105 106 /** Search fields to display */ 107 protected Set<String> _searchFields; 108 109 /** List of imported contents */ 110 protected Map<String, Integer> _importedContents; 111 112 /** List of synchronized contents having differences */ 113 protected Set<String> _synchronizedContents; 114 115 /** List of updated contents by relation */ 116 protected Set<String> _updatedRelationContents; 117 118 /** Map to link contents to its children at the end of the process */ 119 protected Map<String, Set<String>> _contentsChildren; 120 121 /** Default language configured for ODF */ 122 protected String _odfLang; 123 124 @Override 125 public void service(ServiceManager manager) throws ServiceException 126 { 127 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 128 _catalogsManager = (CatalogsManager) manager.lookup(CatalogsManager.ROLE); 129 super.service(manager); 130 } 131 132 @Override 133 public String getIdField() 134 { 135 return "pegaseSyncCode"; 136 } 137 138 /** 139 * Get the identifier JSON field. 140 * @return the column id 141 */ 142 protected String getIdColumn() 143 { 144 return "id"; 145 } 146 147 @SuppressWarnings("unchecked") 148 @Override 149 public Set<String> getLocalAndExternalFields(Map<String, Object> additionalParameters) 150 { 151 return Optional.ofNullable(additionalParameters) 152 .map(params -> params.get("contentTypes")) 153 .filter(List.class::isInstance) 154 .map(cTypes -> (List<String>) cTypes) 155 .filter(Predicate.not(List::isEmpty)) 156 .map(l -> l.get(0)) 157 .map(_mappingByContentType::get) 158 .map(Map::keySet) 159 .orElse(Set.of()); 160 } 161 162 @Override 163 protected Map<String, Object> putIdParameter(String idValue) 164 { 165 Map<String, Object> parameters = new HashMap<>(); 166 parameters.put(getIdColumn(), List.of(idValue)); 167 return parameters; 168 } 169 170 @SuppressWarnings("unchecked") 171 @Override 172 protected Map<String, Map<String, Object>> internalSearch(Map<String, Object> parameters, int offset, int limit, List<Object> sort, Logger logger) 173 { 174 Map<String, Map<String, Object>> results = new HashMap<>(); 175 176 try (CloseableHttpClient httpClient = _getHttpClient()) 177 { 178 // FIXME If 403 error with header cache-control = no-cacheno-storemax-age=0must-revalidate, automatically get a new token ? 179 String token = getToken(httpClient); 180 boolean importMode = (boolean) parameters.getOrDefault("import", false); 181 182 List<String> idValues = (List<String>) parameters.computeIfAbsent(getIdColumn(), __ -> new ArrayList<>()); 183 184 // Simple search or no defined ID during import (global import) 185 if (idValues.isEmpty()) 186 { 187 /* Future proof (not implemented in v1 nor in v2-draft) 188 Map<String, Object> pageable = new HashMap<>(); 189 int pageSize = limit - offset; 190 pageable.put("page", offset / limit); Numérotation à partir de 0 ou 1 ? 191 pageable.put("taille", pageSize); 192 pageable.put("tri", sort...); 193 => add to url parameters 194 */ 195 196 String jsonString = _executeGetRequest(httpClient, "/formations", Map.of(), token); 197 198 // Handle the response 199 List<Object> json = _jsonUtils.convertJsonToList(jsonString); 200 for (Object obj : json) 201 { 202 Map<String, Object> programDescription = (Map<String, Object>) obj; 203 String idValue = (String) programDescription.get(getIdColumn()); 204 205 // If not an import, fill the results 206 if (!importMode) 207 { 208 Map<String, Object> result = results.computeIfAbsent(idValue, __ -> new HashMap<>()); 209 for (String keptField : _searchFields) 210 { 211 result.put(keptField, programDescription.get(keptField)); 212 } 213 result.put(SCC_UNIQUE_ID, idValue); 214 } 215 else 216 { 217 idValues.add(idValue); 218 } 219 } 220 } 221 222 // If import, get the tree 223 if (importMode) 224 { 225 Set<String> handledObjects = new HashSet<>(); 226 for (String idValue : idValues) 227 { 228 String jsonValue = _executeGetRequest(httpClient, "/arbres/" + idValue, Map.of(), token); 229 230 // Handle the response 231 // First iteration on the root 232 List<Map<String, Object>> children = new ArrayList<>(); 233 children.add((Map<String, Object>) _jsonUtils.convertJsonToMap(jsonValue).get("racine")); 234 235 while (!children.isEmpty()) 236 { 237 Map<String, Object> child = children.remove(0); 238 String childCode = (String) child.get("code"); 239 if (handledObjects.add(childCode)) 240 { 241 Map<String, List<Object>> mappedChildObject = _jsonToObject(child); 242 // Add children to the list to iterate on it 243 children.addAll((List<Map<String, Object>>) (Object) mappedChildObject.remove("jsonChildren")); 244 // Add mapped object to the results 245 results.put((String) mappedChildObject.remove(getIdField()).get(0), (Map<String, Object>) (Object) mappedChildObject); 246 } 247 } 248 } 249 } 250 } 251 catch (Exception e) 252 { 253 throw new RuntimeException("Error while getting remote values", e); 254 } 255 return results; 256 } 257 258 @Override 259 public List<ModifiableContent> importContent(String idValue, Map<String, Object> importParams, Logger logger) throws Exception 260 { 261 Map<String, Object> parameters = putIdParameter(idValue); 262 return _importOrSynchronizeContents(parameters, true, logger); 263 } 264 265 @Override 266 public void synchronizeContent(ModifiableContent content, Logger logger) throws Exception 267 { 268 String idValue = content.getValue(getIdField()); 269 Map<String, Object> parameters = putIdParameter(idValue); 270 _importOrSynchronizeContents(parameters, true, logger); 271 } 272 273 @Override 274 protected List<ModifiableContent> _importOrSynchronizeContents(Map<String, Object> searchParams, boolean forceImport, Logger logger) 275 { 276 _importedContents = new HashMap<>(); 277 _synchronizedContents = new HashSet<>(); 278 _updatedRelationContents = new HashSet<>(); 279 _contentsChildren = new HashMap<>(); 280 281 List<ModifiableContent> contents = super._importOrSynchronizeContents(searchParams, forceImport, logger); 282 _updateRelations(logger); 283 _updateWorkflowStatus(logger); 284 285 _importedContents = null; 286 _synchronizedContents = null; 287 _updatedRelationContents = null; 288 _contentsChildren = null; 289 290 return contents; 291 } 292 293 private void _updateRelations(Logger logger) 294 { 295 for (String contentId : _contentsChildren.keySet()) 296 { 297 WorkflowAwareContent content = _resolver.resolveById(contentId); 298 Set<String> childrenCodes = _contentsChildren.get(contentId); 299 String contentLanguage = content.getLanguage(); 300 String contentCatalog = content.getValue("catalog"); 301 Map<String, Set<String>> childrenByAttributeName = new HashMap<>(); 302 303 for (String childCode : childrenCodes) 304 { 305 String idSync = _buildSynCodeFromCode(childCode); 306 307 Expression expression = new AndExpression( 308 _sccHelper.getCollectionExpression(getId()), 309 new StringExpression(getIdField(), Operator.EQ, idSync), 310 new StringExpression("catalog", Operator.EQ, contentCatalog), 311 new LanguageExpression(Operator.EQ, contentLanguage) 312 ); 313 314 ModifiableContent childContent = _resolver.<ModifiableContent>query(ContentQueryHelper.getContentXPathQuery(expression)) 315 .stream() 316 .findFirst() 317 .orElse(null); 318 319 if (childContent == null) 320 { 321 logger.warn("Content with code '{}' in {} on catalog '{}' was not found in the repository to update relations with content [{}] '{}' ({}).", idSync, contentLanguage, contentCatalog, content.getValue(getIdField()), content.getTitle(), contentCatalog); 322 } 323 else 324 { 325 String attributesName = _getChildAttributeName(content, childContent); 326 if (attributesName != null) 327 { 328 Set<String> children = childrenByAttributeName.computeIfAbsent(attributesName, __ -> new LinkedHashSet<>()); 329 children.add(childContent.getId()); 330 } 331 else 332 { 333 logger.warn("The child content [{}] '{}' of type '{}' is not compatible with parent content [{}] '{}' of type '{}'.", idSync, childContent.getTitle(), childContent.getTypes()[0], content.getValue(getIdField()), content.getTitle(), content.getTypes()[0]); 334 } 335 } 336 } 337 338 _updateRelations(content, childrenByAttributeName, logger); 339 } 340 341 } 342 343 private String _getChildAttributeName(Content parentContent, Content childContent) 344 { 345 if (childContent instanceof Course && parentContent instanceof CourseList) 346 { 347 return CourseList.CHILD_COURSES; 348 } 349 350 if (parentContent instanceof Course && childContent instanceof CourseList) 351 { 352 return Course.CHILD_COURSE_LISTS; 353 } 354 355 if (parentContent instanceof TraversableProgramPart && childContent instanceof ProgramPart) 356 { 357 return TraversableProgramPart.CHILD_PROGRAM_PARTS; 358 } 359 360 return null; 361 } 362 363 private void _updateRelations(WorkflowAwareContent content, Map<String, Set<String>> contentRelationsByAttribute, Logger logger) 364 { 365 // Compute the view 366 View view = View.of(content.getModel(), contentRelationsByAttribute.keySet().toArray(new String[contentRelationsByAttribute.size()])); 367 368 // Compute values 369 Map<String, Object> values = new HashMap<>(); 370 for (String attributeName : contentRelationsByAttribute.keySet()) 371 { 372 // Add the content relations to the existing ones 373 List<String> attributeValue = _getContentAttributeValue(content, attributeName); 374 contentRelationsByAttribute.get(attributeName) 375 .stream() 376 .filter(id -> !attributeValue.contains(id)) 377 .forEach(attributeValue::add); 378 379 values.put(attributeName, attributeValue.toArray(new String[attributeValue.size()])); 380 } 381 382 // Compute not synchronized contents 383 Set<String> notSynchronizedContentIds = contentRelationsByAttribute.values() 384 .stream() 385 .flatMap(Collection::stream) 386 .filter(id -> !_synchronizedContents.contains(id)) 387 .collect(Collectors.toSet()); 388 389 try 390 { 391 _editContent(content, Optional.of(view), values, Map.of(), false, notSynchronizedContentIds, logger); 392 } 393 catch (WorkflowException e) 394 { 395 _nbError++; 396 logger.error("The content '{}' cannot be links edited (workflow action)", content, e); 397 } 398 } 399 400 private List<String> _getContentAttributeValue(Content content, String attributeName) 401 { 402 return Optional.of(attributeName) 403 .map(content::<ContentValue[]>getValue) 404 .map(Stream::of) 405 .orElseGet(Stream::empty) 406 .map(ContentValue::getContentId) 407 .collect(Collectors.toList()); 408 } 409 410 private void _updateWorkflowStatus(Logger logger) 411 { 412 // Validate contents -> only on newly imported contents 413 if (validateAfterImport()) 414 { 415 for (String contentId : _importedContents.keySet()) 416 { 417 WorkflowAwareContent content = _resolver.resolveById(contentId); 418 Integer validationActionId = _importedContents.get(contentId); 419 if (validationActionId > 0) 420 { 421 validateContent(content, validationActionId, logger); 422 } 423 } 424 } 425 } 426 427 @SuppressWarnings("unchecked") 428 private Map<String, List<Object>> _jsonToObject(Map<String, Object> jsonObject) 429 { 430 ContentWorkflowDescription wfDescription = _mapWorkflowDescription((String) jsonObject.get("categorie")); 431 String contentType = wfDescription.getContentType(); 432 Map<String, List<String>> contentTypeMapping = _mappingByContentType.get(contentType); 433 434 Map<String, List<Object>> mappedObject = new HashMap<>(); 435 // Add the contentType 436 mappedObject.put("workflowDescription", List.of(wfDescription)); 437 438 for (String attribute : contentTypeMapping.keySet()) 439 { 440 List<String> jsonKeys = contentTypeMapping.get(attribute); 441 442 List<Object> values = jsonKeys.stream() // For each column corresponding to the metadata 443 .map(key -> _getDeepJsonValue(jsonObject, key)) // Map the values 444 .flatMap(o -> 445 { 446 if (o instanceof Collection<?>) 447 { 448 return ((Collection<?>) o).stream(); 449 } 450 return Stream.of(o); 451 }) // If it's a list of objects, get a flat stream 452 .filter(Objects::nonNull) // Remove null values 453 .collect(Collectors.toList()); // Collect it into a List 454 mappedObject.put(attribute, values); // Add the retrieved metadata values list to the contentResult 455 } 456 457 // Add the ID field 458 // pegaseSyncCode : codeStructure:code::detail/formation/version 459 String syncCode = _buildSynCodeFromCode((String) jsonObject.get("code")); 460 if (ProgramFactory.PROGRAM_CONTENT_TYPE.equals(contentType)) 461 { 462 syncCode += _getDeepJsonValue(jsonObject, "detail/formation/version"); 463 } 464 mappedObject.put(getIdField(), List.of(syncCode)); 465 466 Map<String, Map<String, Object>> jsonChildren = (Map<String, Map<String, Object>>) jsonObject.get("enfants"); 467 // If not GROUPEMENT (CourseList) => split OP (Course) children 468 if (!CourseListFactory.COURSE_LIST_CONTENT_TYPE.equals(contentType)) 469 { 470 Map<String, Map<String, Object>> courses = new LinkedHashMap<>(); 471 for (String childId : Map.copyOf(jsonChildren).keySet()) 472 { 473 Map<String, Object> jsonChild = jsonChildren.get(childId); 474 if ("OP".equals(jsonChild.get("categorie"))) 475 { 476 courses.put(childId, jsonChildren.remove(childId)); 477 } 478 } 479 480 // If there are OP (Course) children 481 if (!courses.isEmpty()) 482 { 483 // Copy the current JSON object with : 484 // - category = "GROUPEMENT" 485 // - code = code + "-list" 486 // - enfants = courses 487 488 String courseListSyncCode = jsonObject.get("code") + "-list"; 489 490 Map<String, Object> jsonCourseList = new HashMap<>(jsonObject); 491 jsonCourseList.put("categorie", "GROUPEMENT"); 492 jsonCourseList.put("code", courseListSyncCode); 493 jsonCourseList.put("enfants", courses); 494 495 // Add the copy to jsonChildren 496 jsonChildren.put(courseListSyncCode, jsonCourseList); 497 } 498 } 499 500 mappedObject.put("jsonChildren", List.copyOf(jsonChildren.values())); 501 502 Set<? extends Object> children = jsonChildren.keySet(); 503 if (!children.isEmpty()) 504 { 505 mappedObject.put("children", List.copyOf(children)); 506 } 507 508 return mappedObject; 509 } 510 511 private String _buildSynCodeFromCode(String code) 512 { 513 return getStructureCode() + ":" + code + "::"; 514 } 515 516 @SuppressWarnings("unchecked") 517 private Object _getDeepJsonValue(Map<String, Object> jsonObject, String jsonKey) 518 { 519 String[] deepKeys = jsonKey.split("/"); 520 Optional<Object> optional = Optional.of(jsonObject); 521 for (int i = 0; i < deepKeys.length && optional.isPresent(); i++) 522 { 523 String deepKey = deepKeys[i]; 524 optional = optional 525 .map(map -> (Map<String, Object>) map) 526 .map(map -> map.get(deepKey)); 527 } 528 529 return optional.orElse(null); 530 } 531 532 /** 533 * Get the workflow description for the given category. 534 * @param category The category 535 * @return the matching workflow description 536 */ 537 protected ContentWorkflowDescription _mapWorkflowDescription(String category) 538 { 539 switch (category) 540 { 541 case "formation": 542 return ContentWorkflowDescription.PROGRAM_WF_DESCRIPTION; 543 case "OO": 544 return ContentWorkflowDescription.SUBPROGRAM_WF_DESCRIPTION; 545 case "OTT": 546 return ContentWorkflowDescription.CONTAINER_WF_DESCRIPTION; 547 case "GROUPEMENT": 548 return ContentWorkflowDescription.COURSELIST_WF_DESCRIPTION; 549 case "OP": 550 return ContentWorkflowDescription.COURSE_WF_DESCRIPTION; 551 default: 552 // throw an exception 553 } 554 throw new IllegalArgumentException("The category '" + category + "' cannot be converted to an Ametys content type."); 555 } 556 557 @SuppressWarnings("unchecked") 558 @Override 559 protected Map<String, Map<String, List<Object>>> getRemoteValues(Map<String, Object> parameters, Logger logger) 560 { 561 return (Map<String, Map<String, List<Object>>>) (Object) internalSearch(parameters, 0, Integer.MAX_VALUE, null, logger); 562 } 563 564 @Override 565 protected void configureDataSource(Configuration configuration) throws ConfigurationException 566 { 567 _odfLang = Config.getInstance().getValue("odf.programs.lang"); 568 569 // Champs affichés dans la recherche 570 _searchFields = new HashSet<>(); 571 _searchFields.add("id"); 572 _searchFields.add("code"); 573 _searchFields.add("libelleCourt"); 574 _searchFields.add("libelleLong"); 575 _searchFields.add("codeTypeDiplome"); 576 _searchFields.add("codeNiveauFormation"); 577 _searchFields.add("codeDomaineFormation"); 578 _searchFields.add("codeNatureDiplome"); 579 _searchFields.add("codeNiveauDiplome"); 580 _searchFields.add("codeMention"); 581 _searchFields.add("codeChampFormation"); 582 _searchFields.add("codeDiplomeSise"); 583 _searchFields.add("niveauSise"); 584 _searchFields.add("ects"); 585 586 _mappingByContentType = new HashMap<>(); 587 588 // Program 589 Map<String, List<String>> contentTypeMapping = new HashMap<>(); 590 contentTypeMapping.put("title", List.of("libelleLong", "libelle")); 591 contentTypeMapping.put("ects", List.of("ects")); 592 contentTypeMapping.put("educationKind", List.of("detail/formation/typeFormation/code")); 593 contentTypeMapping.put("siseCode", List.of("codeDiplomeSise")); 594 contentTypeMapping.put("presentation", List.of("description")); 595 contentTypeMapping.put("mention", List.of("codeMention")); 596 contentTypeMapping.put("domain", List.of("codeDomaineFormation")); 597 contentTypeMapping.put("programField", List.of("codeChampFormation")); 598 contentTypeMapping.put("educationLevel", List.of("codeNiveauFormation")); 599 contentTypeMapping.put("rncpLevel", List.of("codeNiveauDiplome")); 600 contentTypeMapping.put("degree", List.of("codeTypeDiplome")); 601 _mappingByContentType.put(ProgramFactory.PROGRAM_CONTENT_TYPE, contentTypeMapping); 602 603 // SubProgram 604 contentTypeMapping = new HashMap<>(); 605 contentTypeMapping.put("title", List.of("libelleLong", "libelle")); 606 contentTypeMapping.put("ects", List.of("ects")); 607 contentTypeMapping.put("presentation", List.of("description")); 608 contentTypeMapping.put("educationKind", List.of("type/code")); 609 _mappingByContentType.put(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE, contentTypeMapping); 610 611 // Container 612 contentTypeMapping = new HashMap<>(); 613 contentTypeMapping.put("title", List.of("libelleLong", "libelle")); 614 contentTypeMapping.put("ects", List.of("ects")); 615 contentTypeMapping.put("description", List.of("description")); 616 contentTypeMapping.put("nature", List.of("type/code")); 617 contentTypeMapping.put("period", List.of("type/code")); // Pour année en tout cas... 618 contentTypeMapping.put("siseCode", ImmutableList.of("codeDiplomeSise")); 619 _mappingByContentType.put(ContainerFactory.CONTAINER_CONTENT_TYPE, contentTypeMapping); 620 621 // CourseList 622 contentTypeMapping = new HashMap<>(); 623 contentTypeMapping.put("title", List.of("libelleLong", "libelle")); 624 contentTypeMapping.put("plageDeChoix", List.of("plageDeChoix")); 625 contentTypeMapping.put("obligatoire", List.of("obligatoire")); 626 contentTypeMapping.put("min", List.of("plageMin")); 627 contentTypeMapping.put("max", List.of("plageMax")); 628 _mappingByContentType.put(CourseListFactory.COURSE_LIST_CONTENT_TYPE, contentTypeMapping); 629 630 // Course 631 contentTypeMapping = new HashMap<>(); 632 contentTypeMapping.put("title", List.of("libelleLong", "libelle")); 633 contentTypeMapping.put("ects", List.of("ects")); 634 contentTypeMapping.put("description", List.of("description")); 635 contentTypeMapping.put("courseType", List.of("type/code")); 636 contentTypeMapping.put("siseCode", ImmutableList.of("codeDiplomeSise")); 637 _mappingByContentType.put(CourseFactory.COURSE_CONTENT_TYPE, contentTypeMapping); 638 } 639 640 @Override 641 protected void configureSearchModel() 642 { 643 // J'avais trouvé ces critères à une époque (et la pagination) mais tout semble avoir disparu. Mais je n'a pas pu les inventer... 644// _searchModelConfiguration.addCriterion("type", new I18nizableText("odf-sync", "PLUGINS_ODF_PEGASE_CRITERION_TYPE"), "string"); 645// _searchModelConfiguration.addCriterion("texte", new I18nizableText("odf-sync", "PLUGINS_ODF_PEGASE_CRITERION_TEXTE"), "string"); 646// _searchModelConfiguration.addCriterion("code", new I18nizableText("odf-sync", "PLUGINS_ODF_PEGASE_CRITERION_CODE"), "string"); 647// _searchModelConfiguration.addCriterion("libelle", new I18nizableText("odf-sync", "PLUGINS_ODF_PEGASE_CRITERION_LIBELLE"), "string"); 648// _searchModelConfiguration.addCriterion("mutualise", new I18nizableText("odf-sync", "PLUGINS_ODF_PEGASE_CRITERION_MUTUALISE"), "boolean", "edition.boolean-combobox"); 649// _searchModelConfiguration.addCriterion("codeNature", new I18nizableText("odf-sync", "PLUGINS_ODF_PEGASE_CRITERION_CODE_NATURE"), "string"); 650 651 for (String keptField : _searchFields) 652 { 653 _searchModelConfiguration.addColumn(keptField, new I18nizableText(keptField)); 654 } 655 } 656 657 /** 658 * Get the catalog for import. 659 * @return the catalog 660 */ 661 protected String getCatalog() 662 { 663 return Optional.of(getParameterValues()) 664 .map(params -> params.get("catalog")) 665 .map(String.class::cast) 666 .filter(StringUtils::isNotBlank) 667 .orElseGet(() -> _catalogsManager.getDefaultCatalogName()); 668 } 669 670 /** 671 * Get the Pegase base URL 672 * @return the Pegase base URL 673 */ 674 protected String getBaseUrl() 675 { 676 return (String) getParameterValues().get("baseUrl"); 677 } 678 679 /** 680 * Get the structure code from Pegase 681 * @return the structure code 682 */ 683 protected String getStructureCode() 684 { 685 return (String) getParameterValues().get("structureCode"); 686 } 687 688 /** 689 * Get the token to log to Pegase API 690 * @param httpClient Http client 691 * @return a valid token 692 * @throws IOException if an error occurs 693 */ 694 protected String getToken(CloseableHttpClient httpClient) throws IOException 695 { 696 String username = (String) getParameterValues().get("username"); 697 String password = (String) getParameterValues().get("password"); 698 String url = (String) getParameterValues().get("authUrl"); 699 700 List<NameValuePair> urlParams = new ArrayList<>(); 701 urlParams.add(new BasicNameValuePair("username", username)); 702 urlParams.add(new BasicNameValuePair("password", password)); 703 urlParams.add(new BasicNameValuePair("token", "true")); 704 705 // Prepare a request object 706 HttpPost postRequest = new HttpPost(url); 707 708 // HTTP parameters 709 postRequest.setEntity(new UrlEncodedFormEntity(urlParams, "UTF-8")); 710 postRequest.setHeader("Content-Type", "application/x-www-form-urlencoded"); 711 712 // Execute the request 713 return _executeHttpRequest(httpClient, postRequest); 714 } 715 716 private String _executeGetRequest(CloseableHttpClient httpClient, String url, Map<String, String> urlParameters, String token) throws IOException 717 { 718 String completeUrl = URIUtils.encodeURI(getBaseUrl() + "/etablissements/" + getStructureCode() + url, urlParameters); 719 720 // Prepare a request object 721 HttpGet getRequest = new HttpGet(completeUrl); 722 723 // HTTP parameters 724 getRequest.setHeader("Authorization", "Bearer " + token); 725 726 // Execute the request 727 return _executeHttpRequest(httpClient, getRequest); 728 } 729 730 private String _executeHttpRequest(CloseableHttpClient httpClient, HttpUriRequest httpRequest) throws IOException 731 { 732 httpRequest.setHeader("accept", "application/json"); 733 734 // Execute the request 735 try (CloseableHttpResponse httpResponse = httpClient.execute(httpRequest)) 736 { 737 StatusLine statusLine = httpResponse.getStatusLine(); 738 if (statusLine.getStatusCode() / 100 != 2) 739 { 740 throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase()); 741 } 742 743 HttpEntity entity = httpResponse.getEntity(); 744 if (entity == null) 745 { 746 throw new IOException("The response entity is empty."); 747 } 748 749 try (InputStream is = entity.getContent()) 750 { 751 return IOUtils.toString(is, StandardCharsets.UTF_8); 752 } 753 } 754 } 755 756 private CloseableHttpClient _getHttpClient() 757 { 758 RequestConfig requestConfig = RequestConfig.custom().build(); 759 return HttpClientBuilder.create() 760 .setDefaultRequestConfig(requestConfig) 761 .useSystemProperties() 762 .build(); 763 } 764 765 /** 766 * Get or create the content defined by the given parameters. 767 * @param lang The content language 768 * @param idValue The synchronization code 769 * @param remoteValues The remote values 770 * @param forceImport <code>true</code> to force import (only on single import or unlimited global synchronization) 771 * @param logger The logger 772 * @return the content 773 * @throws Exception if an error occurs 774 */ 775 protected ModifiableContent _getOrCreateContent(String lang, String idValue, Map<String, List<Object>> remoteValues, boolean forceImport, Logger logger) throws Exception 776 { 777 ContentWorkflowDescription wfDescription = (ContentWorkflowDescription) remoteValues.get("workflowDescription").get(0); 778 ModifiableContent content = _getContent(lang, idValue, wfDescription.getContentType()); 779 if (content == null && (forceImport || !synchronizeExistingContentsOnly())) 780 { 781 // Calculate contentTitle 782 String contentTitle = Optional.of(remoteValues) 783 .map(v -> v.get("title")) 784 .map(List::stream) 785 .orElseGet(Stream::empty) 786 .filter(String.class::isInstance) 787 .map(String.class::cast) 788 .filter(StringUtils::isNotEmpty) 789 .findFirst() 790 .orElse(idValue); 791 792 String contentName = FilterNameHelper.filterName(_contentPrefix + "-" + contentTitle + "-" + lang); 793 794 Map<String, Object> inputs = new HashMap<>(); 795 String catalog = getCatalog(); 796 if (catalog != null) 797 { 798 inputs.put(AbstractCreateODFContentFunction.CONTENT_CATALOG_KEY, catalog); 799 } 800 801 Map<String, Object> resultMap = _contentWorkflowHelper.createContent( 802 wfDescription.getWorkflowName(), 803 wfDescription.getInitialActionId(), 804 contentName, 805 contentTitle, 806 new String[] {wfDescription.getContentType()}, 807 null, 808 lang, 809 null, 810 null, 811 inputs); 812 813 if ((boolean) resultMap.getOrDefault("error", false)) 814 { 815 _nbError++; 816 } 817 818 content = (ModifiableContent) resultMap.get(Content.class.getName()); 819 820 if (content != null) 821 { 822 _sccHelper.updateSCCProperty(content, getId()); 823 824 // Set sync code 825 content.setValue(getIdField(), idValue); 826 827 content.saveChanges(); 828 _importedContents.put(content.getId(), wfDescription.getValidationActionId()); 829 _nbCreatedContents++; 830 } 831 } 832 return content; 833 } 834 835 /** 836 * Get the content from the synchronization code, the lang, the catalog and the content type. 837 * @param lang The lang 838 * @param syncCode The synchronization code 839 * @param contentType The content type 840 * @return the retrieved content 841 */ 842 protected ModifiableContent _getContent(String lang, String syncCode, String contentType) 843 { 844 List<Expression> expList = _getExpressionsList(lang, syncCode, contentType); 845 AndExpression andExp = new AndExpression(expList.toArray(new Expression[expList.size()])); 846 String xPathQuery = ContentQueryHelper.getContentXPathQuery(andExp); 847 848 AmetysObjectIterable<ModifiableContent> contents = _resolver.query(xPathQuery); 849 850 if (contents.getSize() > 0) 851 { 852 return contents.iterator().next(); 853 } 854 855 return null; 856 } 857 858 @Override 859 protected Optional<ModifiableContent> _importOrSynchronizeContent(String idValue, String lang, Map<String, List<Object>> remoteValues, boolean forceImport, Logger logger) 860 { 861 try 862 { 863 ModifiableContent content = _getOrCreateContent(lang, idValue, remoteValues, forceImport, logger); 864 if (content != null) 865 { 866 return Optional.of(_synchronizeContent(content, remoteValues, logger)); 867 } 868 } 869 catch (Exception e) 870 { 871 _nbError++; 872 logger.error("An error occurred while importing or synchronizing content", e); 873 } 874 875 return Optional.empty(); 876 } 877 878 @SuppressWarnings("unchecked") 879 @Override 880 protected ModifiableContent _synchronizeContent(ModifiableContent content, Map<String, List<Object>> remoteValues, Logger logger) throws Exception 881 { 882 super._synchronizeContent(content, remoteValues, logger); 883 // Add children to the list to handle later to add relations 884 if (remoteValues.containsKey("children")) 885 { 886 Set<String> children = _contentsChildren.computeIfAbsent(content.getId(), __ -> new LinkedHashSet<>()); 887 children.addAll((List<String>) (Object) remoteValues.get("children")); 888 } 889 return content; 890 } 891 892 @Override 893 protected boolean _fillContent(Map<String, List<Object>> remoteValues, ModifiableContent content, Map<String, Object> additionalParameters, boolean create, Logger logger) throws Exception 894 { 895 _synchronizedContents.add(content.getId()); 896 return super._fillContent(remoteValues, content, additionalParameters, create, logger); 897 } 898 899 @Override 900 public List<String> getLanguages() 901 { 902 return List.of(_odfLang); 903 } 904 905 @Override 906 protected List<Expression> _getExpressionsList(String lang, String idValue, String contentType) 907 { 908 List<Expression> expList = super._getExpressionsList(lang, idValue, contentType); 909 String catalog = getCatalog(); 910 if (catalog != null) 911 { 912 expList.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog)); 913 } 914 return expList; 915 } 916 917 @Override 918 protected Map<String, Map<String, List<Object>>> getTransformedRemoteValues(Map<String, Object> searchParameters, Logger logger) 919 { 920 searchParameters.put("import", true); 921 return super.getTransformedRemoteValues(searchParameters, logger); 922 } 923 924 @Override 925 protected Map<String, Object> _transformRemoteValuesCardinality(Map<String, List<Object>> remoteValues, String obsoleteContentTypeId) 926 { 927 String realContentTypeId = Optional.of(remoteValues) 928 .map(v -> v.get("workflowDescription")) 929 .map(l -> l.get(0)) 930 .map(ContentWorkflowDescription.class::cast) 931 .map(ContentWorkflowDescription::getContentType) 932 .orElse(null); 933 return super._transformRemoteValuesCardinality(remoteValues, realContentTypeId); 934 } 935}