com.edgenius.wiki.service.impl.PageServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.edgenius.wiki.service.impl.PageServiceImpl.java

Source

/* 
 * =============================================================
 * Copyright (C) 2007-2011 Edgenius (http://www.edgenius.com)
 * =============================================================
 * License Information: http://www.edgenius.com/licensing/edgenius/2.0/
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2.0
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 * http://www.gnu.org/licenses/gpl.txt
 *  
 * ****************************************************************
 */
package com.edgenius.wiki.service.impl;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;

import com.edgenius.core.Global;
import com.edgenius.core.model.User;
import com.edgenius.core.repository.FileNode;
import com.edgenius.core.repository.ITicket;
import com.edgenius.core.repository.RepositoryException;
import com.edgenius.core.repository.RepositoryQuotaException;
import com.edgenius.core.repository.RepositoryService;
import com.edgenius.core.repository.RepositoryTiemoutExcetpion;
import com.edgenius.core.service.UserReadingService;
import com.edgenius.core.util.AuditLogger;
import com.edgenius.wiki.MenuItem;
import com.edgenius.wiki.MenuItem.MenuItemComparator;
import com.edgenius.wiki.WikiConstants;
import com.edgenius.wiki.dao.DraftDAO;
import com.edgenius.wiki.dao.HistoryDAO;
import com.edgenius.wiki.dao.PageDAO;
import com.edgenius.wiki.dao.PageLinkDAO;
import com.edgenius.wiki.dao.PageProgressDAO;
import com.edgenius.wiki.dao.SpaceDAO;
import com.edgenius.wiki.dao.UserPageDAO;
import com.edgenius.wiki.gwt.client.model.LinkModel;
import com.edgenius.wiki.gwt.client.model.MacroModel;
import com.edgenius.wiki.gwt.client.model.RenderPiece;
import com.edgenius.wiki.gwt.client.server.constant.PageType;
import com.edgenius.wiki.gwt.client.server.utils.NameConstants;
import com.edgenius.wiki.gwt.client.server.utils.PageAttribute;
import com.edgenius.wiki.gwt.client.server.utils.SharedConstants;
import com.edgenius.wiki.model.AbstractPage;
import com.edgenius.wiki.model.Draft;
import com.edgenius.wiki.model.DraftContent;
import com.edgenius.wiki.model.History;
import com.edgenius.wiki.model.HistoryContent;
import com.edgenius.wiki.model.Page;
import com.edgenius.wiki.model.PageContent;
import com.edgenius.wiki.model.PageLink;
import com.edgenius.wiki.model.PageProgress;
import com.edgenius.wiki.model.PageTag;
import com.edgenius.wiki.model.Space;
import com.edgenius.wiki.model.UserPageMark;
import com.edgenius.wiki.render.macro.MenuItemMacro;
import com.edgenius.wiki.security.WikiSecurityValues.WikiOPERATIONS;
import com.edgenius.wiki.security.service.SecurityService;
import com.edgenius.wiki.service.CommentService;
import com.edgenius.wiki.service.DuplicatedPageException;
import com.edgenius.wiki.service.EventContainer;
import com.edgenius.wiki.service.PageEventHanderException;
import com.edgenius.wiki.service.PageEventListener;
import com.edgenius.wiki.service.PageException;
import com.edgenius.wiki.service.PageSaveTiemoutExcetpion;
import com.edgenius.wiki.service.PageService;
import com.edgenius.wiki.service.RenderService;
import com.edgenius.wiki.service.SettingService;
import com.edgenius.wiki.service.SpaceNotFoundException;
import com.edgenius.wiki.service.TagService;
import com.edgenius.wiki.service.TouchService;
import com.edgenius.wiki.service.VersionConflictException;
import com.edgenius.wiki.util.PageComparator;
import com.edgenius.wiki.util.WikiUtil;

/**
 * @author Dapeng.Ni
 */
@Transactional
public class PageServiceImpl implements PageService {
    private static final Logger log = LoggerFactory.getLogger(PageServiceImpl.class);

    //Basic refer services
    private UserReadingService userReadingService;
    private RepositoryService repositoryService;
    private TagService tagService;

    private TouchService touchService;
    private CommentService commentService;
    private RenderService renderService;
    private SecurityService securityService;
    @Autowired
    private SettingService settingService;

    //For memory saving reason, cached page only save uid, Uuid, level, spaceUname, title 
    //and parent info(parent page also includes same info)
    private Cache pageTreeCache;

    private EventContainer eventContainer;

    //save: pageUuid -> map of username and time editing. 
    private Cache pageEditingCache;
    //DAO 
    private PageDAO pageDAO;
    private PageProgressDAO pageProgressDAO;
    private DraftDAO draftDAO;
    private HistoryDAO historyDAO;
    private SpaceDAO spaceDAO;
    private PageLinkDAO pageLinkDAO;
    private UserPageDAO userPageDAO;

