001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.module; 029 030import org.opencms.file.CmsFile; 031import org.opencms.file.CmsObject; 032import org.opencms.file.CmsProject; 033import org.opencms.file.CmsProperty; 034import org.opencms.file.CmsPropertyDefinition; 035import org.opencms.file.CmsResource; 036import org.opencms.file.CmsResourceFilter; 037import org.opencms.file.CmsVfsResourceNotFoundException; 038import org.opencms.file.types.I_CmsResourceType; 039import org.opencms.importexport.CmsImportParameters; 040import org.opencms.importexport.CmsImportResourceDataReader; 041import org.opencms.importexport.CmsImportVersion10; 042import org.opencms.importexport.CmsImportVersion10.RelationData; 043import org.opencms.importexport.Messages; 044import org.opencms.lock.CmsLock; 045import org.opencms.main.CmsException; 046import org.opencms.main.CmsLog; 047import org.opencms.main.CmsShell; 048import org.opencms.main.OpenCms; 049import org.opencms.relations.CmsRelation; 050import org.opencms.relations.CmsRelationFilter; 051import org.opencms.relations.CmsRelationType; 052import org.opencms.relations.I_CmsLinkParseable; 053import org.opencms.report.I_CmsReport; 054import org.opencms.security.CmsAccessControlEntry; 055import org.opencms.util.CmsFileUtil; 056import org.opencms.util.CmsStringUtil; 057import org.opencms.util.CmsUUID; 058 059import java.io.ByteArrayOutputStream; 060import java.io.PrintStream; 061import java.util.ArrayList; 062import java.util.Arrays; 063import java.util.Collection; 064import java.util.Collections; 065import java.util.HashMap; 066import java.util.HashSet; 067import java.util.List; 068import java.util.Map; 069import java.util.Optional; 070import java.util.Set; 071import java.util.stream.Collectors; 072 073import org.apache.commons.logging.Log; 074 075import com.google.common.base.Objects; 076import com.google.common.collect.Sets; 077 078/** 079 * Class used for updating modules.<p> 080 * 081 * This class updates modules in a smarter way than simply deleting and importing them again: The resources in the import 082 * ZIP file are compared to the resources in the currently installed module and only makes changes when necessary. The reason 083 * for this is that deletions of resources can be slow in some very large OpenCms installations, and the classic way of updating modules 084 * (delete/import) can take a long time because of this. 085 */ 086public class CmsModuleUpdater { 087 088 /** The logger instance for this class. */ 089 private static final Log LOG = CmsLog.getLog(CmsModuleUpdater.class); 090 091 /** Structure ids of imported resources.*/ 092 private Set<CmsUUID> m_importIds = new HashSet<CmsUUID>(); 093 094 /** The module data read from the ZIP. */ 095 private CmsModuleImportData m_moduleData; 096 097 /** The report to write to. */ 098 private I_CmsReport m_report; 099 100 /** 101 * Creates a new instance.<p> 102 * 103 * @param moduleData the module import data 104 * @param report the report to write to 105 */ 106 public CmsModuleUpdater(CmsModuleImportData moduleData, I_CmsReport report) { 107 108 m_moduleData = moduleData; 109 m_report = report; 110 } 111 112 /** 113 * Checks whether the module resources and sites of the two module versions are suitable for updating.<p> 114 * 115 * @param installedModule the installed module 116 * @param newModule the module to import 117 * 118 * @return true if the module resources are compatible 119 */ 120 public static boolean checkCompatibleModuleResources(CmsModule installedModule, CmsModule newModule) { 121 122 if (!(installedModule.hasOnlySystemAndSharedResources() && newModule.hasOnlySystemAndSharedResources())) { 123 String oldSite = installedModule.getSite(); 124 String newSite = newModule.getSite(); 125 if (!((oldSite != null) && (newSite != null) && CmsStringUtil.comparePaths(oldSite, newSite))) { 126 return false; 127 } 128 129 } 130 for (String oldModRes : installedModule.getResources()) { 131 for (String newModRes : newModule.getResources()) { 132 if (CmsStringUtil.isProperPrefixPath(oldModRes, newModRes)) { 133 return false; 134 } 135 } 136 } 137 return true; 138 139 } 140 141 /** 142 * Tries to create a new updater instance.<p> 143 * 144 * If the module is deemed non-updatable, an empty result is returned.<p> 145 * 146 * @param cms the current CMS context 147 * @param importFile the import file path 148 * @param report the report to write to 149 * @return an optional module updater 150 * 151 * @throws CmsException if something goes wrong 152 */ 153 public static Optional<CmsModuleUpdater> create(CmsObject cms, String importFile, I_CmsReport report) 154 throws CmsException { 155 156 CmsModuleImportData moduleData = readModuleData(cms, importFile, report); 157 if (moduleData.checkUpdatable(cms)) { 158 return Optional.of(new CmsModuleUpdater(moduleData, report)); 159 } else { 160 return Optional.empty(); 161 } 162 } 163 164 /** 165 * Check if a resource needs to be updated because of its direct fields.<p> 166 * 167 * @param existingRes the existing resource 168 * @param newRes the new resource 169 * @param reduced true if we are in reduced export mode 170 * 171 * @return true if we need to update the resource based on its direct fields 172 */ 173 public static boolean needToUpdateResourceFields(CmsResource existingRes, CmsResource newRes, boolean reduced) { 174 175 boolean result = false; 176 result |= existingRes.getTypeId() != newRes.getTypeId(); 177 result |= differentDates(existingRes.getDateCreated(), newRes.getDateCreated()); // Export format date is not precise to millisecond 178 result |= differentDates(existingRes.getDateReleased(), newRes.getDateReleased()); 179 result |= differentDates(existingRes.getDateExpired(), newRes.getDateExpired()); 180 result |= existingRes.getFlags() != newRes.getFlags(); 181 if (!reduced) { 182 result |= !Objects.equal(existingRes.getUserCreated(), newRes.getUserCreated()); 183 result |= !Objects.equal(existingRes.getUserLastModified(), newRes.getUserLastModified()); 184 result |= existingRes.getDateLastModified() != newRes.getDateLastModified(); 185 } 186 return result; 187 } 188 189 /** 190 * Normalizes the path.<p> 191 * 192 * @param pathComponents the path components 193 * 194 * @return the normalized path 195 */ 196 public static String normalizePath(String... pathComponents) { 197 198 return CmsFileUtil.removeTrailingSeparator(CmsStringUtil.joinPaths(pathComponents)); 199 } 200 201 /** 202 * Reads the module data from an import zip file.<p> 203 * 204 * @param cms the CMS context 205 * @param importFile the import file 206 * @param report the report to write to 207 * @return the module data 208 * @throws CmsException if something goes wrong 209 */ 210 public static CmsModuleImportData readModuleData(CmsObject cms, String importFile, I_CmsReport report) 211 throws CmsException { 212 213 CmsModuleImportData result = new CmsModuleImportData(); 214 CmsModule module = CmsModuleImportExportHandler.readModuleFromImport(importFile); 215 cms = OpenCms.initCmsObject(cms); 216 217 String importSite = module.getImportSite(); 218 if (!CmsStringUtil.isEmptyOrWhitespaceOnly(importSite)) { 219 cms.getRequestContext().setSiteRoot(importSite); 220 } else { 221 String siteToSet = cms.getRequestContext().getSiteRoot(); 222 if ("".equals(siteToSet)) { 223 siteToSet = "/"; 224 } 225 module.setSite(siteToSet); 226 } 227 result.setModule(module); 228 result.setCms(cms); 229 CmsImportResourceDataReader importer = new CmsImportResourceDataReader(result); 230 CmsImportParameters params = new CmsImportParameters(importFile, "/", false); 231 importer.importData(cms, report, params); // This only reads the module data into Java objects 232 return result; 233 234 } 235 236 /** 237 * Checks that two longs representing dates differ by more than 1000 (milliseconds).<p> 238 * 239 * @param d1 the first date 240 * @param d2 the second date 241 * 242 * @return true if the dates differ by more than 1000 milliseconds 243 */ 244 static boolean differentDates(long d1, long d2) { 245 246 return 1000 < Math.abs(d2 - d1); 247 } 248 249 /** 250 * Gets all resources in the module.<p> 251 * 252 * @param cms the current CMS context 253 * @param module the module 254 * @return the resources in the module 255 * @throws CmsException if something goes wrong 256 */ 257 private static Set<CmsResource> getAllResourcesInModule(CmsObject cms, CmsModule module) throws CmsException { 258 259 Set<CmsResource> result = new HashSet<>(); 260 for (CmsResource resource : CmsModule.calculateModuleResources(cms, module)) { 261 result.add(resource); 262 if (resource.isFolder()) { 263 result.addAll(cms.readResources(resource, CmsResourceFilter.ALL, true)); 264 } 265 } 266 return result; 267 } 268 269 /** 270 * Update relations for all imported resources.<p> 271 * 272 * @param cms the current CMS context 273 * @throws CmsException if something goes wrong 274 */ 275 public void importRelations(CmsObject cms) throws CmsException { 276 277 for (CmsResourceImportData resData : m_moduleData.getResourceData()) { 278 if (!resData.getRelations().isEmpty()) { 279 CmsResource importResource = resData.getImportResource(); 280 if (importResource != null) { 281 importResource = cms.readResource(importResource.getStructureId(), CmsResourceFilter.ALL); 282 updateRelations(cms, importResource, resData.getRelations()); 283 } 284 } 285 } 286 287 } 288 289 /** 290 * Performs the module update.<p> 291 */ 292 public void run() { 293 294 try { 295 CmsObject cms = m_moduleData.getCms(); 296 CmsModule module = m_moduleData.getModule(); 297 CmsModule oldModule = OpenCms.getModuleManager().getModule(module.getName()); 298 Map<CmsUUID, CmsUUID> conflictingIds = m_moduleData.getConflictingIds(); 299 if (!conflictingIds.isEmpty()) { 300 deleteConflictingResources(cms, module, conflictingIds); 301 } 302 CmsProject importProject = createAndSetModuleImportProject(cms, module); 303 CmsModuleImportExportHandler.reportBeginImport(m_report, module.getName()); 304 305 Map<CmsUUID, CmsResourceImportData> importResourcesById = new HashMap<>(); 306 for (CmsResourceImportData resData : m_moduleData.getResourceData()) { 307 importResourcesById.put(resData.getResource().getStructureId(), resData); 308 } 309 Set<CmsResource> oldModuleResources = getAllResourcesInModule(cms, oldModule); 310 List<CmsResource> toDelete = new ArrayList<>(); 311 Set<String> immutables = OpenCms.getImportExportManager().getImmutableResources().stream().flatMap( 312 path -> Arrays.asList( 313 CmsFileUtil.removeTrailingSeparator(path), 314 CmsFileUtil.addTrailingSeparator(path)).stream()).collect(Collectors.toSet()); 315 for (CmsResource oldRes : oldModuleResources) { 316 if (immutables.contains(oldRes.getRootPath())) { 317 continue; 318 } 319 CmsResourceImportData newRes = importResourcesById.get(oldRes.getStructureId()); 320 if (newRes == null) { 321 toDelete.add(oldRes); 322 } 323 } 324 int index = 0; 325 for (CmsResourceImportData resData1 : m_moduleData.getResourceData()) { 326 index += 1; 327 processImportResource(cms, resData1, index); 328 } 329 processDeletions(cms, toDelete); 330 parseLinks(cms); 331 332 importRelations(cms); 333 if (!CmsStringUtil.isEmptyOrWhitespaceOnly(module.getImportScript())) { 334 runImportScript(cms, module); 335 } 336 337 OpenCms.getModuleManager().updateModule(cms, module); 338 module.setCheckpointTime(System.currentTimeMillis()); 339 // reinitialize the resource manager with additional module resource types if necessary 340 if (module.getResourceTypes() != Collections.EMPTY_LIST) { 341 OpenCms.getResourceManager().initialize(cms); 342 } 343 // reinitialize the workplace manager with additional module explorer types if necessary 344 if (module.getExplorerTypes() != Collections.EMPTY_LIST) { 345 OpenCms.getWorkplaceManager().addExplorerTypeSettings(module); 346 } 347 for (CmsResourceImportData resData : m_moduleData.getResourceData()) { 348 if (m_importIds.contains(resData.getResource().getStructureId()) 349 && !OpenCms.getResourceManager().matchResourceType( 350 resData.getTypeName(), 351 resData.getResource().getTypeId())) { 352 if (OpenCms.getResourceManager().hasResourceType(resData.getTypeName())) { 353 try { 354 CmsResource res = cms.readResource(resData.getResource().getStructureId()); 355 cms.chtype(res, OpenCms.getResourceManager().getResourceType(resData.getTypeName())); 356 } catch (Exception e) { 357 m_report.println(e); 358 } 359 } 360 } 361 } 362 cms.unlockProject(importProject.getUuid()); 363 OpenCms.getPublishManager().publishProject(cms, m_report); 364 OpenCms.getPublishManager().waitWhileRunning(); 365 CmsModuleImportExportHandler.reportEndImport(m_report); 366 } catch (Exception e) { 367 m_report.println(e); 368 } finally { 369 cleanUp(); 370 } 371 } 372 373 /** 374 * Updates the access control list fr a resource.<p> 375 * 376 * @param cms the current cms context 377 * @param resData the resource data 378 * @param resource the existing resource 379 * @return the resource 380 * 381 * @throws CmsException if something goes wrong 382 */ 383 public boolean updateAcls(CmsObject cms, CmsResourceImportData resData, CmsResource resource) throws CmsException { 384 385 boolean changed = false; 386 Map<CmsUUID, CmsAccessControlEntry> importAces = buildAceMap(resData.getAccessControlEntries()); 387 388 String path = cms.getSitePath(resource); 389 List<CmsAccessControlEntry> existingAcl = cms.getAccessControlEntries(path, false); 390 Map<CmsUUID, CmsAccessControlEntry> existingAces = buildAceMap(existingAcl); 391 Set<CmsUUID> keys = new HashSet<>(existingAces.keySet()); 392 keys.addAll(importAces.keySet()); 393 for (CmsUUID key : keys) { 394 CmsAccessControlEntry existingEntry = existingAces.get(key); 395 CmsAccessControlEntry newEntry = importAces.get(key); 396 if ((existingEntry == null) 397 || (newEntry == null) 398 || !existingEntry.withNulledResource().equals(newEntry.withNulledResource())) { 399 cms.importAccessControlEntries(resource, resData.getAccessControlEntries()); 400 changed = true; 401 break; 402 } 403 } 404 return changed; 405 } 406 407 /** 408 * Creates the project used to import module resources and sets it on the CmsObject. 409 * 410 * @param cms the CmsObject to set the project on 411 * @param module the module 412 * @return the created project 413 * @throws CmsException if something goes wrong 414 */ 415 protected CmsProject createAndSetModuleImportProject(CmsObject cms, CmsModule module) throws CmsException { 416 417 CmsProject importProject = cms.createProject( 418 org.opencms.module.Messages.get().getBundle(cms.getRequestContext().getLocale()).key( 419 org.opencms.module.Messages.GUI_IMPORT_MODULE_PROJECT_NAME_1, 420 new Object[] {module.getName()}), 421 org.opencms.module.Messages.get().getBundle(cms.getRequestContext().getLocale()).key( 422 org.opencms.module.Messages.GUI_IMPORT_MODULE_PROJECT_DESC_1, 423 new Object[] {module.getName()}), 424 OpenCms.getDefaultUsers().getGroupAdministrators(), 425 OpenCms.getDefaultUsers().getGroupAdministrators(), 426 CmsProject.PROJECT_TYPE_TEMPORARY); 427 cms.getRequestContext().setCurrentProject(importProject); 428 cms.copyResourceToProject("/"); 429 return importProject; 430 } 431 432 /** 433 * Deletes and publishes resources with ID conflicts. 434 * 435 * @param cms the CMS context to use 436 * @param module the module 437 * @param conflictingIds the conflicting ids 438 * @throws CmsException if something goes wrong 439 * @throws Exception if something goes wrong 440 */ 441 protected void deleteConflictingResources(CmsObject cms, CmsModule module, Map<CmsUUID, CmsUUID> conflictingIds) 442 throws CmsException, Exception { 443 444 CmsProject conflictProject = cms.createProject( 445 "Deletion of conflicting resources for " + module.getName(), 446 "Deletion of conflicting resources for " + module.getName(), 447 OpenCms.getDefaultUsers().getGroupAdministrators(), 448 OpenCms.getDefaultUsers().getGroupAdministrators(), 449 CmsProject.PROJECT_TYPE_TEMPORARY); 450 CmsObject deleteCms = OpenCms.initCmsObject(cms); 451 deleteCms.getRequestContext().setCurrentProject(conflictProject); 452 for (CmsUUID vfsId : conflictingIds.values()) { 453 CmsResource toDelete = deleteCms.readResource(vfsId, CmsResourceFilter.ALL); 454 lock(deleteCms, toDelete); 455 deleteCms.deleteResource(toDelete, CmsResource.DELETE_PRESERVE_SIBLINGS); 456 } 457 OpenCms.getPublishManager().publishProject(deleteCms); 458 OpenCms.getPublishManager().waitWhileRunning(); 459 } 460 461 /** 462 * Parses links for XMLContents etc. 463 * 464 * @param cms the CMS context to use 465 * @throws CmsException if something goes wrong 466 */ 467 protected void parseLinks(CmsObject cms) throws CmsException { 468 469 List<CmsResource> linkParseables = new ArrayList<>(); 470 for (CmsResourceImportData resData : m_moduleData.getResourceData()) { 471 CmsResource importRes = resData.getImportResource(); 472 if ((importRes != null) && m_importIds.contains(importRes.getStructureId()) && isLinkParsable(importRes)) { 473 linkParseables.add(importRes); 474 } 475 } 476 m_report.println(Messages.get().container(Messages.RPT_START_PARSE_LINKS_0), I_CmsReport.FORMAT_HEADLINE); 477 CmsImportVersion10.parseLinks(cms, linkParseables, m_report); 478 m_report.println(Messages.get().container(Messages.RPT_END_PARSE_LINKS_0), I_CmsReport.FORMAT_HEADLINE); 479 } 480 481 /** 482 * Handles the file deletions. 483 * 484 * @param cms the CMS context to use 485 * @param toDelete the resources to delete 486 * 487 * @throws CmsException if something goes wrong 488 */ 489 protected void processDeletions(CmsObject cms, List<CmsResource> toDelete) throws CmsException { 490 491 Collections.sort(toDelete, (a, b) -> b.getRootPath().compareTo(a.getRootPath())); 492 for (CmsResource deleteRes : toDelete) { 493 m_report.print( 494 org.opencms.importexport.Messages.get().container(org.opencms.importexport.Messages.RPT_DELFOLDER_0), 495 I_CmsReport.FORMAT_NOTE); 496 m_report.print( 497 org.opencms.report.Messages.get().container( 498 org.opencms.report.Messages.RPT_ARGUMENT_1, 499 deleteRes.getRootPath())); 500 CmsLock lock = cms.getLock(deleteRes); 501 if (lock.isUnlocked()) { 502 lock(cms, deleteRes); 503 } 504 cms.deleteResource(deleteRes, CmsResource.DELETE_PRESERVE_SIBLINGS); 505 m_report.println( 506 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0), 507 I_CmsReport.FORMAT_OK); 508 509 } 510 } 511 512 /** 513 * Processes single resource from module import data. 514 * 515 * @param cms the CMS context to use 516 * @param resData the resource data from the module import 517 * @param index index of the current import resource 518 */ 519 protected void processImportResource(CmsObject cms, CmsResourceImportData resData, int index) { 520 521 boolean changed = false; 522 m_report.print( 523 org.opencms.report.Messages.get().container( 524 org.opencms.report.Messages.RPT_ARGUMENT_1, 525 "( " + index + " / " + m_moduleData.getResourceData().size() + " ) "), 526 I_CmsReport.FORMAT_NOTE); 527 m_report.print(Messages.get().container(Messages.RPT_IMPORTING_0), I_CmsReport.FORMAT_NOTE); 528 m_report.print( 529 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_ARGUMENT_1, resData.getPath())); 530 m_report.print(org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_DOTS_0)); 531 try { 532 CmsResource oldRes = null; 533 try { 534 if (resData.hasStructureId()) { 535 oldRes = cms.readResource( 536 resData.getResource().getStructureId(), 537 CmsResourceFilter.IGNORE_EXPIRATION); 538 } else { 539 oldRes = cms.readResource(resData.getPath(), CmsResourceFilter.IGNORE_EXPIRATION); 540 } 541 } catch (CmsVfsResourceNotFoundException e) { 542 LOG.debug(e.getLocalizedMessage(), e); 543 } 544 CmsResource currentRes = oldRes; 545 if (oldRes != null) { 546 String oldPath = cms.getSitePath(oldRes); 547 String newPath = resData.getPath(); 548 if (!CmsStringUtil.comparePaths(oldPath, resData.getPath())) { 549 cms.moveResource(oldPath, newPath); 550 changed = true; 551 currentRes = cms.readResource(oldRes.getStructureId(), CmsResourceFilter.IGNORE_EXPIRATION); 552 } 553 } 554 boolean needsImport = true; 555 boolean reducedExport = !resData.hasDateLastModified(); 556 byte[] content = resData.getContent(); 557 if (oldRes != null) { 558 if (!resData.hasStructureId()) { 559 needsImport = false; 560 } else if (oldRes.getState().isUnchanged() 561 && !needToUpdateResourceFields(oldRes, resData.getResource(), reducedExport)) { 562 563 // if resource is changed or new, we don't want to go into this code block 564 // because even if the content / metaadata are the same, we still want the file to be published at the end, 565 // so we import it to add it to the current working project 566 567 if (oldRes.isFile() && (content != null)) { 568 CmsFile file = cms.readFile(oldRes); 569 if (Arrays.equals(file.getContents(), content)) { 570 needsImport = false; 571 } else { 572 LOG.debug("Content mismatch for " + file.getRootPath()); 573 } 574 } else { 575 needsImport = false; 576 } 577 } 578 } 579 if (needsImport || (oldRes == null)) { // oldRes null check is redundant, we just do it to remove the warning in Eclipse 580 currentRes = cms.importResource( 581 resData.getPath(), 582 m_report, 583 resData.getResource(), 584 content, 585 new ArrayList<CmsProperty>()); 586 changed = true; 587 m_importIds.add(currentRes.getStructureId()); 588 } else { 589 currentRes = cms.readResource(oldRes.getStructureId(), CmsResourceFilter.ALL); 590 CmsLock lock = cms.getLock(currentRes); 591 if (lock.isUnlocked()) { 592 lock(cms, currentRes); 593 } 594 } 595 resData.setImportResource(currentRes); 596 List<CmsProperty> propsToWrite = compareProperties(cms, resData, currentRes); 597 if (!propsToWrite.isEmpty()) { 598 cms.writePropertyObjects(currentRes, propsToWrite); 599 changed = true; 600 } 601 changed |= updateAcls(cms, resData, currentRes); 602 if (changed) { 603 m_report.println( 604 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0), 605 I_CmsReport.FORMAT_OK); 606 } else { 607 m_report.println( 608 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_SKIPPED_0), 609 I_CmsReport.FORMAT_NOTE); 610 } 611 612 } catch (Exception e) { 613 m_report.println(e); 614 LOG.error(e.getLocalizedMessage(), e); 615 616 } 617 } 618 619 /** 620 * Runs the module import script. 621 * 622 * @param cms the CMS context to use 623 * @param module the module for which to run the script 624 */ 625 protected void runImportScript(CmsObject cms, CmsModule module) { 626 627 LOG.info("Executing import script for module " + module.getName()); 628 m_report.println( 629 org.opencms.module.Messages.get().container(org.opencms.module.Messages.RPT_IMPORT_SCRIPT_HEADER_0), 630 I_CmsReport.FORMAT_HEADLINE); 631 String importScript = "echo on\n" + module.getImportScript(); 632 ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 633 PrintStream out = new PrintStream(buffer); 634 CmsShell shell = new CmsShell(cms, "${user}@${project}:${siteroot}|${uri}>", null, out, out); 635 shell.execute(importScript); 636 String outputString = buffer.toString(); 637 LOG.info("Shell output for import script was: \n" + outputString); 638 m_report.println( 639 org.opencms.module.Messages.get().container( 640 org.opencms.module.Messages.RPT_IMPORT_SCRIPT_OUTPUT_1, 641 outputString)); 642 } 643 644 /** 645 * Converts access control list to map form, with principal ids as keys.<p> 646 * 647 * @param acl an access control list 648 * @return the map with the access control entries 649 */ 650 Map<CmsUUID, CmsAccessControlEntry> buildAceMap(Collection<CmsAccessControlEntry> acl) { 651 652 if (acl == null) { 653 acl = new ArrayList<>(); 654 } 655 Map<CmsUUID, CmsAccessControlEntry> result = new HashMap<>(); 656 for (CmsAccessControlEntry ace : acl) { 657 result.put(ace.getPrincipal(), ace); 658 } 659 return result; 660 } 661 662 /** 663 * Cleans up temp files. 664 */ 665 private void cleanUp() { 666 667 for (CmsResourceImportData resData : m_moduleData.getResourceData()) { 668 resData.cleanUp(); 669 } 670 } 671 672 /** 673 * Compares properties of an existing resource with those to be imported, and returns a list of properties that need to be updated.<p> 674 * 675 * @param cms the current CMS context 676 * @param resData the resource import data 677 * @param existingResource the existing resource 678 * @return the list of properties that need to be updated 679 * 680 * @throws CmsException if something goes wrong 681 */ 682 private List<CmsProperty> compareProperties( 683 CmsObject cms, 684 CmsResourceImportData resData, 685 CmsResource existingResource) 686 throws CmsException { 687 688 if (existingResource == null) { 689 return Collections.emptyList(); 690 } 691 692 Map<String, CmsProperty> importProps = resData.getProperties(); 693 Map<String, CmsProperty> existingProps = CmsProperty.getPropertyMap( 694 cms.readPropertyObjects(existingResource, false)); 695 Map<String, CmsProperty> propsToWrite = new HashMap<>(); 696 Set<String> keys = new HashSet<>(); 697 keys.addAll(existingProps.keySet()); 698 keys.addAll(importProps.keySet()); 699 700 for (String key : keys) { 701 if (existingResource.isFile() && CmsPropertyDefinition.PROPERTY_IMAGE_SIZE.equals(key)) { 702 // Depending on the configuration of the image loader, an image is potentially resized when importing/creating it, 703 // and the image.size property is set to the size of the resized image. However, the property value in the import may 704 // be from a system with different image loader settings, and thus may not correspond to the actual size of the image 705 // in the current system anymore, leading to problems with image scaling later. 706 // 707 // To prevent this state, we skip setting the image.size property for module updates. 708 continue; 709 } 710 CmsProperty existingProp = existingProps.get(key); 711 CmsProperty importProp = importProps.get(key); 712 if (existingProp == null) { 713 propsToWrite.put(key, importProp); 714 } else if (importProp == null) { 715 propsToWrite.put(key, new CmsProperty(key, "", "")); 716 } else if (!existingProp.isIdentical(importProp)) { 717 propsToWrite.put(key, importProp); 718 } 719 } 720 return new ArrayList<>(propsToWrite.values()); 721 722 } 723 724 /** 725 * Checks if a resource is link parseable.<P> 726 * 727 * @param importRes the resource to check 728 * @return true if the resource is link parseable 729 * 730 * @throws CmsException if something goes wrong 731 */ 732 private boolean isLinkParsable(CmsResource importRes) throws CmsException { 733 734 int typeId = importRes.getTypeId(); 735 I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(typeId); 736 return type instanceof I_CmsLinkParseable; 737 738 } 739 740 /** 741 * Locks a resource, or steals the lock if it's already locked.<p> 742 * 743 * @param cms the CMS context 744 * @param resource the resource to lock 745 * @throws CmsException if something goes wrong 746 */ 747 private void lock(CmsObject cms, CmsResource resource) throws CmsException { 748 749 CmsLock lock = cms.getLock(resource); 750 if (lock.isUnlocked()) { 751 cms.lockResourceTemporary(resource); 752 } else { 753 cms.changeLock(resource); 754 } 755 } 756 757 /** 758 * Compares list of existing relations with list of relations to import and returns true if they are different. 759 * 760 * @param noContentRelations the existing relations which are not in-content relations 761 * @param newRelations the relations to import 762 * 763 * @return true if the relations need to be updated 764 */ 765 private boolean needToUpdateRelations(List<CmsRelation> noContentRelations, Set<CmsRelation> newRelations) { 766 767 if (noContentRelations.size() != newRelations.size()) { 768 return true; 769 } 770 771 for (CmsRelation relation : noContentRelations) { 772 if (!(newRelations.contains(relation) || newRelations.contains(relation.withTargetId(null)))) { 773 return true; 774 } 775 } 776 777 return false; 778 } 779 780 /** 781 * Compares the relation (not defined in content) for a resource with those to be imported, and makes 782 * the necessary modifications. 783 * 784 * @param cms the CMS context 785 * @param importResource the resource 786 * @param relations the relations to be imported 787 * 788 * @throws CmsException if something goes wrong 789 */ 790 private void updateRelations(CmsObject cms, CmsResource importResource, List<RelationData> relations) 791 throws CmsException { 792 793 Map<String, CmsRelationType> relTypes = new HashMap<>(); 794 for (CmsRelationType relType : OpenCms.getResourceManager().getRelationTypes()) { 795 relTypes.put(relType.getName(), relType); 796 } 797 Set<CmsRelation> existingRelations = Sets.newHashSet( 798 cms.readRelations(CmsRelationFilter.relationsFromStructureId(importResource.getStructureId()))); 799 List<CmsRelation> noContentRelations = existingRelations.stream().filter( 800 rel -> !rel.getType().isDefinedInContent()).collect(Collectors.toList()); 801 Set<CmsRelation> newRelations = new HashSet<>(); 802 for (RelationData rel : relations) { 803 if (!rel.getType().isDefinedInContent()) { 804 newRelations.add( 805 new CmsRelation( 806 importResource.getStructureId(), 807 importResource.getRootPath(), 808 rel.getTargetId(), 809 rel.getTarget(), 810 rel.getType())); 811 } 812 } 813 814 if (needToUpdateRelations(noContentRelations, newRelations)) { 815 816 CmsRelationFilter relFilter = CmsRelationFilter.TARGETS.filterNotDefinedInContent(); 817 try { 818 cms.deleteRelationsFromResource(importResource, relFilter); 819 } catch (CmsException e) { 820 LOG.error(e.getLocalizedMessage(), e); 821 m_report.println(e); 822 } 823 824 for (CmsRelation newRel : newRelations) { 825 try { 826 CmsResource targetResource; 827 if (newRel.getTargetId() != null) { 828 targetResource = cms.readResource(newRel.getTargetId(), CmsResourceFilter.IGNORE_EXPIRATION); 829 } else { 830 try (AutoCloseable ac = cms.tempChangeSiteRoot("")) { 831 targetResource = cms.readResource( 832 newRel.getTargetPath(), 833 CmsResourceFilter.IGNORE_EXPIRATION); 834 } 835 } 836 if (targetResource != null) { 837 cms.addRelationToResource(importResource, targetResource, newRel.getType().getName()); 838 } 839 } catch (Exception e) { 840 LOG.error(e.getLocalizedMessage(), e); 841 m_report.println(e); 842 } 843 } 844 } 845 } 846 847}