001/* 002 Licensed to the Apache Software Foundation (ASF) under one 003 or more contributor license agreements. See the NOTICE file 004 distributed with this work for additional information 005 regarding copyright ownership. The ASF licenses this file 006 to you under the Apache License, Version 2.0 (the 007 "License"); you may not use this file except in compliance 008 with the License. You may obtain a copy of the License at 009 010 http://www.apache.org/licenses/LICENSE-2.0 011 012 Unless required by applicable law or agreed to in writing, 013 software distributed under the License is distributed on an 014 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 KIND, either express or implied. See the License for the 016 specific language governing permissions and limitations 017 under the License. 018 */ 019package org.apache.wiki.pages; 020 021import org.apache.commons.lang3.ArrayUtils; 022import org.apache.log4j.Logger; 023import org.apache.wiki.WikiBackgroundThread; 024import org.apache.wiki.WikiEngine; 025import org.apache.wiki.WikiPage; 026import org.apache.wiki.WikiProvider; 027import org.apache.wiki.api.exceptions.NoRequiredPropertyException; 028import org.apache.wiki.api.exceptions.ProviderException; 029import org.apache.wiki.api.exceptions.WikiException; 030import org.apache.wiki.auth.WikiPrincipal; 031import org.apache.wiki.auth.WikiSecurityException; 032import org.apache.wiki.auth.acl.Acl; 033import org.apache.wiki.auth.acl.AclEntry; 034import org.apache.wiki.auth.acl.AclEntryImpl; 035import org.apache.wiki.auth.user.UserProfile; 036import org.apache.wiki.event.WikiEvent; 037import org.apache.wiki.event.WikiEventManager; 038import org.apache.wiki.event.WikiPageEvent; 039import org.apache.wiki.event.WikiSecurityEvent; 040import org.apache.wiki.modules.ModuleManager; 041import org.apache.wiki.modules.WikiModuleInfo; 042import org.apache.wiki.providers.RepositoryModifiedException; 043import org.apache.wiki.providers.WikiPageProvider; 044import org.apache.wiki.util.ClassUtil; 045import org.apache.wiki.util.TextUtil; 046 047import java.io.IOException; 048import java.security.Permission; 049import java.security.Principal; 050import java.util.ArrayList; 051import java.util.Collection; 052import java.util.Date; 053import java.util.Enumeration; 054import java.util.Iterator; 055import java.util.List; 056import java.util.NoSuchElementException; 057import java.util.Properties; 058import java.util.concurrent.ConcurrentHashMap; 059 060 061/** 062 * Manages the WikiPages. This class functions as an unified interface towards 063 * the page providers. It handles initialization and management of the providers, 064 * and provides utility methods for accessing the contents. 065 * <p/> 066 * Saving a page is a two-stage Task; first the pre-save operations and then the 067 * actual save. See the descriptions of the tasks for further information. 068 * 069 * @since 2.0 070 */ 071// FIXME: This class currently only functions just as an extra layer over providers, 072// complicating things. We need to move more provider-specific functionality 073// from WikiEngine (which is too big now) into this class. 074public class DefaultPageManager extends ModuleManager implements PageManager { 075 076 private static final Logger LOG = Logger.getLogger(DefaultPageManager.class); 077 078 private WikiPageProvider m_provider; 079 080 protected ConcurrentHashMap<String, PageLock> m_pageLocks = new ConcurrentHashMap<>(); 081 082 //private WikiEngine m_engine; //inherited protected field from the ModuleManager 083 084 private int m_expiryTime = 60; 085 086 private LockReaper m_reaper = null; 087 088 private PageSorter pageSorter = new PageSorter(); 089 090 /** 091 * Creates a new PageManager. 092 * 093 * @param engine WikiEngine instance 094 * @param props Properties to use for initialization 095 * @throws NoSuchElementException {@value #PROP_PAGEPROVIDER} property not found on WikiEngine properties 096 * @throws WikiException If anything goes wrong, you get this. 097 */ 098 public DefaultPageManager(WikiEngine engine, Properties props) throws NoSuchElementException, WikiException { 099 super(engine); 100 String classname; 101 m_engine = engine; 102 boolean useCache = "true".equals(props.getProperty(PROP_USECACHE)); 103 104 m_expiryTime = TextUtil.parseIntParameter(props.getProperty(PROP_LOCKEXPIRY), 60); 105 106 // 107 // If user wants to use a cache, then we'll use the CachingProvider. 108 // 109 if (useCache) { 110 classname = "org.apache.wiki.providers.CachingProvider"; 111 } else { 112 classname = TextUtil.getRequiredProperty(props, PROP_PAGEPROVIDER); 113 } 114 115 pageSorter.initialize( props ); 116 117 try { 118 LOG.debug("Page provider class: '" + classname + "'"); 119 Class<?> providerclass = ClassUtil.findClass("org.apache.wiki.providers", classname); 120 m_provider = (WikiPageProvider) providerclass.newInstance(); 121 122 LOG.debug("Initializing page provider class " + m_provider); 123 m_provider.initialize(m_engine, props); 124 } catch (ClassNotFoundException e) { 125 LOG.error("Unable to locate provider class '" + classname + "' (" + e.getMessage() + ")", e); 126 throw new WikiException("No provider class. (" + e.getMessage() + ")", e); 127 } catch (InstantiationException e) { 128 LOG.error("Unable to create provider class '" + classname + "' (" + e.getMessage() + ")", e); 129 throw new WikiException("Faulty provider class. (" + e.getMessage() + ")", e); 130 } catch (IllegalAccessException e) { 131 LOG.error("Illegal access to provider class '" + classname + "' (" + e.getMessage() + ")", e); 132 throw new WikiException("Illegal provider class. (" + e.getMessage() + ")", e); 133 } catch (NoRequiredPropertyException e) { 134 LOG.error("Provider did not found a property it was looking for: " + e.getMessage(), e); 135 throw e; // Same exception works. 136 } catch (IOException e) { 137 LOG.error("An I/O exception occurred while trying to create a new page provider: " + classname, e); 138 throw new WikiException("Unable to start page provider: " + e.getMessage(), e); 139 } 140 141 } 142 143 /* (non-Javadoc) 144 * @see org.apache.wiki.pages.PageManager#getProvider() 145 */ 146 @Override 147 public WikiPageProvider getProvider() { 148 return m_provider; 149 } 150 151 /* (non-Javadoc) 152 * @see org.apache.wiki.pages.PageManager#getAllPages() 153 */ 154 @Override 155 public Collection< WikiPage > getAllPages() throws ProviderException { 156 return m_provider.getAllPages(); 157 } 158 159 /* (non-Javadoc) 160 * @see org.apache.wiki.pages.PageManager#getPageText(java.lang.String, int) 161 */ 162 @Override 163 public String getPageText(String pageName, int version) throws ProviderException { 164 if (pageName == null || pageName.length() == 0) { 165 throw new ProviderException("Illegal page name"); 166 } 167 String text = null; 168 169 try { 170 text = m_provider.getPageText(pageName, version); 171 } catch (RepositoryModifiedException e) { 172 // 173 // This only occurs with the latest version. 174 // 175 LOG.info("Repository has been modified externally while fetching page " + pageName); 176 177 // 178 // Empty the references and yay, it shall be recalculated 179 // 180 //WikiPage p = new WikiPage( pageName ); 181 WikiPage p = m_provider.getPageInfo(pageName, version); 182 183 m_engine.updateReferences(p); 184 185 if (p != null) { 186 m_engine.getSearchManager().reindexPage(p); 187 text = m_provider.getPageText(pageName, version); 188 } else { 189 // 190 // Make sure that it no longer exists in internal data structures either. 191 // 192 WikiPage dummy = new WikiPage(m_engine, pageName); 193 m_engine.getSearchManager().pageRemoved(dummy); 194 m_engine.getReferenceManager().pageRemoved(dummy); 195 } 196 } 197 198 return text; 199 } 200 201 /* (non-Javadoc) 202 * @see org.apache.wiki.pages.PageManager#getEngine() 203 */ 204 @Override 205 public WikiEngine getEngine() { 206 return m_engine; 207 } 208 209 /* (non-Javadoc) 210 * @see org.apache.wiki.pages.PageManager#putPageText(org.apache.wiki.WikiPage, java.lang.String) 211 */ 212 @Override 213 public void putPageText(WikiPage page, String content) throws ProviderException { 214 if (page == null || page.getName() == null || page.getName().length() == 0) { 215 throw new ProviderException("Illegal page name"); 216 } 217 218 m_provider.putPageText(page, content); 219 } 220 221 /* (non-Javadoc) 222 * @see org.apache.wiki.pages.PageManager#lockPage(org.apache.wiki.WikiPage, java.lang.String) 223 */ 224 @Override 225 public PageLock lockPage(WikiPage page, String user) { 226 if (m_reaper == null) { 227 // 228 // Start the lock reaper lazily. We don't want to start it in 229 // the constructor, because starting threads in constructors 230 // is a bad idea when it comes to inheritance. Besides, 231 // laziness is a virtue. 232 // 233 m_reaper = new LockReaper(m_engine); 234 m_reaper.start(); 235 } 236 237 fireEvent(WikiPageEvent.PAGE_LOCK, page.getName()); // prior to or after actual lock? 238 PageLock lock = m_pageLocks.get(page.getName()); 239 240 if (lock == null) { 241 // 242 // Lock is available, so make a lock. 243 // 244 Date d = new Date(); 245 lock = new PageLock(page, user, d, new Date(d.getTime() + m_expiryTime * 60 * 1000L)); 246 m_pageLocks.put(page.getName(), lock); 247 LOG.debug("Locked page " + page.getName() + " for " + user); 248 } else { 249 LOG.debug("Page " + page.getName() + " already locked by " + lock.getLocker()); 250 lock = null; // Nothing to return 251 } 252 253 return lock; 254 } 255 256 /* (non-Javadoc) 257 * @see org.apache.wiki.pages.PageManager#unlockPage(org.apache.wiki.pages.PageLock) 258 */ 259 @Override 260 public void unlockPage(PageLock lock) { 261 if (lock == null) { 262 return; 263 } 264 265 m_pageLocks.remove(lock.getPage()); 266 LOG.debug("Unlocked page " + lock.getPage()); 267 268 fireEvent(WikiPageEvent.PAGE_UNLOCK, lock.getPage()); 269 } 270 271 /* (non-Javadoc) 272 * @see org.apache.wiki.pages.PageManager#getCurrentLock(org.apache.wiki.WikiPage) 273 */ 274 @Override 275 public PageLock getCurrentLock(WikiPage page) { 276 return m_pageLocks.get(page.getName()); 277 } 278 279 /* (non-Javadoc) 280 * @see org.apache.wiki.pages.PageManager#getActiveLocks() 281 */ 282 @Override 283 public List<PageLock> getActiveLocks() { 284 ArrayList<PageLock> result = new ArrayList<>(); 285 286 for (PageLock lock : m_pageLocks.values()) { 287 result.add(lock); 288 } 289 290 return result; 291 } 292 293 /* (non-Javadoc) 294 * @see org.apache.wiki.pages.PageManager#getPageInfo(java.lang.String, int) 295 */ 296 @Override 297 public WikiPage getPageInfo(String pageName, int version) throws ProviderException { 298 if (pageName == null || pageName.length() == 0) { 299 throw new ProviderException("Illegal page name '" + pageName + "'"); 300 } 301 302 WikiPage page = null; 303 304 try { 305 page = m_provider.getPageInfo(pageName, version); 306 } catch (RepositoryModifiedException e) { 307 // 308 // This only occurs with the latest version. 309 // 310 LOG.info("Repository has been modified externally while fetching info for " + pageName); 311 page = m_provider.getPageInfo(pageName, version); 312 if (page != null) { 313 m_engine.updateReferences(page); 314 } else { 315 m_engine.getReferenceManager().pageRemoved(new WikiPage(m_engine, pageName)); 316 } 317 } 318 319 // 320 // Should update the metadata. 321 // 322 /* 323 if( page != null && !page.hasMetadata() ) 324 { 325 WikiContext ctx = new WikiContext(m_engine,page); 326 m_engine.textToHTML( ctx, getPageText(pageName,version) ); 327 } 328 */ 329 return page; 330 } 331 332 /* (non-Javadoc) 333 * @see org.apache.wiki.pages.PageManager#getVersionHistory(java.lang.String) 334 */ 335 @Override 336 public List< WikiPage > getVersionHistory(String pageName) throws ProviderException { 337 if (pageExists(pageName)) { 338 return m_provider.getVersionHistory(pageName); 339 } 340 341 return null; 342 } 343 344 /* (non-Javadoc) 345 * @see org.apache.wiki.pages.PageManager#getProviderDescription() 346 */ 347 @Override 348 public String getProviderDescription() { 349 return m_provider.getProviderInfo(); 350 } 351 352 /* (non-Javadoc) 353 * @see org.apache.wiki.pages.PageManager#getTotalPageCount() 354 */ 355 @Override 356 public int getTotalPageCount() { 357 try { 358 return m_provider.getAllPages().size(); 359 } catch (ProviderException e) { 360 LOG.error("Unable to count pages: ", e); 361 return -1; 362 } 363 } 364 365 /* (non-Javadoc) 366 * @see org.apache.wiki.pages.PageManager#pageExists(java.lang.String) 367 */ 368 @Override 369 public boolean pageExists(String pageName) throws ProviderException { 370 if (pageName == null || pageName.length() == 0) { 371 throw new ProviderException("Illegal page name"); 372 } 373 374 return m_provider.pageExists(pageName); 375 } 376 377 /* (non-Javadoc) 378 * @see org.apache.wiki.pages.PageManager#pageExists(java.lang.String, int) 379 */ 380 @Override 381 public boolean pageExists(String pageName, int version) throws ProviderException { 382 if (pageName == null || pageName.length() == 0) { 383 throw new ProviderException("Illegal page name"); 384 } 385 386 if (version == WikiProvider.LATEST_VERSION) { 387 return pageExists(pageName); 388 } 389 390 return m_provider.pageExists(pageName, version); 391 } 392 393 /* (non-Javadoc) 394 * @see org.apache.wiki.pages.PageManager#deleteVersion(org.apache.wiki.WikiPage) 395 */ 396 @Override 397 public void deleteVersion(WikiPage page) throws ProviderException { 398 m_provider.deleteVersion(page.getName(), page.getVersion()); 399 400 // FIXME: If this was the latest, reindex Lucene 401 // FIXME: Update RefMgr 402 } 403 404 /* (non-Javadoc) 405 * @see org.apache.wiki.pages.PageManager#deletePage(org.apache.wiki.WikiPage) 406 */ 407 @Override 408 public void deletePage(WikiPage page) throws ProviderException { 409 fireEvent(WikiPageEvent.PAGE_DELETE_REQUEST, page.getName()); 410 m_provider.deletePage(page.getName()); 411 fireEvent(WikiPageEvent.PAGE_DELETED, page.getName()); 412 } 413 414 /** 415 * This is a simple reaper thread that runs roughly every minute 416 * or so (it's not really that important, as long as it runs), 417 * and removes all locks that have expired. 418 */ 419 private class LockReaper extends WikiBackgroundThread { 420 /** 421 * Create a LockReaper for a given engine. 422 * 423 * @param engine WikiEngine to own this thread. 424 */ 425 public LockReaper(WikiEngine engine) { 426 super(engine, 60); 427 setName("JSPWiki Lock Reaper"); 428 } 429 430 @Override 431 public void backgroundTask() throws Exception { 432 Collection<PageLock> entries = m_pageLocks.values(); 433 for (Iterator<PageLock> i = entries.iterator(); i.hasNext(); ) { 434 PageLock p = i.next(); 435 436 if ( p.isExpired() ) { 437 i.remove(); 438 439 LOG.debug("Reaped lock: " + p.getPage() + 440 " by " + p.getLocker() + 441 ", acquired " + p.getAcquisitionTime() + 442 ", and expired " + p.getExpiryTime()); 443 } 444 } 445 } 446 } 447 448 // events processing ....................................................... 449 450 /** 451 * Fires a WikiPageEvent of the provided type and page name 452 * to all registered listeners. 453 * 454 * @param type the event type to be fired 455 * @param pagename the wiki page name as a String 456 * @see org.apache.wiki.event.WikiPageEvent 457 */ 458 protected final void fireEvent(int type, String pagename) { 459 if (WikiEventManager.isListening(this)) { 460 WikiEventManager.fireEvent(this, new WikiPageEvent(m_engine, type, pagename)); 461 } 462 } 463 464 /** 465 * {@inheritDoc} 466 */ 467 @Override 468 public Collection< WikiModuleInfo > modules() { 469 return new ArrayList<>(); 470 } 471 472 /** 473 * Returns null! 474 * {@inheritDoc} 475 */ 476 @Override 477 public WikiModuleInfo getModuleInfo(String moduleName) { 478 return null; 479 } 480 481 /* (non-Javadoc) 482 * @see org.apache.wiki.pages.PageManager#actionPerformed(org.apache.wiki.event.WikiEvent) 483 */ 484 @Override 485 public void actionPerformed(WikiEvent event) { 486 if (!(event instanceof WikiSecurityEvent)) { 487 return; 488 } 489 490 WikiSecurityEvent se = (WikiSecurityEvent) event; 491 if (se.getType() == WikiSecurityEvent.PROFILE_NAME_CHANGED) { 492 UserProfile[] profiles = (UserProfile[]) se.getTarget(); 493 Principal[] oldPrincipals = new Principal[] 494 {new WikiPrincipal(profiles[0].getLoginName()), 495 new WikiPrincipal(profiles[0].getFullname()), 496 new WikiPrincipal(profiles[0].getWikiName())}; 497 Principal newPrincipal = new WikiPrincipal(profiles[1].getFullname()); 498 499 // Examine each page ACL 500 try { 501 int pagesChanged = 0; 502 Collection< WikiPage > pages = getAllPages(); 503 for (Iterator< WikiPage > it = pages.iterator(); it.hasNext(); ) { 504 WikiPage page = it.next(); 505 boolean aclChanged = changeAcl(page, oldPrincipals, newPrincipal); 506 if (aclChanged) { 507 // If the Acl needed changing, change it now 508 try { 509 m_engine.getAclManager().setPermissions(page, page.getAcl()); 510 } catch (WikiSecurityException e) { 511 LOG.error("Could not change page ACL for page " + page.getName() + ": " + e.getMessage(), e); 512 } 513 pagesChanged++; 514 } 515 } 516 LOG.info("Profile name change for '" + newPrincipal.toString() + 517 "' caused " + pagesChanged + " page ACLs to change also."); 518 } catch (ProviderException e) { 519 // Oooo! This is really bad... 520 LOG.error("Could not change user name in Page ACLs because of Provider error:" + e.getMessage(), e); 521 } 522 } 523 } 524 525 /** 526 * For a single wiki page, replaces all Acl entries matching a supplied array of Principals 527 * with a new Principal. 528 * 529 * @param page the wiki page whose Acl is to be modified 530 * @param oldPrincipals an array of Principals to replace; all AclEntry objects whose 531 * {@link AclEntry#getPrincipal()} method returns one of these Principals will be replaced 532 * @param newPrincipal the Principal that should receive the old Principals' permissions 533 * @return <code>true</code> if the Acl was actually changed; <code>false</code> otherwise 534 */ 535 protected boolean changeAcl(WikiPage page, Principal[] oldPrincipals, Principal newPrincipal) { 536 Acl acl = page.getAcl(); 537 boolean pageChanged = false; 538 if (acl != null) { 539 Enumeration<AclEntry> entries = acl.entries(); 540 Collection<AclEntry> entriesToAdd = new ArrayList<>(); 541 Collection<AclEntry> entriesToRemove = new ArrayList<>(); 542 while (entries.hasMoreElements()) { 543 AclEntry entry = entries.nextElement(); 544 if (ArrayUtils.contains(oldPrincipals, entry.getPrincipal())) { 545 // Create new entry 546 AclEntry newEntry = new AclEntryImpl(); 547 newEntry.setPrincipal(newPrincipal); 548 Enumeration<Permission> permissions = entry.permissions(); 549 while (permissions.hasMoreElements()) { 550 Permission permission = permissions.nextElement(); 551 newEntry.addPermission(permission); 552 } 553 pageChanged = true; 554 entriesToRemove.add(entry); 555 entriesToAdd.add(newEntry); 556 } 557 } 558 for (Iterator<AclEntry> ix = entriesToRemove.iterator(); ix.hasNext(); ) { 559 AclEntry entry = ix.next(); 560 acl.removeEntry(entry); 561 } 562 for (Iterator<AclEntry> ix = entriesToAdd.iterator(); ix.hasNext(); ) { 563 AclEntry entry = ix.next(); 564 acl.addEntry(entry); 565 } 566 } 567 return pageChanged; 568 } 569 570 /* (non-Javadoc) 571 * @see org.apache.wiki.pages.PageManager#getPageSorter() 572 */ 573 @Override 574 public PageSorter getPageSorter() { 575 return pageSorter; 576 } 577 578}