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