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