    //********************************************************************
    //                       Methods
    //********************************************************************
    public Page savePage(Page pageValue, int requireNotified, boolean forceSave)
            throws PageException, VersionConflictException, DuplicatedPageException, PageSaveTiemoutExcetpion {
        Page page = null;
        String spaceUname = pageValue.getSpace().getUnixName();
        String newPageTitle = pageValue.getTitle();
        Integer newPageUid = pageValue.getUid();

        log.info("Page saving for " + pageValue.getTitle() + " on space " + spaceUname);
        Space space;
        //page already exist, need clone then save a new record in database
        String oldTitle = null;
        boolean needRefreshCache = false;

        if (newPageUid != null) {
            //The page will create  old version to new record but update same UID as current 
            //it would get same result by pageDAO.getCurrentByUuid() but a little bit faster in performance.
            page = pageDAO.get(newPageUid);
        } else if (!StringUtils.isBlank(pageValue.getPageUuid())) {
            //if user choose a item from My Draft in Dashboard, this won't bring in a newPageUid 
            //There are 3 scenarios for this case. 
            //1. it is a existed page draft.Following method returns current page,
            //2. non-existed page draft. Following method returns null.
            //3. non-existed page but page has a copy in trash bin! The below method return null as well, but the uuid is already invalid
            // as it is used by trashed page - so need further check - if it has trashed page, reset pageUUID
            page = pageDAO.getCurrentByUuid(pageValue.getPageUuid());

            if (page == null) {
                Page removedPage = pageDAO.getByUuid(pageValue.getPageUuid());
                if (removedPage != null && removedPage.isRemoved()) {
                    //case 3, treat it as new page
                    pageValue.setPageUuid(null);
                }
            }
        }

        if (!forceSave && !checkVersion(pageValue, page)) {
            throw new VersionConflictException(page.getVersion());
        }

        //!!!Title duplicated problem: user try to create a new page or rename a page but same title already exist in space 
        Page sameTitlePage = pageDAO.getCurrentPageByTitle(spaceUname, newPageTitle);
        if (page != null) {
            if (sameTitlePage != null) {
                if (!sameTitlePage.getPageUuid().equals(page.getPageUuid()))
                    throw new DuplicatedPageException();
            }

            //keep old page :NOTE: this piece code has duplicate with fixLinksToTitle() method
            History oldPage = (History) page.cloneToHistory();
            //put this page to history page:create a new record with cloned value except Uid
            //         history page does not save link, tag and attachment info. 
            //         The key is save content change!
            oldPage.setAttachments(null);
            oldPage.setParent(null);
            historyDAO.saveOrUpdate(oldPage);

            if (!StringUtils.equalsIgnoreCase(oldPage.getTitle(), newPageTitle)) {
                // oldTitle is not null, so that update PageLink on below
                oldTitle = oldPage.getTitle();
                needRefreshCache = true;
                //remove old page with old title from cache first, new page should add after page saved
                removePageCache(spaceUname, page, false);
            }
            //update current page with new value
            space = page.getSpace();

            copyValueFromView(page, pageValue);
            //         page.setUnixName(WikiUtil.getPageUnixname(newPageTitle));
            WikiUtil.setTouchedInfo(userReadingService, page);
            page.setVersion(page.getVersion() + 1);
        } else {
            //for new create page: same title page must not exist
            if (sameTitlePage != null) {
                throw new DuplicatedPageException("Page has duplicated title:" + newPageTitle);
            }

            needRefreshCache = true;
            //a new page first time save:
            page = new Page();
            copyValueFromView(page, pageValue);

            space = spaceDAO.getByUname(spaceUname);
            page.setSpace(space);

            //??? CascadeType.PERSIST seems does not work well. I must explicit call save(), but in CascadeType.ALL, it is not necessary.
            pageProgressDAO.saveOrUpdate(page.getPageProgress());

            page.setVersion(1);
            //if there is draft existed before page first create, keep draft uuid as page uuid!!! 
            if (StringUtils.isBlank(pageValue.getPageUuid()))
                page.setPageUuid(WikiUtil.createPageUuid(spaceUname, spaceUname, spaceUname, repositoryService));
            else
                page.setPageUuid(pageValue.getPageUuid());
            //         page.setUnixName(WikiUtil.getPageUnixname(newPageTitle));
            WikiUtil.setTouchedInfo(userReadingService, page);

            if (pageValue.getParent() != null && !StringUtils.isBlank(pageValue.getParent().getPageUuid())) {
                Page parentPage = pageDAO.getCurrentByUuid(pageValue.getParent().getPageUuid());
                if (parentPage != null) {
                    //maybe parent page is deleted as well.
                    page.setParent(parentPage);
                    page.setLevel(parentPage.getLevel() + 1);
                } else {
                    log.warn("page parent page does not exist. Page title is " + pageValue.getTitle()
                            + ". Parent page uuid is " + pageValue.getParent().getPageUuid());
                }
            } else
                //root page, such as home page
                page.setLevel(0);
        }

        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        //update page tags
        tagService.saveUpdatePageTag(page, pageValue.getTagString());

        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        // !!!! Important: this update attachments status must before  renderService.renderHTML(page)
        // otherwise, the drafts attachment won't render in {attach} or {gallery} macro....

        // Update page attachment status 
        //   remove this user's draft, does not use getDraft() then remove, because following error: 
        //      org.hibernate.HibernateException: Found shared references to a collection: com.edgenius.wiki.model.Page.tags
        try {
            User viewer = WikiUtil.getUser(userReadingService);
            mergeAttahment(getPageAttachment(spaceUname, page.getPageUuid(), true, true, viewer),
                    pageValue.getAttachments(), spaceUname, viewer, PageType.NONE_DRAFT);
            upgradeAttachmentStatus(spaceUname, page.getPageUuid(), page.getModifier(), PageType.NONE_DRAFT);
        } catch (RepositoryException e) {
            //not critical exception, just log:
            log.error("Update attachment status during saving page:" + page.getPageUuid() + " in space "
                    + spaceUname + ".Error: ", e);
        } catch (RepositoryTiemoutExcetpion e) {
            log.error("Merge attachment saving page:" + page.getPageUuid() + " in space " + spaceUname + ".Error: ",
                    e);
        }

        List<RenderPiece> pieces = renderService.renderHTML(page);
        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        //update page links
        Set<PageLink> links = page.getLinks();
        if (links == null) {
            links = new HashSet<PageLink>();
            page.setLinks(links);
        }

        List<PageLink> newLinks = new ArrayList<PageLink>();
        for (RenderPiece object : pieces) {
            if (object instanceof LinkModel) {
                LinkModel ln = (LinkModel) object;
                //!!! Only linkToCreate and LinkToView support at moment(29/10/2008)
                if (ln.getType() == LinkModel.LINK_TO_CREATE_FLAG || ln.getType() == LinkModel.LINK_TO_VIEW_FLAG) {
                    if (StringUtils.length(ln.getLink()) > SharedConstants.TITLE_MAX_LEN) {
                        log.warn("Found invalid link(too long), skip it on PageLink table:" + ln.getLink()
                                + " on page " + newPageTitle);
                    } else {
                        PageLink link = PageLink.copyFrom(page, ln);
                        newLinks.add(link);
                    }
                }
            }
        }

        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        //update menu item for space
        //found other if current page has menuItem macro.

        MenuItem menuItem = null;
        //if this marked true, it will trigger the Shell update space request, so update Shell menu.
        page.setMenuUpdated(false);

        for (RenderPiece renderPiece : pieces) {
            if (renderPiece instanceof MacroModel
                    && MenuItemMacro.NAME.equalsIgnoreCase(((MacroModel) renderPiece).macroName)) {
                //copy value to MenuItem object
                menuItem = new MenuItem();
                HashMap<String, String> values = ((MacroModel) renderPiece).values;
                if (values != null) {
                    menuItem.setTitle(values.get(NameConstants.TITLE));
                    menuItem.setOrder(NumberUtils.toInt(values.get(NameConstants.ORDER)));
                    menuItem.setParent(values.get(NameConstants.PARENT_UUID));
                }
                menuItem.setPageTitle(page.getTitle());
                menuItem.setPageUuid(page.getPageUuid());
                //suppose only one menuItem in a page, if multiple, even also only use first of them.
                break;
            }
        }

        Set<MenuItem> menuItems = space.getSetting().getMenuItems();
        if (menuItem != null) {
            //update menu list in current space setting
            if (menuItems == null) {
                menuItems = new TreeSet<MenuItem>(new MenuItemComparator());
                space.getSetting().setMenuItems(menuItems);
            } else {
                //try to remove old value
                menuItems.remove(menuItem);
            }

            log.info("Menu item is add or update to page {}.", page.getPageUuid());
            menuItems.add(menuItem);
            settingService.saveOrUpdateSpaceSetting(space, space.getSetting());
            page.setMenuUpdated(true);
        } else if (menuItems != null) {
            //need check if menu item is deleted from page if it had. Try to remove it.
            if (menuItems.remove(new MenuItem(page.getPageUuid()))) {
                log.info("Menu item is removed from page {}.", page.getPageUuid());
                settingService.saveOrUpdateSpaceSetting(space, space.getSetting());
                page.setMenuUpdated(true);
            }
        }

        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        //merge new links and existed links
        //delete non-existed
        for (Iterator<PageLink> iter = links.iterator(); iter.hasNext();) {
            PageLink ln = iter.next();
            ln.setAmount(0);
            for (Iterator<PageLink> newIter = newLinks.iterator(); newIter.hasNext();) {
                PageLink nlnk = newIter.next();
                if (ln.equals(nlnk)) {
                    ln.setAmount(ln.getAmount() + 1);
                    newIter.remove();
                }
            }
            if (ln.getAmount() == 0) {
                iter.remove();
            }
        }
        if (newLinks.size() > 0) {
            ArrayList<PageLink> linksList = new ArrayList<PageLink>(links);
            //there some new added links
            int idx;
            for (PageLink newLnk : newLinks) {
                if ((idx = linksList.indexOf(newLnk)) != -1) {
                    PageLink ln = linksList.get(idx);
                    ln.setAmount(ln.getAmount() + 1);
                } else {
                    linksList.add(newLnk);
                }
            }
            links.clear();
            links.addAll(linksList);
        }

        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        //persistent
        page.setType(PageType.NONE_DRAFT);
        pageDAO.saveOrUpdate(page);

        //!!!NOTE: follow 3 lines code must after pageDAO.saveOrUpdate(),otherwise, if home page
        // is new page and contain link, method pageDAO.getCurrentByTitle() in LinkRenderHelper.exist() 
        //method will throw exception!!!(15/03/2007_my new car Honda Accord arrives in home:)
        if (pageValue.getNewPageType() == PageAttribute.NEW_HOMEPAGE) {
            space.setHomepage(page);
            spaceDAO.saveOrUpdate(space);
        }

        //update cache only when a new page created or page title updated
        if (needRefreshCache) {
            addPageCache(spaceUname, page);
        }
        refreshAncestors(spaceUname, page);

        //page title change so change all page which refer link to this page.
        //only title change,oldTitle is not null.
        if ((Global.AutoFixLinks & WikiConstants.AUTO_FIX_TITLE_CHANGE_LINK) > 0 && oldTitle != null) {
            String newTitle = page.getTitle();
            try {
                fixLinksToTitle(spaceUname, oldTitle, newTitle);
            } catch (Exception e) {
                log.error("Unable to fix page title change on other pages content.", e);
            }
        }

        //remove all draft whatever auto or manual
        removeDraftInternal(spaceUname, page.getPageUuid(), page.getModifier(), PageType.NONE_DRAFT, false);

        //MOVE to PageIndexInterceptor
        //      if(requireNotified)
        //         sendNodification(page);

        log.info("Page saved " + newPageTitle + " on space " + spaceUname + ". Page uid: " + newPageUid);

        PageEventListener[] listeners = eventContainer.getPageEventListeners(page.getPageUuid());
        if (listeners != null && listeners.length > 0) {
            log.info("Page saved event dispatching...");
            for (PageEventListener listener : listeners) {
                try {
                    listener.pageSaving(page.getPageUuid());
                } catch (PageEventHanderException e) {
                    log.error("Page saved event processed failed on " + listener.getClass().getName(), e);
                }
            }
        }
        return page;

    }

    public Draft saveDraft(User user, Draft pageValue, PageType type) throws PageException {
        String spaceUname = pageValue.getSpace().getUnixName();

        log.info("Draft saving for page " + pageValue.getTitle() + " on space " + spaceUname);
        Draft draft = null;
        User owner = WikiUtil.getUser(userReadingService);
        if (pageValue.getPageUuid() != null) {
            //check whether current user has draft for this page
            draft = draftDAO.getDraftByUuid(spaceUname, pageValue.getPageUuid(), owner, type);
        }

        if (draft != null) {
            //update current page with new value
            copyValueFromView(draft, pageValue);
            draft.setType(type);
            //         draft.setUnixName(WikiUtil.getPageUnixname(pageValue.getTitle()));
            WikiUtil.setTouchedInfo(userReadingService, draft);
            draftDAO.saveOrUpdate(draft);
        } else {
            //a new page first time save:
            draft = new Draft();
            copyValueFromView(draft, pageValue);
            Space space = spaceDAO.getByUname(spaceUname);
            draft.setSpace(space);
            draft.setType(type);
            //draft will remember current version, later, if it will save to Page, it also need do version check.
            draft.setVersion(pageValue.getVersion());
            //if there is page existed before draft first create, keep page uuid as page uuid!!! 
            if (StringUtils.isBlank(pageValue.getPageUuid()))
                draft.setPageUuid(WikiUtil.createPageUuid(spaceUname, spaceUname, spaceUname, repositoryService));
            else
                draft.setPageUuid(pageValue.getPageUuid());
            //         draft.setUnixName(WikiUtil.getPageUnixname(pageValue.getTitle()));
            WikiUtil.setTouchedInfo(userReadingService, draft);
            //root page, such as home page
            if (pageValue.getParent() != null && pageValue.getParent().getPageUuid() != null) {
                Page parentPage = pageDAO.getCurrentByUuid(pageValue.getParent().getPageUuid());
                if (parentPage != null) {
                    draft.setParent(parentPage);
                    draft.setLevel(parentPage.getLevel() + 1);
                } else {
                    log.warn("Draft page parent page does not exist. Draft title is " + pageValue.getTitle()
                            + ". Parent page uuid is " + pageValue.getParent().getPageUuid());
                }
            } else
                draft.setLevel(0);
            //          Since version 0.3, page, draft are split into 2 tables. So tags, many-to-many relation,  becomes complex. 
            //and it is most useless, if saving tag also with draft, so just comment this piece so far.
            //         Set<PageTag> tags = draft.getTags();
            //         if(tags == null){
            //            tags = new HashSet<PageTag>();
            //            draft.setTags(tags);
            //         }
            //         Set<PageTag> newTags = pageValue.getTags();
            //         if(newTags != null){
            //            for (PageTag tag : newTags) {
            //               PageTag t = pageTagDAO.getByName(spaceUname,tag.getName());
            //               if(t == null){
            //                  WikiUtil.setTouchedInfo(userService, tag);
            //                  tag.setSpace(space);
            //                  pageTagDAO.saveOrUpdate(tag);
            //                  tags.add(tag);
            //               }else{
            //                  tags.add(t);
            //               }
            //            }
            //         }
            if (draft.getPageProgress() != null)
                pageProgressDAO.saveOrUpdate(draft.getPageProgress());
            draftDAO.saveOrUpdate(draft);
        }
        //manual drafted saved, auto draft removed. But in reverse, auto draft can not replace manual draft anyway.
        try {
            //IMPORTANT: can not re-order following 3 methods
            mergeAttahment(getPageAttachment(spaceUname, draft.getPageUuid(), true, true, owner),
                    pageValue.getAttachments(), spaceUname, owner, type);
            if (type == PageType.MANUAL_DRAFT) {
                upgradeAttachmentStatus(spaceUname, draft.getPageUuid(), draft.getModifier(),
                        PageType.MANUAL_DRAFT);
                removeDraftInternal(spaceUname, draft.getPageUuid(), draft.getModifier(), PageType.AUTO_DRAFT,
                        false);
            }
        } catch (RepositoryException e) {
            //not critical exception, just log:
            log.error("Remove draft failed during saving draft:" + draft.getPageUuid() + " in space " + spaceUname
                    + ".Error ", e);
        } catch (RepositoryTiemoutExcetpion e) {
            log.error("Merge draft failed during saving draft:" + draft.getPageUuid() + " in space " + spaceUname
                    + ".Error ", e);
        }

        //TODO: need confirm draft's permission setting.
        renderService.renderHTML(draft);
        //does not need set pagelink

        log.info("Draft saved for page " + pageValue.getTitle() + " on space " + spaceUname + ". Page uid: "
                + draft.getUid());
        return draft;
    }

