001/* 002 * Copyright 2010 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 */ 016 017package org.ametys.plugins.repository.provider; 018 019import java.io.File; 020import java.io.IOException; 021import java.io.InputStream; 022import java.time.Instant; 023import java.time.ZoneId; 024import java.time.format.DateTimeFormatter; 025import java.util.HashMap; 026import java.util.Map; 027 028import javax.jcr.LoginException; 029import javax.jcr.NoSuchWorkspaceException; 030import javax.jcr.RepositoryException; 031import javax.jcr.Session; 032import javax.jcr.SimpleCredentials; 033 034import org.apache.avalon.framework.CascadingRuntimeException; 035import org.apache.avalon.framework.activity.Disposable; 036import org.apache.avalon.framework.activity.Initializable; 037import org.apache.avalon.framework.component.Component; 038import org.apache.avalon.framework.context.ContextException; 039import org.apache.avalon.framework.context.Contextualizable; 040import org.apache.avalon.framework.service.ServiceException; 041import org.apache.avalon.framework.service.ServiceManager; 042import org.apache.cocoon.Constants; 043import org.apache.cocoon.components.ContextHelper; 044import org.apache.cocoon.environment.Context; 045import org.apache.cocoon.environment.Request; 046import org.apache.commons.io.IOUtils; 047import org.apache.excalibur.source.ModifiableSource; 048import org.apache.excalibur.source.ModifiableTraversableSource; 049import org.apache.excalibur.source.MoveableSource; 050import org.apache.excalibur.source.Source; 051import org.apache.excalibur.source.SourceResolver; 052import org.apache.excalibur.source.TraversableSource; 053import org.apache.jackrabbit.api.stats.RepositoryStatistics; 054import org.apache.jackrabbit.core.cache.CacheManager; 055import org.apache.jackrabbit.core.config.ConfigurationException; 056import org.apache.jackrabbit.core.config.RepositoryConfig; 057import org.apache.jackrabbit.stats.RepositoryStatisticsImpl; 058 059import org.ametys.core.datasource.ConnectionHelper; 060import org.ametys.plugins.repository.RepositoryConstants; 061import org.ametys.runtime.config.Config; 062import org.ametys.runtime.util.AmetysHomeHelper; 063 064/** 065 * JackrabbitRepository is a JCR repository component based on Jackrabbit 066 */ 067public class JackrabbitRepository extends AbstractRepository implements LogoutManager, Initializable, Contextualizable, Disposable, Component 068{ 069 private static final String __REPOSITORY_NODETYPES_PATH = "ametys-home://data/repository/repository/nodetypes"; 070 071 private org.apache.avalon.framework.context.Context _avalonContext; 072 private Context _context; 073 074 private Session _adminSession; 075 076 private SourceResolver _resolver; 077 078 /** 079 * Must implements ModifiableTraversableSource, MoveableSource 080 */ 081 private Source _customNodeTypesBackupSource; 082 083 @Override 084 public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException 085 { 086 _avalonContext = context; 087 _context = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 088 } 089 090 @Override 091 public void service(ServiceManager smanager) throws ServiceException 092 { 093 super.service(smanager); 094 // Make sure the ConnectionHelper is initialized first, so the SQLDataSourceManager can be used in AmetysPersistenceManager 095 smanager.lookup(ConnectionHelper.ROLE); 096 _resolver = (SourceResolver) smanager.lookup(SourceResolver.ROLE); 097 } 098 099 100 public void initialize() throws Exception 101 { 102 _backupCustomNodetypes(); 103 RepositoryConfig repositoryConfig = createRepositoryConfig(); 104 AmetysRepository repo = new AmetysRepository(repositoryConfig); 105 106 _delegate = repo; 107 ((AmetysRepository) _delegate).setLogoutManager(this); 108 109 Long cacheSize = Config.getInstance().getValue("org.ametys.plugins.repository.cache"); 110 if (cacheSize == null || cacheSize <= 0) 111 { 112 cacheSize = 16777216L; // default value; 113 } 114 115 CacheManager cacheManager = repo.getCacheManager(); 116 cacheManager.setMaxMemory(cacheSize); 117 cacheManager.setMaxMemoryPerCache(cacheSize / 4); 118 cacheManager.setMinMemoryPerCache(cacheSize / 128); 119 120 // Register the delegate repository in the context for the repository workspace. 121 _context.setAttribute(CONTEXT_REPOSITORY_KEY, _delegate); 122 _context.setAttribute(CONTEXT_CREDENTIALS_KEY, new SimpleCredentials("ametys", new char[]{})); 123 _context.setAttribute(CONTEXT_IS_JNDI_KEY, false); 124 125 _adminSession = login("default"); 126 127 if (getLogger().isDebugEnabled()) 128 { 129 getLogger().debug("JCR Repository running"); 130 } 131 } 132 133 public void dispose() 134 { 135 AmetysRepository repo = (AmetysRepository) _delegate; 136 repo.setLogoutManager(null); 137 repo.shutdown(); 138 } 139 140 /** 141 * Get the JCR repository statistics 142 * @return a {@link RepositoryStatisticsImpl} representing all the statistics available 143 */ 144 public RepositoryStatistics getRepositoryStatistics() 145 { 146 AmetysRepository repo = (AmetysRepository) _delegate; 147 return repo.getRepositoryStatistics(); 148 } 149 150 /** 151 * Returns the admin session 152 * @return the admin session 153 * @throws RepositoryException if an error occurs. 154 */ 155 public Session getAdminSession() throws RepositoryException 156 { 157 _adminSession.refresh(false); 158 return _adminSession; 159 } 160 161 162 /** 163 * Returns the all JCR workspaces 164 * @return the available workspaces 165 * @throws RepositoryException if an error occurred 166 */ 167 public String[] getWorkspaces() throws RepositoryException 168 { 169 return _adminSession.getWorkspace().getAccessibleWorkspaceNames(); 170 } 171 172 173 /** 174 * Create the repository configuration from the configuration file 175 * @return The repository configuration 176 * @throws ConfigurationException if an error occurred 177 */ 178 RepositoryConfig createRepositoryConfig() throws ConfigurationException 179 { 180 String config = _context.getRealPath("/WEB-INF/param/repository.xml"); 181 182 File homeFile = new File(AmetysHomeHelper.getAmetysHomeData(), "repository"); 183 184 if (getLogger().isDebugEnabled()) 185 { 186 getLogger().debug("Creating JCR Repository config at: " + homeFile.getAbsolutePath()); 187 } 188 189 return RepositoryConfig.create(config, homeFile.getAbsolutePath()); 190 } 191 192 int getSessionCount() 193 { 194 return ((AmetysRepository) _delegate).getSessionCount(); 195 } 196 197 public Session login(String workspace) throws LoginException, NoSuchWorkspaceException, RepositoryException 198 { 199 try 200 { 201 if (getLogger().isDebugEnabled()) 202 { 203 getLogger().debug("Getting JCR Session"); 204 } 205 206 try 207 { 208 Request request = ContextHelper.getRequest(_avalonContext); 209 210 @SuppressWarnings("unchecked") 211 Map<String, Session> sessions = (Map<String, Session>) request.getAttribute(RepositoryConstants.JCR_SESSION_REQUEST_ATTRIBUTE); 212 213 if (sessions == null) 214 { 215 sessions = new HashMap<>(); 216 request.setAttribute(RepositoryConstants.JCR_SESSION_REQUEST_ATTRIBUTE, sessions); 217 } 218 219 Session session = sessions.get(workspace); 220 221 if (session == null || !session.isLive()) 222 { 223 session = _delegate.login(new SimpleCredentials("ametys", new char[]{}), workspace); 224 sessions.put(workspace, session); 225 } 226 227 return session; 228 } 229 catch (CascadingRuntimeException e) 230 { 231 if (e.getCause() instanceof ContextException) 232 { 233 // Unable to get request. Must be in another thread or at init time. 234 Session session = _delegate.login(new SimpleCredentials("ametys", new char[]{}), workspace); 235 return session; 236 } 237 else 238 { 239 throw e; 240 } 241 } 242 } 243 catch (RepositoryException e) 244 { 245 throw e; 246 } 247 catch (Exception e) 248 { 249 throw new RuntimeException("Unable to get Session", e); 250 } 251 } 252 253 public void logout(Session session) 254 { 255 if (!(session instanceof AmetysSession)) 256 { 257 throw new IllegalArgumentException("JCR Session should be an instance of AmetysSession"); 258 } 259 260 AmetysSession ametysSession = (AmetysSession) session; 261 262 if (getLogger().isDebugEnabled()) 263 { 264 getLogger().debug("Logging out AmetysSession"); 265 } 266 267 try 268 { 269 // the following statement will fail if we are not processing a request. 270 ContextHelper.getRequest(_avalonContext); 271 272 // does nothing as the session will be actually logged out at the end of the request. 273 if (getLogger().isDebugEnabled()) 274 { 275 getLogger().debug("AmetysSession logout delayed until the end of the HTTP request."); 276 } 277 278 } 279 catch (Exception e) 280 { 281 // unable to get request. Must be in another thread or at init time. 282 if (getLogger().isDebugEnabled()) 283 { 284 getLogger().debug("Not in a request. AmetysSession will be actually logged out."); 285 } 286 287 ametysSession.forceLogout(); 288 } 289 } 290 291 /** 292 * Create a backup of custom_nodetypes.xml and make a backup with the curent time in the filename 293 * The backup file name is stored to be compared ONCE with the re-created custom_nodetypes.xml by calling {@link JackrabbitRepository#compareCustomNodetypes()} 294 */ 295 protected void _backupCustomNodetypes() 296 { 297 Source source = null; 298 Source destination = null; 299 try 300 { 301 source = _resolver.resolveURI(__REPOSITORY_NODETYPES_PATH + "/custom_nodetypes.xml"); 302 if (source.exists() && source instanceof ModifiableSource) 303 { 304 ModifiableSource fsource = (ModifiableSource) source; 305 306 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss").withZone(ZoneId.systemDefault()); 307 String timestamp = formatter.format(Instant.now()); 308 309 destination = _resolver.resolveURI(__REPOSITORY_NODETYPES_PATH + "/custom_nodetypes-" + timestamp + ".xml"); 310 if (destination.exists()) 311 { 312 fsource.delete(); 313 getLogger().warn("Impossible to backup custom_nodetypes.xml, custom_nodetypes-" + timestamp + ".xml already exists."); 314 } 315 else 316 { 317 _customNodeTypesBackupSource = destination; 318 getLogger().info("Backup custom_nodetypes.xml as custom_nodetypes-" + timestamp + ".xml"); 319 ((MoveableSource) fsource).moveTo(destination); 320 } 321 322 if (fsource.exists()) 323 { 324 getLogger().error("Impossible to delete custom_nodetypes.xml after creating a backup."); 325 } 326 } 327 } 328 catch (IOException e) 329 { 330 getLogger().error("An error occurred while backuping the custom_nodetypes.xml file", e); 331 } 332 finally 333 { 334 _resolver.release(source); 335 _resolver.release(destination); 336 } 337 } 338 339 /** 340 * Compare custom_nodetypes.xml with custom_nodetypes-[timestamp].xml and delete the backup if they are the same 341 */ 342 public void compareCustomNodetypes() 343 { 344 if (_customNodeTypesBackupSource != null && _customNodeTypesBackupSource.exists()) 345 { 346 Source source = null; 347 try 348 { 349 source = _resolver.resolveURI(__REPOSITORY_NODETYPES_PATH + "/custom_nodetypes.xml"); 350 351 if (source.exists() && source instanceof ModifiableTraversableSource) 352 { 353 if (_compareSources((TraversableSource) source, (TraversableSource) _customNodeTypesBackupSource)) 354 { 355 getLogger().info(((TraversableSource) _customNodeTypesBackupSource).getName() + " will be deleted, no changes found."); 356 ((ModifiableTraversableSource) _customNodeTypesBackupSource).delete(); 357 358 359 if (_customNodeTypesBackupSource.exists()) 360 { 361 getLogger().error("Impossible to delete custom_nodetypes.xml after creating a backup."); 362 } 363 } 364 else 365 { 366 getLogger().info(((TraversableSource) _customNodeTypesBackupSource).getName() + " will be kept, changes found with last version."); 367 } 368 } 369 else 370 { 371 getLogger().warn("Impossible to compare custom_nodetypes.xml, it seems to be unavailable."); 372 } 373 } 374 catch (IOException e) 375 { 376 getLogger().error("Impossible to compare custom_nodetypes.xml with it's backup '" + _customNodeTypesBackupSource.getURI() + "'", e); 377 } 378 finally 379 { 380 _resolver.release(source); 381 } 382 } 383 else 384 { 385 getLogger().debug("There is no backup of custom_nodetypes.xml to compare"); 386 } 387 388 _resolver.release(_customNodeTypesBackupSource); 389 _customNodeTypesBackupSource = null; 390 } 391 392 /** 393 * Compare 2 sources 394 * @param source1 source to compare 395 * @param source2 source to compare 396 * @return true if the content of both sources is equal 397 * @throws IOException something went wrong reading the sources 398 */ 399 protected boolean _compareSources(TraversableSource source1, TraversableSource source2) throws IOException 400 { 401 if (source1 == null && source2 == null) 402 { 403 return true; 404 } 405 if (source1 == null || source2 == null) 406 { 407 return false; 408 } 409 final boolean source1Exists = source1.exists(); 410 if (source1Exists != source2.exists()) 411 { 412 return false; 413 } 414 415 if (!source1Exists) 416 { 417 // two not existing files are equal 418 return true; 419 } 420 421 422 423 if (source1.isCollection() || source2.isCollection()) 424 { 425 // don't want to compare directory contents 426 throw new IOException("Can't compare collections, only content source"); 427 } 428 429 if (source1.getContentLength() != source2.getContentLength()) 430 { 431 // lengths differ, cannot be equal 432 return false; 433 } 434 435 if (source1.getURI().equals(source2.getURI())) 436 { 437 // same source 438 return true; 439 } 440 441 try (InputStream input1 = source1.getInputStream(); 442 InputStream input2 = source2.getInputStream()) 443 { 444 return IOUtils.contentEquals(input1, input2); 445 } 446 } 447}