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.CmsObject; 031import org.opencms.file.CmsProject; 032import org.opencms.file.CmsResource; 033import org.opencms.file.CmsResourceFilter; 034import org.opencms.file.CmsVfsResourceNotFoundException; 035import org.opencms.importexport.CmsImportExportException; 036import org.opencms.main.CmsException; 037import org.opencms.main.CmsLog; 038import org.opencms.main.OpenCms; 039import org.opencms.module.CmsModuleLog.Action; 040import org.opencms.report.CmsLogReport; 041import org.opencms.report.I_CmsReport; 042import org.opencms.util.CmsFileUtil; 043import org.opencms.util.CmsStringUtil; 044 045import java.io.File; 046import java.io.FileInputStream; 047import java.io.FileOutputStream; 048import java.io.IOException; 049import java.util.Arrays; 050import java.util.Collections; 051import java.util.List; 052import java.util.Locale; 053import java.util.Map; 054import java.util.Set; 055import java.util.concurrent.ConcurrentHashMap; 056import java.util.concurrent.TimeUnit; 057 058import org.apache.commons.lang3.RandomStringUtils; 059import org.apache.commons.logging.Log; 060 061import com.google.common.base.Objects; 062import com.google.common.cache.CacheBuilder; 063import com.google.common.collect.Lists; 064import com.google.common.collect.Sets; 065 066/** 067 * Class which manages import/export of modules from repositories configured in opencms-importexport.xml.<p> 068 */ 069public class CmsModuleImportExportRepository { 070 071 /** Export folder path. */ 072 public static final String EXPORT_FOLDER_PATH = "packages/_export"; 073 074 /** Import folder path. */ 075 public static final String IMPORT_FOLDER_PATH = "packages/_import"; 076 077 /** Suffix for module zip files. */ 078 public static final String SUFFIX = ".zip"; 079 080 /** The log instance for this class. */ 081 private static final Log LOG = CmsLog.getLog(CmsModuleImportExportRepository.class); 082 083 /** The admin CMS context. */ 084 private CmsObject m_adminCms; 085 086 /** Cache for module hashes, used to detect changes in modules. */ 087 private Map<CmsModule, String> m_moduleHashCache = new ConcurrentHashMap<CmsModule, String>(); 088 089 /** Module log. */ 090 private CmsModuleLog m_moduleLog = new CmsModuleLog(); 091 092 /** Timed cache for newly calculated module hashes, used to avoid very frequent recalculation. */ 093 private Map<CmsModule, String> m_newModuleHashCache = CacheBuilder.newBuilder().expireAfterWrite( 094 3, 095 TimeUnit.SECONDS).<CmsModule, String> build().asMap(); 096 097 /** 098 * Creates a new instance.<p> 099 */ 100 public CmsModuleImportExportRepository() { 101 102 } 103 104 /** 105 * Deletes the module corresponding to the given virtual module file name.<p> 106 * 107 * @param fileName the file name 108 * @return true if the module could be deleted 109 * 110 * @throws CmsException if something goes wrong 111 */ 112 public synchronized boolean deleteModule(String fileName) throws CmsException { 113 114 String moduleName = null; 115 boolean ok = true; 116 try { 117 CmsModule module = getModuleForFileName(fileName); 118 if (module == null) { 119 LOG.error("Deletion request for invalid module file name: " + fileName); 120 ok = false; 121 return false; 122 } 123 I_CmsReport report = createReport(); 124 moduleName = module.getName(); 125 OpenCms.getModuleManager().deleteModule(m_adminCms, module.getName(), false, report); 126 ok = !(report.hasWarning() || report.hasError()); 127 return true; 128 } catch (Exception e) { 129 ok = false; 130 if (e instanceof CmsException) { 131 throw (CmsException)e; 132 } 133 if (e instanceof RuntimeException) { 134 throw (RuntimeException)e; 135 } 136 return true; 137 } finally { 138 m_moduleLog.log(moduleName, Action.deleteModule, ok); 139 } 140 141 } 142 143 /** 144 * Exports a module and returns the export zip file content in a byte array.<p> 145 * 146 * @param virtualModuleFileName the virtual file name for the module 147 * @param project the project from which the module should be exported 148 * 149 * @return the module export data 150 * 151 * @throws CmsException if something goes wrong 152 */ 153 @SuppressWarnings("resource") 154 public synchronized byte[] getExportedModuleData(String virtualModuleFileName, CmsProject project) 155 throws CmsException { 156 157 CmsModule module = getModuleForFileName(virtualModuleFileName); 158 if (module == null) { 159 LOG.warn("Invalid module export path requested: " + virtualModuleFileName); 160 return null; 161 } 162 try { 163 String moduleName = module.getName(); 164 ensureFoldersExist(); 165 166 String moduleFilePath = OpenCms.getSystemInfo().getAbsoluteRfsPathRelativeToWebInf( 167 CmsStringUtil.joinPaths(EXPORT_FOLDER_PATH, moduleName + ".zip")); 168 File moduleFile = new File(moduleFilePath); 169 170 boolean needToRunExport = needToExportModule(module, moduleFile, project); 171 if (needToRunExport) { 172 CmsObject exportCms = OpenCms.initCmsObject(m_adminCms); 173 exportCms.getRequestContext().setCurrentProject(project); 174 LOG.info("Module export is needed for " + module.getName()); 175 moduleFile.delete(); 176 CmsModuleImportExportHandler handler = new CmsModuleImportExportHandler(); 177 List<String> moduleResources = CmsModule.calculateModuleResourceNames(exportCms, module); 178 handler.setAdditionalResources(moduleResources.toArray(new String[] {})); 179 // the import/export handler adds the zip extension if it is not there, so we append it here 180 String tempFileName = RandomStringUtils.randomAlphanumeric(8) + ".zip"; 181 String tempFilePath = OpenCms.getSystemInfo().getAbsoluteRfsPathRelativeToWebInf( 182 CmsStringUtil.joinPaths(EXPORT_FOLDER_PATH, tempFileName)); 183 handler.setFileName(tempFilePath); 184 handler.setModuleName(moduleName); 185 CmsException exportException = null; 186 I_CmsReport report = createReport(); 187 try { 188 handler.exportData(exportCms, report); 189 } catch (CmsException e) { 190 exportException = e; 191 } 192 boolean failed = ((exportException != null) || report.hasWarning() || report.hasError()); 193 m_moduleLog.log(moduleName, Action.exportModule, !failed); 194 195 if (exportException != null) { 196 new File(tempFilePath).delete(); 197 throw exportException; 198 } 199 new File(tempFilePath).renameTo(new File(moduleFilePath)); 200 LOG.info("Created module export " + moduleFilePath); 201 } 202 byte[] result = CmsFileUtil.readFully(new FileInputStream(moduleFilePath)); 203 return result; 204 } catch (IOException e) { 205 LOG.error(e.getLocalizedMessage(), e); 206 return null; 207 } 208 } 209 210 /** 211 * Gets the list of modules as file names.<p> 212 * 213 * @return the list of modules as file names 214 */ 215 public List<String> getModuleFileNames() { 216 217 List<String> result = Lists.newArrayList(); 218 for (CmsModule module : OpenCms.getModuleManager().getAllInstalledModules()) { 219 result.add(getFileNameForModule(module)); 220 } 221 return result; 222 } 223 224 /** 225 * Gets the object used to access the module log.<p> 226 * 227 * @return the module log 228 */ 229 public CmsModuleLog getModuleLog() { 230 231 return m_moduleLog; 232 } 233 234 /** 235 * Imports module data.<p> 236 * 237 * @param name the module file name 238 * @param content the module ZIP file data 239 * @throws CmsException if something goes wrong 240 */ 241 public synchronized void importModule(String name, byte[] content) throws CmsException { 242 243 String moduleName = null; 244 boolean ok = true; 245 try { 246 if (content.length == 0) { 247 // Happens when using CmsResourceWrapperModules with JLAN and createResource is called 248 LOG.debug("Zero-length module import content, ignoring it..."); 249 } else { 250 ensureFoldersExist(); 251 String targetFilePath = createImportZipPath(name); 252 try { 253 FileOutputStream out = new FileOutputStream(new File(targetFilePath)); 254 out.write(content); 255 out.close(); 256 } catch (IOException e) { 257 throw new CmsImportExportException( 258 Messages.get().container(Messages.ERR_FILE_IO_1, targetFilePath)); 259 } 260 CmsModule module = CmsModuleImportExportHandler.readModuleFromImport(targetFilePath); 261 moduleName = module.getName(); 262 I_CmsReport report = createReport(); 263 OpenCms.getModuleManager().replaceModule(m_adminCms, targetFilePath, report); 264 new File(targetFilePath).delete(); 265 if (report.hasError() || report.hasWarning()) { 266 ok = false; 267 } 268 } 269 } catch (CmsException e) { 270 ok = false; 271 throw e; 272 } catch (RuntimeException e) { 273 ok = false; 274 throw e; 275 } finally { 276 m_moduleLog.log(moduleName, Action.importModule, ok); 277 } 278 } 279 280 /** 281 * Initializes the CMS context.<p> 282 * 283 * @param adminCms the admin CMS context 284 */ 285 public void initialize(CmsObject adminCms) { 286 287 m_adminCms = adminCms; 288 } 289 290 /** 291 * Computes a module hash, which should change when a module changes and stay the same when the module doesn't change.<p> 292 * 293 * We only use the modification time of the module resources and their descendants and the modification time of the metadata 294 * for computing it. 295 * 296 * @param module the module for which to compute the module signature 297 * @param project the project in which to compute the module hash 298 * @return the module signature 299 * @throws CmsException if something goes wrong 300 */ 301 private String computeModuleHash(CmsModule module, CmsProject project) throws CmsException { 302 303 LOG.info("Getting module hash for " + module.getName()); 304 // This method may be called very frequently during a short time, but it is unlikely 305 // that a module changes multiple times in a few seconds, so we use a timed cache here 306 String cachedValue = m_newModuleHashCache.get(module); 307 if (cachedValue != null) { 308 LOG.info("Using cached value for module hash of " + module.getName()); 309 return cachedValue; 310 } 311 312 CmsObject cms = OpenCms.initCmsObject(m_adminCms); 313 if (!CmsStringUtil.isEmptyOrWhitespaceOnly(module.getSite())) { 314 cms.getRequestContext().setSiteRoot(module.getSite()); 315 } 316 cms.getRequestContext().setCurrentProject(project); 317 318 // We compute a hash code from the paths of all resources belonging to the module and their respective modification dates. 319 List<String> entries = Lists.newArrayList(); 320 for (String path : module.getResources()) { 321 try { 322 Set<CmsResource> resources = Sets.newHashSet(); 323 CmsResource moduleRes = cms.readResource(path, CmsResourceFilter.IGNORE_EXPIRATION); 324 resources.add(moduleRes); 325 if (moduleRes.isFolder()) { 326 resources.addAll(cms.readResources(path, CmsResourceFilter.IGNORE_EXPIRATION, true)); 327 } 328 for (CmsResource res : resources) { 329 entries.add(res.getRootPath() + ":" + res.getDateLastModified()); 330 } 331 } catch (CmsVfsResourceNotFoundException e) { 332 entries.add(path + ":null"); 333 } 334 } 335 Collections.sort(entries); 336 String inputString = CmsStringUtil.listAsString(entries, "\n") + "\nMETA:" + module.getObjectCreateTime(); 337 LOG.debug("Computing module hash from base string:\n" + inputString); 338 return "" + inputString.hashCode(); 339 } 340 341 /** 342 * Creates a randomized path for the temporary file used to store the import data.<p> 343 * 344 * @param name the module name 345 * 346 * @return the generated path 347 */ 348 private String createImportZipPath(String name) { 349 350 String path = ""; 351 do { 352 String prefix = RandomStringUtils.randomAlphanumeric(6) + "-"; 353 path = OpenCms.getSystemInfo().getAbsoluteRfsPathRelativeToWebInf( 354 CmsStringUtil.joinPaths(IMPORT_FOLDER_PATH, prefix + name)); 355 } while (new File(path).exists()); 356 return path; 357 } 358 359 /** 360 * Creates a new report for an export/import.<p> 361 * 362 * @return the new report 363 */ 364 private I_CmsReport createReport() { 365 366 return new CmsLogReport(Locale.ENGLISH, CmsModuleImportExportRepository.class); 367 } 368 369 /** 370 * Makes sure that the folders used to store the import/export data exist.<p> 371 */ 372 private void ensureFoldersExist() { 373 374 for (String path : Arrays.asList(IMPORT_FOLDER_PATH, EXPORT_FOLDER_PATH)) { 375 String folderPath = OpenCms.getSystemInfo().getAbsoluteRfsPathRelativeToWebInf(path); 376 File folder = new File(folderPath); 377 if (!folder.exists()) { 378 folder.mkdirs(); 379 } 380 } 381 } 382 383 /** 384 * Gets the virtual file name to use for the given module.<p> 385 * 386 * @param module the module for which to get the file name 387 * 388 * @return the file name 389 */ 390 private String getFileNameForModule(CmsModule module) { 391 392 return module.getName() + SUFFIX; 393 } 394 395 /** 396 * Gets the module which corresponds to the given virtual file name.<p> 397 * 398 * @param fileName the file name 399 * 400 * @return the module which corresponds to the given file name 401 */ 402 private CmsModule getModuleForFileName(String fileName) { 403 404 String moduleName = fileName; 405 if (fileName.endsWith(SUFFIX)) { 406 moduleName = fileName.substring(0, fileName.length() - SUFFIX.length()); 407 } 408 CmsModule result = OpenCms.getModuleManager().getModule(moduleName); 409 return result; 410 } 411 412 /** 413 * Checks if a given module needs to be re-exported.<p> 414 * 415 * @param module the module to check 416 * @param moduleFile the file representing the exported module (doesn't necessarily exist) 417 * @param project the project in which to check 418 * 419 * @return true if the module needs to be exported 420 */ 421 private boolean needToExportModule(CmsModule module, File moduleFile, CmsProject project) { 422 423 if (!moduleFile.exists()) { 424 LOG.info("Module export file doesn't exist, export is needed."); 425 try { 426 String moduleSignature = computeModuleHash(module, project); 427 if (moduleSignature != null) { 428 m_moduleHashCache.put(module, moduleSignature); 429 } 430 } catch (CmsException e) { 431 LOG.error(e.getLocalizedMessage(), e); 432 } 433 return true; 434 } else { 435 if (moduleFile.lastModified() < module.getObjectCreateTime()) { 436 return true; 437 } 438 439 String oldModuleSignature = m_moduleHashCache.get(module); 440 String newModuleSignature = null; 441 try { 442 newModuleSignature = computeModuleHash(module, project); 443 } catch (CmsException e) { 444 LOG.error(e.getLocalizedMessage(), e); 445 } 446 447 LOG.info( 448 "Comparing module hashes for " 449 + module.getName() 450 + " to check if export is needed: old = " 451 + oldModuleSignature 452 + ", new=" 453 + newModuleSignature); 454 if ((newModuleSignature == null) || !Objects.equal(oldModuleSignature, newModuleSignature)) { 455 if (newModuleSignature != null) { 456 m_moduleHashCache.put(module, newModuleSignature); 457 } 458 // if an error occurs or the module signatures don't match 459 return true; 460 } else { 461 return false; 462 } 463 } 464 } 465 466}