    public List<History> getHistoryPages(String spaceUname, String pageUuid, int startVer, int returnCount,
            Date touchedDate) {

        return historyDAO.getByUuid(pageUuid, startVer, returnCount, touchedDate);
    }

    //JDK1.6 @Override
    public boolean existCurrentPageByTitle(String spaceUname, String pageTitle) {
        if (StringUtils.isBlank(pageTitle) || StringUtils.isBlank(spaceUname))
            return false;

        return pageDAO.getCurrentPageByTitle(spaceUname, pageTitle) != null;
    }

    /* (non-Javadoc)
     * @see com.edgenius.wiki.service.PageService#getPageByUnixName(java.lang.String)
     */
    public Page getCurrentPageByTitleWithoutSecurity(String spaceUname, String pageTitle, boolean render)
            throws SpaceNotFoundException {
        if (StringUtils.isBlank(pageTitle) || StringUtils.isBlank(spaceUname))
            return null;
        Page page = pageDAO.getCurrentPageByTitle(spaceUname, pageTitle);
        //page not found
        if (page == null) {
            log.error("Page not found by spaceUname (" + spaceUname + ") and title (" + pageTitle + ")");
            //need check if space exist, if space is available just return null, it means "Page Not Found" page will show 
            //and allow user to create new page if he/she has permission. If even the space does not exist, then just throw 
            //exception
            if (spaceDAO.getByUname(spaceUname) == null) {
                throw new SpaceNotFoundException(spaceUname);
            }
            return null;
        }
        if (render) {
            renderService.renderHTML(page);
            refreshAncestors(spaceUname, page);
        }
        return page;
    }

    public Page getCurrentPageByTitle(String spaceUname, String pageTitle) throws SpaceNotFoundException {
        return getCurrentPageByTitleWithoutSecurity(spaceUname, pageTitle, true);
    }

    public Page getCurrentPageByUnixName(String spaceUname, String unixName) {
        return pageDAO.getCurrentByUnixName(spaceUname, unixName);
    }

    public Page getCurrentPageByUuid(String pageUuid, boolean render) {
        if (!render) {
            return pageDAO.getCurrentByUuid(pageUuid);
        } else {
            Page page = pageDAO.getCurrentByUuid(pageUuid);
            if (page != null) {
                String spaceUname = page.getSpace().getUnixName();
                renderService.renderHTML(page);
                refreshAncestors(spaceUname, page);
            }
            return page;
        }
    }

    public Page getCurrentPageByUuid(String pageUuid) {
        return pageDAO.getCurrentByUuid(pageUuid);
    }

    /* 
     * @see com.edgenius.wiki.service.PageService#getPage(java.lang.Integer)
     */
    public Page getPage(Integer pageUid) {
        return pageDAO.get(pageUid);
    }

    public History getHistoryObject(Integer historyUid) {
        if (historyUid == null)
            return null;

        return historyDAO.get(historyUid);
    }

    public History getHistory(Integer historyUid) {

        History history = historyDAO.get(historyUid);
        String spaceUname = history.getSpace().getUnixName();
        renderService.renderHTML(history);

        refreshAncestors(spaceUname, history);
        return history;
    }

    public History getHistoryByVersion(String pageUuid, Integer version) {
        return historyDAO.getVersionByUuid(version, pageUuid);
    }

    public List<AbstractPage> getPageAncestors(String spaceUname, String pageUuid) {
        if (StringUtils.isBlank(pageUuid)) {
            //could be home page,
            return null;
        }
        Page page = pageDAO.getCurrentByUuid(pageUuid);
        if (page == null) {
            log.warn("Unable find page according to uuid " + pageUuid + " when try to get page ancestors.");
            return null;
        }

        refreshAncestors(spaceUname, page);
        return page.getAncestorList();
    }

    public Draft getDraft(User user, Integer draftUid) {
        if (draftUid == null)
            return null;

        Draft draft = draftDAO.get(draftUid);
        if (draft != null) {
            String spaceUname = draft.getSpace().getUnixName();
            refreshAncestors(spaceUname, draft);
        }
        return draft;

    }

    public Page getHomepage(String spaceUname) {
        Space space = spaceDAO.getByUname(spaceUname);
        if (space == null) {
            log.warn("Try get homepage from non-exist space failed {}", spaceUname);
            return null;
        }

        Page page = space.getHomepage();
        //home page deleted!
        if (page == null)
            return page;

        renderService.renderHTML(page);

        //refresh navbar info:only home page itself
        List<AbstractPage> parentList = new ArrayList<AbstractPage>();
        parentList.add(page);
        page.setAncestorList(parentList);

        return page;
    }

    public List<FileNode> uploadAttachments(String spaceUname, String pageUuid, List<FileNode> files,
            boolean compareMd5Digest)
            throws RepositoryException, RepositoryTiemoutExcetpion, RepositoryQuotaException {
        ITicket ticket = repositoryService.login(spaceUname, spaceUname, spaceUname);

        List<FileNode> checkedFiles = new ArrayList<FileNode>();
        for (FileNode file : files) {
            //this method also update file.setNodeUuid(); and setVersion()
            //doesn't force save if compareMd5Digest is true and MD5 digests are equals - return null if digests are equals.
            List<FileNode> nodes = repositoryService.saveFile(ticket, file, compareMd5Digest, compareMd5Digest);
            if (nodes != null) {
                User user = userReadingService.getUserByName(file.getCreateor());
                //normally, it only has one except bulk upload....
                for (FileNode node : nodes) {
                    node.setUserFullname(user.getFullname());
                    checkedFiles.add(node);
                }
            }
        }
        if (checkedFiles.size() > 0)
            touchService.touchPage(pageUuid);

        return checkedFiles;
    }

    public FileNode removeAttachment(String spaceUname, String pageUuid, String nodeUuid, String nodeVersion)
            throws RepositoryException, RepositoryTiemoutExcetpion {

        ITicket ticket = repositoryService.login(spaceUname, spaceUname, spaceUname);
        FileNode node = repositoryService.removeFile(ticket, nodeUuid, nodeVersion);
        touchService.touchPage(pageUuid);

        return node;
    }

    @SuppressWarnings("unchecked")
    public List<Page> getPageTree(String spaceUname) {
        if (Space.SYSTEM_SPACEUNAME.equals(spaceUname)) {
            //for system space, return empty.
            return new ArrayList<Page>();
        }

        Element element = pageTreeCache.get(spaceUname);
        if (element == null) {
            Set<Page> sortedSet = new TreeSet<Page>();
            List<Page> list = pageDAO.getTree(spaceUname);
            log.info("Init page tree for " + spaceUname + " in size " + list.size());

            if (list != null && list.size() > 0) {
                //because parent of page on list only has Uid, PageComparator() need parent title, uuid refer link. 
                //here to build up page's parent refer link(parent's parent's parent etc.)
                Space spaceModel = new Space();
                spaceModel.setUnixName(spaceUname);
                for (Page page : list) {
                    //fill spaceUname
                    page.setSpace(spaceModel);

                    if (page.getParent() == null)
                        continue;
                    Integer pUid = page.getParent().getUid();
                    //loop all page and find parent.
                    boolean found = false;
                    for (Page parent : list) {
                        if (parent == page)
                            continue;
                        if (parent.getUid().equals(pUid)) {
                            page.setParent(parent);
                            found = true;
                            break;
                        }
                    }
                    if (!found)
                        AuditLogger.error("Date integrity error. Page has parent is not in space " + spaceUname
                                + " .PageUid: " + page.getUid() + ". ParentUid: " + pUid);
                }
                //parent refer link built, now PageComparator() works
                //always put homepage in the first node, so check here
                Space space = spaceDAO.getByUname(spaceUname);
                Page home = space.getHomepage();
                sortedSet = new TreeSet<Page>(new PageComparator(home != null ? home.getPageUuid() : null));
                sortedSet.addAll(list);

                log.info("Page tree " + spaceUname + " put into cache " + sortedSet.size());
                //put into cache
                element = new Element(spaceUname, sortedSet);
                pageTreeCache.put(element);
            }
            return list;
        } else
            return new ArrayList<Page>((Set<Page>) element.getValue());

    }

    /*
     * Copy page, if there is same title page exist in target space(maybe in same space). A new title will given (Copy of ...).
     */
    public Page copy(String fromSpaceUname, String fromPageUuid, String toSpaceUname, String toParentUuid,
            boolean withChildren) throws PageException {
        Page page = pageDAO.getCurrentByUuid(fromPageUuid);
        if (page == null) {
            log.error("Unable find page for copy " + fromPageUuid);
            return null;
        }
        if (page.isRemoved()) {
            log.error("User try to copy from a removed page " + page.getTitle() + " on space "
                    + page.getSpace().getUnixName());
            return null;
        }

        Integer fromPageUid = page.getUid();

        Page toParent = null;
        if (toParentUuid != null) {
            toParent = pageDAO.getCurrentByUuid(toParentUuid);
            if (!StringUtils.equalsIgnoreCase(toParent.getSpace().getUnixName(), toSpaceUname)) {
                log.warn("Target space and page parent are different spaces. Set parent as null(root)");
                log.warn("To SpaceUname:" + toSpaceUname + ". To ParentUuid:" + toParentUuid + " of space "
                        + toParent.getSpace().getUnixName());
                toParent = null;
            }
        }

        if (!StringUtils.equalsIgnoreCase(page.getSpace().getUnixName(), fromSpaceUname)) {
            String error = "Given space name is inconsistent with pageUuid during copy."
                    + page.getSpace().getUnixName() + " is pageUuid's space, but given is " + fromSpaceUname;
            log.error(error);
            throw new PageException(error);
        }

        log.info("Page " + fromPageUuid + " is going to copy from space " + fromSpaceUname + " to " + toSpaceUname
                + (toParent == null ? " root node" : ", parent title is " + toParent.getTitle()));

        //copy to different space
        //TODO: avoid copy to its owned children, deadloop, same with move.

        Page newPage = (Page) page.clone();
        newPage.setUid(null);
        if (newPage.getContent() != null)
            newPage.getContent().setUid(null);

        String newUuid = WikiUtil.createPageUuid(toSpaceUname, toSpaceUname, toSpaceUname, repositoryService);
        Space toSpace = spaceDAO.getByUname(toSpaceUname);
        //set new space, progress etc.
        newPage.setSpace(toSpace);
        newPage.setPageUuid(newUuid);
        //            TODO: how to handle draft? some use won't have permission to target space 
        //            if(newPage.isDraft())
        String title = page.getTitle();
        title = getIdeniticalTitle(toSpaceUname, title, "Copy of ");
        newPage.setTitle(title);

        newPage.setParent(toParent);
        newPage.setLevel(toParent == null ? 0 : toParent.getLevel() + 1);

        //try to duplicated tags
        List<PageTag> tags = newPage.getTags();
        List<PageTag> newTags = new ArrayList<PageTag>();
        if (tags != null && tags.size() > 0) {
            if (StringUtils.equalsIgnoreCase(toSpaceUname, fromSpaceUname)) {
                //same space, tag already exist, then just use persistent tag replace transient tag
                //during clone: tag Uid is removed, here just simply copy back original tag
                newPage.setTags(page.getTags());
            } else {
                //different spaces, need check tag one by one, if not exist, create new one
                for (PageTag tag : tags) {
                    PageTag pTag = tagService.getPageTagByName(toSpaceUname, tag.getName());
                    if (pTag != null) {
                        newTags.add(pTag);
                    } else {
                        //create new
                        tag.setUid(null);
                        WikiUtil.setTouchedInfo(userReadingService, tag);
                        tag.setSpace(toSpace);
                        tag.setPages(null);
                        tagService.internalSavePageTag(tag);
                        newTags.add(tag);
                    }
                }
                newPage.setTags(newTags);
            }
        }
        try {
            //copy attachment 
            ITicket fromTicket = repositoryService.login(fromSpaceUname, fromSpaceUname, fromSpaceUname);
            ITicket toTicket = repositoryService.login(toSpaceUname, toSpaceUname, toSpaceUname);
            repositoryService.copy(fromTicket, RepositoryService.TYPE_ATTACHMENT, fromPageUuid, toTicket,
                    RepositoryService.TYPE_ATTACHMENT, newPage.getPageUuid());
        } catch (RepositoryException e) {
            throw new PageException("Copy page failed on exception: " + e);
        } catch (RepositoryTiemoutExcetpion e) {
            throw new PageException("Copy page failed on exception: " + e);
        }

        //copy its children if require
        if (withChildren) {
            List<Page> children = pageDAO.getChildren(page.getUid());
            for (Page child : children) {
                //recursive call to retrieve all child to copy
                copy(fromSpaceUname, child.getPageUuid(), toSpaceUname, page.getPageUuid(), withChildren);
            }
        }

        //need persist page progress first, which is clone from old page but with null uid.
        PageProgress pageProgress = newPage.getPageProgress();
        pageProgress.setUid(null);
        pageProgressDAO.saveOrUpdate(pageProgress);

        //update page links if space is different
        if ((Global.AutoFixLinks & WikiConstants.AUTO_FIX_COPY_LINK) > 0
                && !StringUtils.equals(fromSpaceUname, toSpaceUname)) {
            try {
                fixLinksToSpace(newPage, fromSpaceUname, toSpaceUname);
            } catch (Exception e) {
                log.error("Unable to fix page space change on other pages content.", e);
            }
        }
        Set<PageLink> links = newPage.getLinks();
        if (links != null && links.size() > 0) {
            //any pageLink will create a new record in database
            for (PageLink link : links) {
                link.setUid(null);
            }
        }
        //persist
        WikiUtil.setTouchedInfo(userReadingService, newPage);
        pageDAO.saveOrUpdate(newPage);
        //only save current page, skip history 
        addPageCache(toSpaceUname, newPage);

        commentService.copyComments(fromPageUid, newPage);
        //copy history
        List<History> histories = historyDAO.getByUuid(page.getPageUuid());
        if (histories != null) {
            for (History history : histories) {
                //if not current page(page histories), simply copy history page : no attachment, no children handle
                History newHistory = (History) history.clone();
                newHistory.setUid(null);
                if (newHistory.getContent() != null)
                    newHistory.getContent().setUid(null);
                //set new space, progress etc.
                newHistory.setSpace(toSpace);
                newHistory.setPageUuid(newUuid);
                WikiUtil.setTouchedInfo(userReadingService, newPage);
                historyDAO.saveOrUpdate(newHistory);
            }
        }

        //DON'T Copy favorite/watch: permission is problem
        //permission need be checked : if this user allow access that space?
        //            List<UserPageMark> favorites = userPageDAO.getByPageUid(page.getUid());
        //            if(favorites != null){
        //               for (UserPageMark favorite : favorites) {
        //                  favorite.getUser()
        //                  favorite.setUid(null);
        //                  favorite.setPage(newPage);
        //                  userPageDAO.saveOrUpdate(favorite);
        //               }
        //            }

        //do all necessary things for a new page
        renderService.renderHTML(newPage);
        refreshAncestors(toSpaceUname, newPage);
        return newPage;
    }

    /*
     * If in same space, only update current page parent(history pages' parent is null). Otherwise, do copy then remove.
     */
    public Page move(String fromSpaceUname, String fromPageUuid, String toSpaceUname, String toParentUuid,
            boolean withChildren) throws PageException {
        Page currPage = pageDAO.getCurrentByUuid(fromPageUuid);
        if (currPage == null) {
            log.error("Unable find page for move " + fromPageUuid);
            return null;
        }
        if (currPage.isRemoved()) {
            log.error("User try to move from a removed page " + currPage.getTitle() + " on space "
                    + currPage.getSpace().getUnixName());
            return null;
        }

        Page toParent = null;
        if (toParentUuid != null) {
            toParent = pageDAO.getCurrentByUuid(toParentUuid);
            if (!StringUtils.equalsIgnoreCase(toParent.getSpace().getUnixName(), toSpaceUname)) {
                log.warn("Target space and page parent are different spaces. Set parent as null(root)");
                log.warn("To SpaceUname:" + toSpaceUname + ". To ParentUuid:" + toParentUuid + " of space "
                        + toParent.getSpace().getUnixName());
                toParent = null;
            }
        }

        if (!StringUtils.equalsIgnoreCase(currPage.getSpace().getUnixName(), fromSpaceUname)) {
            String error = "Given space name is inconsistent with pageUuid during moving page."
                    + currPage.getSpace().getUnixName() + " is pageUuid's space, but given is " + fromSpaceUname;
            log.error(error);
            throw new PageException(error);
        }

        log.info("Page UUID(" + fromPageUuid + ") is going to move from space " + fromSpaceUname + " to "
                + toSpaceUname);

        if (StringUtils.equalsIgnoreCase(toSpaceUname, fromSpaceUname)) {
            if (currPage.equals(toParent) || currPage.getParent().equals(toParent)) {
                //does not move to same parent or itself as parent case.
                log.info("Same space moving, either same parent or itself as parent , do nothing. Page title is "
                        + currPage.getTitle() + ". ToParrent title is "
                        + (toParent == null ? "" : toParent.getTitle()));
            } else {
                log.info("Same space moving inside " + toSpaceUname);
                log.info("Moving source page title is " + currPage.getTitle());
                Page oldParent = currPage.getParent();
                currPage.setParent(toParent);
                currPage.setLevel(toParent == null ? 0 : toParent.getLevel() + 1);
                pageDAO.saveOrUpdate(currPage);

                // reset all directly children (son/daughter:) parentUid to
                // removed page's parent
                List<Page> children = pageDAO.getChildren(currPage.getUid());
                for (Page child : children) {
                    removePageCache(toSpaceUname, child, true);
                    child.setParent(oldParent);
                    child.setLevel(oldParent == null ? 0 : oldParent.getLevel() + 1);
                    pageDAO.saveOrUpdate(child);
                    addPageCache(toSpaceUname, child);
                }

                //do all necessary things for a new page render: 
                //update pageTree cache, update ancestors, render page
                removePageCache(toSpaceUname, currPage, true);
                addPageCache(toSpaceUname, currPage);
            }
            //for render correctly:
            renderService.renderHTML(currPage);
            refreshAncestors(toSpaceUname, currPage);
        } else {
            currPage = copy(fromSpaceUname, fromPageUuid, toSpaceUname, toParentUuid, withChildren);
            removePage(fromSpaceUname, fromPageUuid, withChildren, true);
        }

        return currPage;
    }

    /*
     * Return removed page object
     * NOTE:withChildren case never be tested!!!! It assume not works.
     */
    public Page removePage(String spaceUname, String pageUuid, boolean withChildren, boolean permanent)
            throws PageException {
        //this will return all page with such uuid whatever it has removed mark or not
        Page page = pageDAO.getByUuid(pageUuid);
        if (page == null) {
            return null;
        }

        log.info("Page UUID({})-Title({}) is going to removed from space {}",
                new String[] { pageUuid, page.getTitle(), spaceUname });

        spaceUname = page.getSpace().getUnixName();

        // remove attachment
        if (permanent) {
            try {
                ITicket fromTicket = repositoryService.login(spaceUname, spaceUname, spaceUname);
                repositoryService.removeIdentifier(fromTicket, RepositoryService.TYPE_ATTACHMENT,
                        page.getPageUuid());
            } catch (RepositoryException e) {
                log.error("Remove page attachment failed ", e);
            } catch (RepositoryTiemoutExcetpion e) {
                log.error("Remove page attachment failed ", e);
            }
        }
        // remove its children if require
        List<Page> children = pageDAO.getChildren(page.getUid());
        if (withChildren) {
            for (Page child : children) {
                // recursive call to retrieve all child to copy
                removePage(spaceUname, child.getPageUuid(), withChildren, permanent);
            }
        } else {
            // reset all directly children (son/daughter:) parentUid to
            // removed page's parent
            Page parent = page.getParent();
            for (Page child : children) {
                // remove child from cache first, then add it back after
                // its parent update is done.
                removePageCache(spaceUname, child, true);
                child.setParent(parent);
                child.setLevel(parent == null ? 0 : parent.getLevel() + 1);
                pageDAO.saveOrUpdate(child);
                addPageCache(spaceUname, child);
            }
        }

        // check if this page is homepage, if so, reset space home page as null
        Space space = page.getSpace();
        Page home = space.getHomepage();
        boolean removeHomepage = false;
        if (home != null && StringUtils.equals(page.getPageUuid(), home.getPageUuid())) {
            space.setHomepage(null);
            spaceDAO.saveOrUpdate(space);
            removeHomepage = true;
            log.info("Space home page removed: " + spaceUname);
        }

        if (!permanent) {
            if (removeHomepage)
                page.setRemoved(Page.REMOVED_HOMEPAGE);
            else
                page.setRemoved(Page.REMOVED);
            pageDAO.saveOrUpdate(page);
            // try to remove this page from cache: because the page.isRemoved() return true, it will removed from Cache.
            addPageCache(spaceUname, page);
        } else {
            page.setParent(null);
            //remove comments:must before pageDAO.removeObject().
            commentService.removePageComments(page.getUid());

            WikiUtil.setTouchedInfo(userReadingService, page);
            PageProgress progress = page.getPageProgress();
            pageDAO.removeObject(page);

            //after pageDAO.removeObject(page);
            if (progress != null)
                pageProgressDAO.removeObject(progress);

            //remove page history
            List<History> histories = historyDAO.getByUuid(page.getPageUuid());
            if (histories != null) {
                for (History history : histories) {
                    historyDAO.removeObject(history);
                }
            }

        }
        // remove UserPageMark:favorite and watch
        userPageDAO.removeByPageUid(page.getUid());

        log.info("Page removed successed. Title  " + page.getTitle());

        // this page exist in space, already removed, need remove from cache as well
        removePageCache(spaceUname, page, true);
        securityService.removeResource(pageUuid);

        PageEventListener[] listeners = eventContainer.getPageEventListeners(page.getPageUuid());
        if (listeners != null && listeners.length > 0) {
            log.info("Page saved event dispatching...");
            for (PageEventListener listener : listeners) {
                try {
                    listener.pageRemoving(page.getPageUuid(), permanent);
                } catch (PageEventHanderException e) {
                    log.error("Page saved event processed failed on " + listener.getClass().getName(), e);
                }
            }
        }

        return page;

    }

    public String restorePageCheck(String restoredPageUuid) {
        //this will return page whatever if it is marked as removed
        Page page = pageDAO.getByUuid(restoredPageUuid);

        //if any unexpected case, just silence return true, ask restorePage() to throw exception.
        if (page == null)
            return null;

        if (!page.isRemoved()) {
            return null;
        }

        String title = page.getTitle();
        //try to get an new title if space already has same title page exist
        title = getIdeniticalTitle(page.getSpace().getUnixName(), title, "Restored of ");

        int status = SharedConstants.RESTORE_NORMAL;
        if (page.getRemoved() == Page.REMOVED_HOMEPAGE) {
            //OK, the resorted page was home page, need check if current homepage has created again?
            Page homepage = page.getSpace().getHomepage();
            if (homepage != null) {
                status = SharedConstants.RESTORE_HOMEPAGE_EXIST;
            } else {
                status = SharedConstants.RESTORE_HOMEPAGE_NO_EXIST;
            }
        }
        return status + title;

    }

    public Page restorePage(String spaceUname, String pageUuid, boolean homepage, boolean withHistory)
            throws PageException {
        //this will return all page with such uuid whatever it has removed mark or not
        Page page = pageDAO.getByUuid(pageUuid);
        if (page == null)
            return null;

        log.info("Page UUID(" + pageUuid + ") is going to restore from space " + spaceUname);

        if (!page.isRemoved()) {
            String msg = "page was not removed cleanly. Page " + page.getPageUuid() + "  version "
                    + page.getVersion() + " restore failed";
            log.error(msg);
            throw new PageException(msg);
        }
        // need check if the restore page parent if exist or not, if no, just simple set as null
        //try to get available parent, if failed, then set it as null. This parent will update to all version of this page.
        Page existParent = null;
        Page parent = page.getParent();
        if (parent != null) {
            existParent = pageDAO.getCurrentByUuid(parent.getPageUuid());
            if (existParent == null) {
                log.info("Restored page original parent does not exist. The original parent page UUID is "
                        + parent.getPageUuid());
            }
        }

        page.setParent(existParent);
        WikiUtil.setTouchedInfo(userReadingService, page);
        String title = page.getTitle();
        title = getIdeniticalTitle(spaceUname, title, "Restored of ");
        if (title != null)
            page.setTitle(title);
        else {
            AuditLogger.error("Unexpected case: can not get back page title in restore.");
            throw new PageException("Unexpected case: can not get back page title in restore.");
        }

        //MUST: setRemoved() after title check, otherwise, the title should exist because it marked itself removed as false.
        page.setRemoved(Page.REMOVE_FLAG_NO);
        pageDAO.saveOrUpdate(page);

        if (!withHistory) {
            //remove all history if it need not histories
            List<History> histories = historyDAO.getByUuid(page.getPageUuid());
            for (History history : histories) {
                historyDAO.removeObject(history);
            }
        }
        if (homepage) {
            //restore page is home page, need update space object to link
            Space space = spaceDAO.getByUname(spaceUname);
            space.setHomepage(page);
            spaceDAO.saveOrUpdate(space);

        }

        log.info("Page retore successed. Page uuid " + page.getPageUuid() + " Page version " + page.getVersion());
        // well, update page cache
        addPageCache(spaceUname, page);
        return page;

    }

    //JDK1.6 @Override
    public Page restoreHistory(String spaceUname, String currPageUuid, int version)
            throws PageException, DuplicatedPageException, PageSaveTiemoutExcetpion {

        Page page = pageDAO.getCurrentByUuid(currPageUuid);
        if (page == null) {
            throw new PageException("Page not found for uuid " + currPageUuid + " on space " + spaceUname);
        }
        History history = historyDAO.getVersionByUuid(version, page.getPageUuid());
        if (history == null) {
            throw new PageException("Histroy not found for verison " + version + ". Page current title "
                    + page.getTitle() + " on space " + spaceUname);
        }

        Page newPage = null;
        try {
            newPage = history.cloneToPage();
            //OK, to avoid duplicated title exception, use current page title to replace history one.
            newPage.setTitle(page.getTitle());

            //this is not very necessary, as for performance reason, give Uid, then in savePage()method will use key to get page...
            newPage.setUid(page.getUid());
            newPage = savePage(newPage, WikiConstants.NOTIFY_NONE, true);
        } catch (VersionConflictException e) {
            //this exception won't happen as checkVersion flag is false
            throw new PageException(e);
        }
        return newPage;
    }

    public List<UserPageMark> getPageMarks(User user, Page page) {

        return userPageDAO.getByUserAndPage(user, page);
    }

    public List<Draft> hasDraft(String spaceUname, String pageTitle, User owner) {

        return draftDAO.hasDraftByTitle(spaceUname, pageTitle, owner);
    }

    public Draft removeDraft(User user, String spaceUname, String pageUuid, PageType type) throws PageException {
        return removeDraftInternal(spaceUname, pageUuid, user, type, true);
    }

    public List<Draft> getDraftPages(User user, PageType type) {
        return draftDAO.getDrafts(null, user.getUsername(), type);
    }

    public List<Page> getFavoritePages(String username) {
        List<Page> pages = new ArrayList<Page>();
        List<UserPageMark> marks = userPageDAO.getFavorites(null, username);
        if (marks != null)
            for (UserPageMark mark : marks) {
                pages.add(mark.getPage());
            }
        return pages;
    }

    public List<Page> getWatchedPages(String username) {
        List<Page> pages = new ArrayList<Page>();
        List<UserPageMark> marks = userPageDAO.getWatched(null, username);
        if (marks != null)
            for (UserPageMark mark : marks) {
                pages.add(mark.getPage());
            }
        return pages;
    }

    @Transactional(readOnly = true)
    public List<Page> getPinTopPages(Integer spaceUid, String spaceUname, User viewer) {
        List<Page> list = pageDAO.getPinTopPagesInSpace(spaceUid);

        if (list != null && viewer != null) {
            //filter out the page which is not allow viewer to read
            for (Iterator<Page> iter = list.iterator(); iter.hasNext();) {
                Page page = iter.next();
                if (!securityService.isAllowPageReading(page.getSpace().getUnixName(), page.getPageUuid(), viewer))
                    iter.remove();
            }
        }

        return list;
    }

    public List<Page> getUserUpdatedPagesInSpace(String spaceUname, String username, int returnNum, User viewer) {
        User user = userReadingService.getUserByName(username);
        List<Page> list = pageDAO.getUserUpdatedPagesInSpace(spaceUname, user, returnNum);

        if (list != null && viewer != null) {
            //filter out the page which is not allow viewer to read
            for (Iterator<Page> iter = list.iterator(); iter.hasNext();) {
                if (!securityService.isAllowPageReading(spaceUname, iter.next().getPageUuid(), viewer))
                    iter.remove();
            }
        }
        return list;
    }

    public List<Page> getUserAllContributedPages(String username, int limit, User viewer) {
        User user = userReadingService.getUserByName(username);
        List<Page> list = pageDAO.getUserContributedPages(user, limit);
        if (list != null && viewer != null) {
            //filter out the page which is not allow viewer to read
            for (Iterator<Page> iter = list.iterator(); iter.hasNext();) {
                Page page = iter.next();
                if (!securityService.isAllowPageReading(page.getSpace().getUnixName(), page.getPageUuid(), viewer))
                    iter.remove();
            }
        }

        return list;

    }

    public List<History> getUserAllContributedHistories(String username, User viewer) {
        User user = userReadingService.getUserByName(username);
        List<History> list = historyDAO.getUserContributedHistories(user);
        if (list != null && viewer != null) {
            //filter out the page which is not allow viewer to read
            for (Iterator<History> iter = list.iterator(); iter.hasNext();) {
                History page = iter.next();
                if (!securityService.isAllowPageReading(page.getSpace().getUnixName(), page.getPageUuid(), viewer))
                    iter.remove();
            }
        }

        return list;

    }

    //JDK1.6 @Override
    public List<Page> getPagesInSpace(String spaceUname, Date touchedDate, int returnNum, User viewer) {
        List<Page> list = pageDAO.getPagesInSpace(spaceUname, touchedDate, returnNum);
        if (list != null && viewer != null) {
            //filter out the page which is not allow viewer to read
            for (Iterator<Page> iter = list.iterator(); iter.hasNext();) {
                Page page = iter.next();
                if (!securityService.isAllowPageReading(page.getSpace().getUnixName(), page.getPageUuid(), viewer))
                    iter.remove();
            }
        }
        return list;
    }

    //JDK1.6 @Override
    public List<String> getPagesUuidInSpace(String spaceUname, User viewer) {
        //TODO: will get from TreeCache or reading cache?
        List<String> list = pageDAO.getPagesUuidInSpace(spaceUname);
        if (list != null && viewer != null) {
            //filter out the page which is not allow viewer to read
            for (Iterator<String> iter = list.iterator(); iter.hasNext();) {
                String uuid = iter.next();
                if (!securityService.isAllowPageReading(spaceUname, uuid, viewer))
                    iter.remove();
            }
        }
        return list;
    }

    /**
     * Get page method will call this method to fill attachment.
     * Save/saveDraft does not call, because it is possible new attachment submit once after save/savedraft done.
     * E.g, when user choose several upload files and click save button, or, pageUuid is null, attachment auto saving timer
     * will call saveDraft() first.
     * 
     * @param pageUuid
     * @return
     * @throws RepositoryException 
     */
    public List<FileNode> getPageAttachment(String spaceUname, String pageUuid, boolean withHistory,
            boolean withDraft, User viewer) throws RepositoryException {
        //just compare nodeUuid(file name could change, cannot comparable), if same, compare nodeVersion then.
        Set<FileNode> set = new TreeSet<FileNode>(new Comparator<FileNode>() {
            public int compare(FileNode o1, FileNode o2) {
                if (o1.getNodeUuid().equals(o2.getNodeUuid())) {
                    //from large to small
                    return o2.getVersion().compareTo(o1.getVersion());
                } else
                    return o1.getNodeUuid().compareTo(o2.getNodeUuid());
            }

        });
        ITicket ticket = repositoryService.login(spaceUname, spaceUname, spaceUname);
        //don't get file stream, only get necessary description information
        List<FileNode> atts = repositoryService.getAllIdentifierNodes(ticket, RepositoryService.TYPE_ATTACHMENT,
                pageUuid, false);

        for (FileNode node : atts) {
            if (node.getStatus() > 0) {
                if (!withDraft) {
                    continue;
                } else if (viewer == null
                        || !StringUtils.equalsIgnoreCase(node.getCreateor(), viewer.getUsername())) {
                    //viewer is anonymous or the attachment is uploaded by this viewer, skip
                    continue;
                }
            }
            String username = node.getCreateor();
            User user = userReadingService.getUserByName(username);
            //pass back user fullname
            node.setUserFullname(user.getFullname());
            set.add(node);
        }
        if (!withHistory) {
            //remove history version
            List<String> nodeUuids = new ArrayList<String>();
            for (Iterator<FileNode> iter = set.iterator(); iter.hasNext();) {
                FileNode node = iter.next();
                String uuid = node.getNodeUuid();
                if (nodeUuids.contains(uuid)) {
                    iter.remove();
                    continue;
                }
                nodeUuids.add(uuid);
            }
        }
        return new ArrayList<FileNode>(set);
    }

    public FileNode updateAttachmentMetaData(String spaceUname, String pageUuid, String nodeUuid, String name,
            String desc) throws RepositoryException {

        ITicket ticket = repositoryService.login(spaceUname, spaceUname, spaceUname);
        return repositoryService.updateMetaData(ticket, nodeUuid, name, desc);
    }

    //JDK1.6 @Override
    public long getUserAuthoredPageSize(String username) {

        return pageDAO.getUserAuthoredSize(username);
    }

    //JDK1.6 @Override
    public long getUserModifiedPageSize(String username) {
        return pageDAO.getUserModifiedSize(username);
    }

    //JDK1.6 @Override
    public Set<User> getPageContributors(String pageUuid) {
        Set<User> contributors = new HashSet<User>();
        Page page = pageDAO.getCurrentByUuid(pageUuid);

        User user = page.getCreator() == null ? userReadingService.getUser(-1) : page.getCreator();
        contributors.add(user);
        user = page.getModifier() == null ? userReadingService.getUser(-1) : page.getModifier();
        contributors.add(user);

        List<History> histories = historyDAO.getByUuid(pageUuid);
        if (histories != null) {
            for (History history : histories) {
                user = history.getCreator() == null ? userReadingService.getUser(-1) : history.getCreator();
                contributors.add(user);
                user = history.getModifier() == null ? userReadingService.getUser(-1) : history.getModifier();
                contributors.add(user);
            }
        }
        return contributors;
    }

    //JDK1.6 @Override
    public List<Page> getPageChildren(String pageUuid) {
        Page page = pageDAO.getCurrentByUuid(pageUuid);
        if (page != null)
            return pageDAO.getChildren(page.getUid());

        return null;
    }

    public Integer getPageChildrenCount(String pageUuid) {
        Page page = pageDAO.getCurrentByUuid(pageUuid);
        if (page != null)
            return pageDAO.getChildrenCount(page.getUid());

        return 0;
    }

    /*
     * This method does not consider concurrent issue - as this is not critcial task.
     */
    @SuppressWarnings("unchecked")
    public void startEditing(String pageUuid, User user) {

        ConcurrentMap<String, Long> map = null;

        Element ele = pageEditingCache.get(pageUuid);
        if (ele != null) {
            //if ele is not null, then keep existed editing users list
            //this maybe cause users not expired in 1 hour(echache.xml setting), but it is not harmful
            pageEditingCache.remove(pageUuid);
            map = (ConcurrentMap<String, Long>) ele.getValue();
        }
        if (map == null)
            map = new ConcurrentHashMap<String, Long>();

        map.put(user.getUsername(), System.currentTimeMillis());

        ele = new Element(pageUuid, map);
        pageEditingCache.put(ele);
    }

    @SuppressWarnings("unchecked")
    public void stopEditing(String pageUuid, User user) {
        Element ele = pageEditingCache.get(pageUuid);
        if (ele != null) {
            ConcurrentMap<String, Long> map = (ConcurrentMap<String, Long>) ele.getValue();
            if (map != null)
                map.remove(user.getUsername());
            if (map == null || map.size() == 0)
                pageEditingCache.remove(pageUuid);
        }
    }

    public Map<String, Long> isEditing(String pageUuid) {
        Element ele = pageEditingCache.get(pageUuid);
        if (ele != null)
            return (ConcurrentMap<String, Long>) ele.getValue();

        return null;
    }

    public boolean markPageFlag(int type, String pageUuid, String username, boolean add) {
        User user = userReadingService.getUserByName(username);
        if (user == null)
            return false;

        Page page = pageDAO.getCurrentByUuid(pageUuid);
        if (page == null)
            return false;

        UserPageMark up = new UserPageMark();
        up.setPage(page);
        up.setUser(user);
        up.setType(type);
        up.setCreatedDate(new Date());

        UserPageMark persistMark = userPageDAO.getByObject(up);
        if (add) {
            if (persistMark == null) {
                //don't add duplicated
                userPageDAO.saveOrUpdate(up);
            }
        } else {
            if (persistMark != null) {
                userPageDAO.removeObject(persistMark);
            }
        }

        return true;
    }

    public Page getPageByExtLinkID(String spaceUname, String extLinkID) {
        return pageDAO.getPageByProgressExtLinkID(spaceUname, extLinkID);
    }

    public void saveOrUpdatePageProgress(PageProgress pageProgress) {
        pageProgressDAO.saveOrUpdate(pageProgress);
    }

    @Override
    public List<Page> getPageForSitemap(Date lastModifiedDate) {
        if (lastModifiedDate == null) {
            java.util.Calendar cal = Calendar.getInstance();
            cal.set(1970, 0, 1);
            lastModifiedDate = cal.getTime();
        }
        return pageDAO.getPageForSitemap(lastModifiedDate, 0, 0);
    }

    //********************************************************************
    //               Private Methods
    //********************************************************************

    private Draft removeDraftInternal(String spaceUname, String pageUuid, User user, PageType type,
            boolean removeAttahcmnet) throws PageException {
        Draft draft;
        try {

            if (type == PageType.NONE_DRAFT) {
                //remove both
                Draft draft1 = draftDAO.removeDraftByUuid(spaceUname, pageUuid, user, PageType.AUTO_DRAFT);
                Draft draft2 = draftDAO.removeDraftByUuid(spaceUname, pageUuid, user, PageType.MANUAL_DRAFT);
                draft = draft1 == null ? draft2 : draft1;
            } else {
                draft = draftDAO.removeDraftByUuid(spaceUname, pageUuid, user, type);
            }
            if (draft != null && draft.getPageProgress() != null) {
                pageProgressDAO.removeObject(draft.getPageProgress());
            }
            if (draft != null && removeAttahcmnet) {
                boolean pageNotExist = false;
                if (type == PageType.AUTO_DRAFT) {
                    //if auto draft removing, then need check if there is manual draft and page, if no, then remove all from repository
                    if (draftDAO.getDraftByUuid(spaceUname, pageUuid, user, PageType.MANUAL_DRAFT) == null
                            && pageDAO.getByUuid(pageUuid) == null) {
                        pageNotExist = true;
                    }
                } else if (type == PageType.MANUAL_DRAFT) {
                    //if manual draft removing, need check if page exist, if no, all remove from repository.
                    if (pageDAO.getByUuid(pageUuid) == null) {
                        pageNotExist = true;
                    }
                } else if (type == PageType.NONE_DRAFT) {
                    if (pageDAO.getByUuid(pageUuid) == null) {
                        pageNotExist = true;
                    }
                    //for non-draft normal page, all attachment will removed whoever uploaded. this set only use for pageNotExist = false.
                    user = null;
                }

                if (pageNotExist) {
                    ITicket ticket = repositoryService.login(spaceUname, spaceUname, spaceUname);
                    repositoryService.removeIdentifier(ticket, RepositoryService.TYPE_ATTACHMENT, pageUuid);
                } else {
                    //page exist, then only remove specified type attachment
                    removeDraftAttachment(spaceUname, pageUuid, user, type);
                }
            } else {
                if (draft == null)
                    log.info("unable find any draft for page " + pageUuid + " on space " + spaceUname);
            }
        } catch (RepositoryException e) {
            log.error("Remove draft failed", e);
            throw new PageException("Remove draft failed:" + e);
        } catch (RepositoryTiemoutExcetpion e) {
            log.error("Remove draft failed", e);
            throw new PageException("Remove draft failed:" + e);
        }

        return draft;
    }

    /**
     * Try to get a unique page Title in given space. The initial title value is input parameter title. If it is already unique,
     * just return itself.
     *  
     * @param spaceUname
     * @param title
     * @return
     */
    private String getIdeniticalTitle(String spaceUname, String title, String prefix) {
        boolean done = false;
        String orgTitle = title;
        //try 10 times 
        for (int idx = 0; idx < 10; idx++) {
            if (pageDAO.getCurrentPageByTitle(spaceUname, title) == null) {
                if (idx > 0)
                    log.info("An existed page has same title by " + orgTitle
                            + ". The target page will be renamed to " + title);
                done = true;
                break;
            }
            title = prefix + title;
        }
        if (!done) {
            //failure tolerance: OK, so many copy of copy of, then add a UUID to page title
            title = orgTitle + UUID.randomUUID().toString();
        }
        return title;
    }

    /**
     * If user has drafted attachment before, but the user did not resume the draft(whatever auto/manual) when editing then save,
     * This means the previous drafted attachment must be removed because they are not invisible for time being. The remove 
     * strategy is:<Br>
     * Any auto saved attachments which are not in comeinList, will be removed whatever the type (MANUAL-DRAFT or NON_DRAFT or AUTO-DRAFT)
     * Manual saved attachments which are not in comeinList, will be remove only the type is MANUAL-DRAFT or NON_DRAFT  
     * @throws RepositoryTiemoutExcetpion 
     * @throws RepositoryException 
     */
    private void mergeAttahment(List<FileNode> existList, List<FileNode> comeinList, String spaceUname, User user,
            PageType status) throws RepositoryException, RepositoryTiemoutExcetpion {
        if (comeinList != null && existList != null) {
            ITicket ticket = repositoryService.login(spaceUname, spaceUname, spaceUname);
            for (FileNode node : existList) {
                //exist item is current user's draft, and it has same status or higher (auto if given is manual)
                if (node.getStatus() > 0 && node.getStatus() >= status.value()
                        && StringUtils.equalsIgnoreCase(node.getCreateor(), user.getUsername())) {
                    //check if this draft is in comeinList, if not, it means it will be removed.
                    boolean found = false;
                    for (FileNode comeNode : comeinList) {
                        if (comeNode.getNodeUuid().equals(node.getNodeUuid())) {
                            found = true;
                            break;
                        }
                    }
                    if (!found) {
                        repositoryService.removeFile(ticket, node.getNodeUuid(), null);
                        log.info("Attachment {} is removed.", node.getNodeUuid());
                    }
                }
            }
        }

    }

    private void upgradeAttachmentStatus(String spaceUname, String pageUuid, User user, PageType status)
            throws RepositoryException {
        List<FileNode> list = getPageAttachment(spaceUname, pageUuid, true, true, user);
        if (list != null) {
            ITicket ticket = repositoryService.login(spaceUname, spaceUname, spaceUname);
            for (FileNode node : list) {
                //auto < manual < normal, this logic sequence is reverse with actual number of their hold.  
                if (node.getStatus() > status.value() && StringUtils.equalsIgnoreCase(node.getCreateor(),
                        user != null ? user.getUsername() : null)) {
                    //remove draft flag in attachment file node
                    node.setStatus(status.value());
                    repositoryService.updateMetaData(ticket, node);
                }
            }
        }
    }

    private void removeDraftAttachment(String spaceUname, String pageUuid, User user, PageType type)
            throws RepositoryException, RepositoryTiemoutExcetpion {
        //remove this user's attachment from repository

        ITicket ticket = repositoryService.login(spaceUname, spaceUname, spaceUname);
        List<FileNode> list = getPageAttachment(spaceUname, pageUuid, true, true, user);
        if (list != null) {
            for (FileNode node : list) {
                if (node.getStatus() >= type.value()) {
                    //user if not null, then only special user's attachment(draft status) will remove.
                    if ((user != null && StringUtils.equalsIgnoreCase(node.getCreateor(), user.getUsername()))
                            || user == null)
                        repositoryService.removeFile(ticket, node.getNodeUuid(), node.getVersion());
                }
            }
        }
    }

    /**
     * @param pageValue
     * @param page
     * @return
     */
    private boolean checkVersion(Page myPage, Page currentPage) {
        //new page, version can not be conflict
        if (currentPage == null)
            return true;

        if (myPage.getVersion() != currentPage.getVersion())
            return false;
        else
            return true;
    }

    /*
     * Remove give page from cache
     */
    @SuppressWarnings("unchecked")
    private void removePageCache(String spaceUname, Page page, boolean resetChildren) {
        if (page == null)
            return;

        Element treeItem = pageTreeCache.get(spaceUname);
        Integer removedUid = null;
        if (treeItem != null) {
            Set<Page> sortedSet = (Set<Page>) treeItem.getValue();
            for (Iterator<Page> iter = sortedSet.iterator(); iter.hasNext();) {
                Page inPage = iter.next();
                if (page.getUid().equals(inPage.getUid())) {
                    log.info("remove page from tree cache.Uid:" + page.getUid() + " title:" + page.getTitle());
                    iter.remove();
                    removedUid = inPage.getUid();
                    break;
                }
            }
            //rebuild child page, reset child page's parent to this page's parent
            if (removedUid != null && resetChildren) {
                Page parentPage = null;
                //find given page's parent, use this value reset its children's parent (so bad comments:(
                //e.g., A->B->C, if B is removed. C's parent will reset to A: A->C
                if (page.getParent() != null) {
                    for (Page parent : sortedSet) {
                        if (page.getParent().getUid().equals(parent.getUid())) {
                            parentPage = parent;
                            break;
                        }
                    }
                }
                for (Page child : sortedSet) {
                    //this node's parent is removed node, then reset it removed node's parent.
                    if (child.getParent() != null && child.getParent().getUid().equals(page.getUid())) {
                        child.setParent(parentPage);
                    }
                }
            }
        }
    }

    /**
     * PageCache only save current page. If page is any draft, history, removed, it will removed from cache.
     * @param spaceUname
     * @param page
     */
    @SuppressWarnings("unchecked")
    private void addPageCache(String spaceUname, Page page) {
        if (page == null)
            return;

        Element treeItem = pageTreeCache.get(spaceUname);

        if (treeItem != null) {
            Set<Page> sortedSet = (Set<Page>) treeItem.getValue();
            //non-current: remove from page cache
            if (page.isRemoved()) {
                removePageCache(spaceUname, page, true);
            } else {
                log.info("add/update from tree cache.Uid:" + page.getUid() + " title:" + page.getTitle());
                //here does not use clone(): to avoid overuse fields copy.
                //This duplicated fields must keep consist with PageDAOHibernate.getTree() method
                Page cPage = new Page();
                cPage.setUid(page.getUid());
                cPage.setTitle(page.getTitle());
                cPage.setParent(page.getParent());
                cPage.setLevel(page.getLevel());
                cPage.setPageUuid(page.getPageUuid());

                Space spaceModel = new Space();
                spaceModel.setUnixName(spaceUname);
                cPage.setSpace(spaceModel);

                //because above does not deep clone parent, so here use exist page replace its lazy parent object
                if (cPage.getParent() != null) {
                    for (Page parent : sortedSet) {
                        if (cPage.getParent().getUid().equals(parent.getUid())) {
                            cPage.setParent(parent);
                            break;
                        }
                    }
                }
                sortedSet.add(cPage);
                pageTreeCache.put(treeItem);
            }
        } else {
            //retreive all page and rebuild pageTreeCache
            getPageTree(spaceUname);
        }

    }

    /**
     * Copy value: content, title, type , tags
     * @param pageValue
     */
    private void copyValueFromView(AbstractPage page, AbstractPage pageValue) {
        if (page instanceof Page) {
            PageContent content = ((Page) page).getContent();
            if (content == null)
                content = new PageContent();
            content.setContent(((Page) pageValue).getContent().getContent());
            ((Page) page).setContent(content);

            PageProgress progress = ((Page) page).getPageProgress();
            if (progress == null)
                progress = new PageProgress();

            ((Page) page).setPageProgress(progress);

            if (((Page) pageValue).getPageProgress() != null) {
                progress.setLinkExtID(((Page) pageValue).getPageProgress().getLinkExtID());
                progress.setLinkExtInfo(((Page) pageValue).getPageProgress().getLinkExtInfo());
            }
        } else if (page instanceof Draft) {
            DraftContent content = ((Draft) page).getContent();
            if (content == null)
                content = new DraftContent();
            content.setContent(((Draft) pageValue).getContent().getContent());
            ((Draft) page).setContent(content);

            if (((Draft) pageValue).getPageProgress() != null) {
                //draft page progress is optional
                PageProgress progress = ((Draft) page).getPageProgress();
                if (progress == null)
                    progress = new PageProgress();

                progress.setLinkExtID(((Draft) pageValue).getPageProgress().getLinkExtID());
                progress.setLinkExtInfo(((Draft) pageValue).getPageProgress().getLinkExtInfo());
                ((Draft) page).setPageProgress(progress);
            }
        } else if (page instanceof History) {
            HistoryContent content = ((History) page).getContent();
            if (content == null)
                content = new HistoryContent();
            content.setContent(((History) pageValue).getContent().getContent());
            ((History) page).setContent(content);
        }

        page.setTitle(pageValue.getTitle());
        page.setVisibleAttachmentNodeList(pageValue.getVisibleAttachmentNodeList());
        //currently, does not copy attribute
    }

    /**
     * Set this page's ancestor page list for navbar use.
     * @param spaceUname
     * @param page
     */
    private void refreshAncestors(String spaceUname, AbstractPage page) {

        //get page parent navigation information
        List<AbstractPage> parentList = new ArrayList<AbstractPage>();
        AbstractPage myPage = page;
        if (page instanceof Draft || page instanceof History) {
            //it maybe is page history version, so get back current, because Cache only saving current page info.
            myPage = pageDAO.getCurrentByUuid(page.getPageUuid());
            if (myPage == null) {
                if (page instanceof Draft) {
                    //this is draft without formal page saving, so just get from its parent page if it has
                    myPage = page.getParent();
                    if (myPage == null) {
                        //if it is root page, just simple return without any Ancenstoer information
                        return;
                    }
                } else {
                    log.error("Unpexected case: Page can not get correct ancenstor information. UUID: "
                            + page.getPageUuid() + ". Title: " + page.getTitle());
                    return;
                }
            }
        }

        //add itself, it will be first one, the last one after reverse.
        parentList.add(myPage);

        if (myPage.getParent() != null) {
            List<Page> tree = getPageTree(spaceUname);
            Page cachePage = null;
            for (Page pg : tree) {
                if (pg.getPageUuid().equals(myPage.getPageUuid())) {
                    cachePage = pg;
                    break;
                }
            }
            //failure tolerance: only deep into 20 level
            int failure = 20;
            int idx = 0;
            while (cachePage != null && idx++ < failure) {
                cachePage = cachePage.getParent();
                if (cachePage == null)
                    break;
                parentList.add(cachePage);
            }
        }
        //sort from root parent
        Collections.reverse(parentList);

        page.setAncestorList(parentList);
    }

    /**
     * If a page title changed, some links to this page becomes obsolete. This method 
     * will retrieve all page links in instance(space?) to update PageLink table and 
     * page content text in PageContent table. 
     * <br>
     * 
     * This method only update the pages which current login user has write permission. After method,
     * page version is increased.
     * 
     *  
     * @param spaceUname
     * @param oldTitle
     * @param newTitle
     */
    private void fixLinksToTitle(String spaceUname, String oldTitle, String newTitle) {
        //get back links according to space and title
        List<PageLink> linkList = pageLinkDAO.getLinksFromSpace(spaceUname, oldTitle);

        List<Integer> pageUids = new ArrayList<Integer>();
        User loginUser = WikiUtil.getUser(userReadingService);
        boolean readonly;

        for (PageLink oldLink : linkList) {
            //do I need skip current page itself? As current page will save after this method
            Page page = oldLink.getPage();
            if (StringUtils.equalsIgnoreCase(oldTitle, page.getTitle())) {
                continue;
            }
            //don't update same page twice
            if (pageUids.contains(page.getUid())) {
                continue;
            }

            //don't allow update page without write permission
            readonly = true;
            securityService.fillPageWikiOperations(loginUser, page);
            for (WikiOPERATIONS wikiOPERATIONS : page.getWikiOperations()) {
                if (WikiOPERATIONS.PAGE_WRITE.equals(wikiOPERATIONS)) {
                    readonly = false;
                    break;
                }
            }
            if (readonly) {
                StringBuffer buf = new StringBuffer("Page ");
                log.info(buf.append(page.getTitle()).append(":").append(spaceUname)
                        .append(" does not do fix for new title:").append(newTitle)
                        .append(" becuase user has no write permission.").toString());
                continue;
            }

            //upgrade version???
            History oldPage = (History) page.cloneToHistory();
            oldPage.setAttachments(null);
            oldPage.setParent(null);
            historyDAO.saveOrUpdate(oldPage);
            WikiUtil.setTouchedInfo(userReadingService, page);
            page.setVersion(page.getVersion() + 1);

            //update page content - is it dangerous?
            pageUids.add(page.getUid());
            PageContent referContent = page.getContent();
            String content = referContent.getContent();

            //replace old link with new 
            content = renderService.changeLinkTitle(content, page.getSpace().getUnixName(), spaceUname, oldTitle,
                    newTitle);
            referContent.setContent(content);

            //persist
            oldLink.setLink(newTitle);
            pageDAO.saveOrUpdate(page);
            pageLinkDAO.saveOrUpdate(oldLink);
        }
    }

    /**
     * Change all given links space(if equals fromSpaceUname) to toSpaceUname. And update page content 
     * to add spaceUanme suffix, such as "[link@toSpaceUname]".
     * 
     * @param fromSpaceUname
     * @param toSpaceUname
     * @param links
     */
    private void fixLinksToSpace(Page page, String fromSpaceUname, String toSpaceUname) {
        Set<PageLink> links = page.getLinks();
        if (links == null || links.size() == 0)
            return;

        Set<String> titles = new HashSet<String>();
        for (PageLink link : links) {
            //if link has special spaceUname rather than fromSpaceUname, skip it
            //for example original space is SpaceA, if [link@spaceXXX], which won't do space replacement
            //but [link@spaceA] or [link] will do
            if (StringUtils.equals(link.getSpaceUname(), fromSpaceUname)) {
                titles.add(link.getLink());
            }
        }

        if (titles.size() > 0) {
            String content = page.getContent().getContent();
            //replace old link with new 
            content = renderService.changeLinkSpace(content, fromSpaceUname, toSpaceUname);
            page.getContent().setContent(content);
        }
    }

    //********************************************************************
    //               Set / Get
    //********************************************************************
    public void setPageDAO(PageDAO pageDAO) {
        this.pageDAO = pageDAO;
    }

    public void setUserReadingService(UserReadingService userReadingService) {
        this.userReadingService = userReadingService;
    }

    public void setSpaceDAO(SpaceDAO spaceDAO) {
        this.spaceDAO = spaceDAO;
    }

    public void setRepositoryService(RepositoryService repositoryService) {
        this.repositoryService = repositoryService;
    }

    public void setPageLinkDAO(PageLinkDAO pageLinkDAO) {
        this.pageLinkDAO = pageLinkDAO;
    }

    public void setPageTreeCache(Cache pageTreeCache) {
        this.pageTreeCache = pageTreeCache;
    }

    public void setUserPageDAO(UserPageDAO userPageDAO) {
        this.userPageDAO = userPageDAO;
    }

    public void setDraftDAO(DraftDAO draftDAO) {
        this.draftDAO = draftDAO;
    }

    public void setPageProgressDAO(PageProgressDAO pageProgressDAO) {
        this.pageProgressDAO = pageProgressDAO;
    }

    public void setTagService(TagService tagService) {
        this.tagService = tagService;
    }

    public void setCommentService(CommentService commentService) {
        this.commentService = commentService;
    }

    public void setRenderService(RenderService renderService) {
        this.renderService = renderService;
    }

    public void setSecurityService(SecurityService securityService) {
        this.securityService = securityService;
    }

    public void setHistoryDAO(HistoryDAO historyDAO) {
        this.historyDAO = historyDAO;
    }

    public void setTouchService(TouchService touchService) {
        this.touchService = touchService;
    }

    public void setPageEditingCache(Cache pageEditingCache) {
        this.pageEditingCache = pageEditingCache;
    }

    /**
     * @param eventContainer the eventContainer to set
     */
    public void setEventContainer(EventContainer eventContainer) {
        this.eventContainer = eventContainer;
    }

}