/* * Copyright (C) 2010 eXo Platform SAS. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.exoplatform.portal.mop.navigation; import static org.exoplatform.portal.mop.Utils.objectType; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.chromattic.api.ChromatticSession; import org.exoplatform.portal.mop.Described; import org.exoplatform.portal.mop.RestrictAccess; import org.exoplatform.portal.mop.SiteKey; import org.exoplatform.portal.mop.SiteType; import org.exoplatform.portal.mop.Utils; import org.exoplatform.portal.mop.Visible; import org.exoplatform.portal.mop.page.PageKey; import org.exoplatform.portal.pom.config.POMSession; import org.exoplatform.portal.pom.config.POMSessionManager; import org.exoplatform.portal.pom.data.MappedAttributes; import org.gatein.common.logging.Logger; import org.gatein.common.logging.LoggerFactory; import org.gatein.mop.api.Attributes; import org.gatein.mop.api.workspace.Navigation; import org.gatein.mop.api.workspace.ObjectType; import org.gatein.mop.api.workspace.Site; import org.gatein.mop.api.workspace.Workspace; import org.gatein.mop.api.workspace.link.PageLink; /** * @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a> * @version $Revision$ */ public class NavigationServiceImpl implements NavigationService { public static final String CUSTOM_NODE_ATTRIBUTE_PREFIX = "cust-attr-"; /** . */ final POMSessionManager manager; /** . */ private final DataCache dataCache; /** . */ final Logger log = LoggerFactory.getLogger(NavigationServiceImpl.class); public NavigationServiceImpl(POMSessionManager manager) throws NullPointerException { this(manager, new SimpleDataCache()); } public NavigationServiceImpl(POMSessionManager manager, DataCache dataCache) throws NullPointerException { if (manager == null) { throw new NullPointerException("No null pom session manager allowed"); } if (dataCache == null) { throw new NullPointerException("No null data cache allowed"); } this.manager = manager; this.dataCache = dataCache; } public NavigationContext loadNavigation(SiteKey key) { if (key == null) { throw new NullPointerException(); } // POMSession session = manager.getSession(); NavigationData data = dataCache.getNavigationData(session, key); return data != null && data != NavigationData.EMPTY ? new NavigationContext(data) : null; } @Override public List<NavigationContext> loadNavigations(SiteType type) throws NullPointerException, NavigationServiceException { if (type == null) { throw new NullPointerException(); } POMSession session = manager.getSession(); ObjectType<Site> objectType = objectType(type); Collection<Site> sites = session.getWorkspace().getSites(objectType); List<NavigationContext> navigations = new LinkedList<NavigationContext>(); for (Site site : sites) { Navigation defaultNavigation = site.getRootNavigation().getChild("default"); if (defaultNavigation != null) { SiteKey key = new SiteKey(type, site.getName()); navigations.add(new NavigationContext(new NavigationData(key, defaultNavigation))); } } return navigations; } public void saveNavigation(NavigationContext navigation) throws NullPointerException, NavigationServiceException { if (navigation == null) { throw new NullPointerException(); } // POMSession session = manager.getSession(); ObjectType<Site> objectType = objectType(navigation.key.getType()); Workspace workspace = session.getWorkspace(); Site site = workspace.getSite(objectType, navigation.key.getName()); // if (site == null) { throw new NavigationServiceException(NavigationError.NAVIGATION_NO_SITE); } // Navigation rootNode = site.getRootNavigation(); // Navigation defaultNode = rootNode.getChild("default"); if (defaultNode == null) { defaultNode = rootNode.addChild("default"); } // NavigationState state = navigation.state; if (state != null) { Integer priority = state.getPriority(); defaultNode.getAttributes().setValue(MappedAttributes.PRIORITY, priority); } // dataCache.removeNavigationData(session, navigation.key); // Update state navigation.data = dataCache.getNavigationData(session, navigation.key); navigation.state = null; } public boolean destroyNavigation(NavigationContext navigation) throws NullPointerException, NavigationServiceException { if (navigation == null) { throw new NullPointerException("No null navigation argument"); } if (navigation.data == null) { throw new IllegalArgumentException("Already removed"); } // POMSession session = manager.getSession(); ObjectType<Site> objectType = objectType(navigation.key.getType()); Workspace workspace = session.getWorkspace(); Site site = workspace.getSite(objectType, navigation.key.getName()); // if (site == null) { throw new NavigationServiceException(NavigationError.NAVIGATION_NO_SITE); } // Navigation rootNode = site.getRootNavigation(); Navigation defaultNode = rootNode.getChild("default"); // if (defaultNode != null) { // Invalidate cache dataCache.removeNavigation(navigation.key); String rootId = navigation.data.rootId; if (rootId != null) { dataCache.removeNodes(Collections.singleton(rootId)); } // Destroy nav defaultNode.destroy(); // Update state navigation.data = null; // return true; } else { return false; } } public <N> NodeContext<N> loadNode(NodeModel<N> model, NavigationContext navigation, Scope scope, NodeChangeListener<NodeContext<N>> listener) { if (model == null) { throw new NullPointerException("No null model accepted"); } if (navigation == null) { throw new NullPointerException("No null navigation accepted"); } if (scope == null) { throw new NullPointerException("No null scope accepted"); } String nodeId = navigation.data.rootId; if (navigation.data.rootId != null) { POMSession session = manager.getSession(); NodeData data = dataCache.getNodeData(session, nodeId); if (data != null) { NodeContext<N> context = new NodeContext<N>(model, data); updateNode(context, scope, listener); return context; } else { return null; } } else { return null; } } public <N> void updateNode(NodeContext<N> root, Scope scope, NodeChangeListener<NodeContext<N>> listener) throws NullPointerException, IllegalArgumentException, NavigationServiceException { Scope.Visitor visitor; if (scope != null) { visitor = new FederatingVisitor<N>(root.tree, root, scope); } else { visitor = root.tree; } // updateTree(root.tree, visitor, listener); } public <N> void saveNode(NodeContext<N> context, NodeChangeListener<NodeContext<N>> listener) throws NullPointerException, NavigationServiceException { saveTree(context.tree, listener); } public <N> void rebaseNode(NodeContext<N> context, Scope scope, NodeChangeListener<NodeContext<N>> listener) throws NavigationServiceException { Scope.Visitor visitor; if (scope != null) { visitor = new FederatingVisitor<N>(context.tree.origin(), context, scope); } else { visitor = context.tree.origin(); } // rebaseTree(context.tree, visitor, listener); } private <N> void updateTree(TreeContext<N> tree, Scope.Visitor visitor, NodeChangeListener<NodeContext<N>> listener) throws NullPointerException, IllegalArgumentException, NavigationServiceException { if (tree.hasChanges()) { throw new IllegalArgumentException("For now we don't accept to update a context that has pending changes"); } // POMSession session = manager.getSession(); NodeData data = dataCache.getNodeData(session, tree.root.data.id); if (data == null) { throw new NavigationServiceException(NavigationError.UPDATE_CONCURRENTLY_REMOVED_NODE); } // Switch to edit mode tree.editMode = true; // Apply diff changes to the model try { TreeUpdate.perform(tree, NodeContextUpdateAdapter.<N> create(), data, NodeDataUpdateAdapter.create(dataCache, session), listener, visitor); } finally { // Disable edit mode tree.editMode = false; } } public void clearCache() { dataCache.clear(); } private <N> void saveTree(TreeContext<N> tree, NodeChangeListener<NodeContext<N>> listener) throws NullPointerException, NavigationServiceException { POMSession session = manager.getSession(); // NodeData data = dataCache.getNodeData(session, tree.root.data.id); if (data == null) { throw new NavigationServiceException(NavigationError.UPDATE_CONCURRENTLY_REMOVED_NODE); } // Attempt to rebase TreeContext<N> rebased = rebase(tree, tree.origin()); // NavigationPersister<N> persister = new NavigationPersister<N>(session); // NodeChangeQueue<NodeContext<N>> changes = rebased.getChanges(); if (changes != null) { changes.broadcast(persister); // Update the tree handles to the persistent values for (Map.Entry<String, String> entry : persister.toPersist.entrySet()) { NodeContext<N> a = tree.getNode(entry.getKey()); a.handle = entry.getValue(); } // Update data for (String ddd : persister.toUpdate) { NodeContext<N> a = tree.getNode(ddd); a.data = new NodeData(a); a.name = null; a.state = null; } // Clear changes changes.clear(); tree.getChanges().clear(); } // Update TreeUpdate.perform(tree, NodeContextUpdateAdapter.<N> create(), rebased.root, NodeContextUpdateAdapter.<N> create(), listener, rebased); // dataCache.removeNodeData(session, persister.toEvict); } private <N> void rebaseTree(TreeContext<N> tree, Scope.Visitor visitor, NodeChangeListener<NodeContext<N>> listener) throws NavigationServiceException { if (!tree.hasChanges()) { updateTree(tree, visitor, listener); } else { TreeContext<N> rebased = rebase(tree, visitor); // TreeUpdate.perform(tree, NodeContextUpdateAdapter.<N> create(), rebased.root, NodeContextUpdateAdapter.<N> create(), listener, rebased); } } private <N> TreeContext<N> rebase(TreeContext<N> tree, Scope.Visitor visitor) throws NavigationServiceException { POMSession session = manager.getSession(); NodeData data = dataCache.getNodeData(session, tree.root.getId()); if (data == null) { throw new NavigationServiceException(NavigationError.UPDATE_CONCURRENTLY_REMOVED_NODE); } // TreeContext<N> rebased = new NodeContext<N>(tree.model, data).tree; // TreeUpdate.perform(rebased, NodeContextUpdateAdapter.<N> create(), data, NodeDataUpdateAdapter.create(dataCache, session), null, visitor); // NodeChangeQueue<NodeContext<N>> changes = tree.getChanges(); // NodeChangeListener<NodeContext<N>> merger = new TreeMerge<N>(rebased, rebased); // if (changes != null) { changes.broadcast(merger); } // return rebased; } private static class NodeContextUpdateAdapter<N> implements TreeUpdateAdapter<NodeContext<N>> { /** . */ private static final NodeContextUpdateAdapter<?> _instance = new NodeContextUpdateAdapter(); static <N> NodeContextUpdateAdapter<N> create() { @SuppressWarnings("unchecked") NodeContextUpdateAdapter<N> instance = (NodeContextUpdateAdapter<N>) _instance; return instance; } public String getHandle(NodeContext<N> node) { return node.handle; } public String[] getChildren(NodeContext<N> node) { if (node.getFirst() != null) { ArrayList<String> tmp = new ArrayList<String>(); for (NodeContext<N> current = node.getFirst(); current != null; current = current.getNext()) { tmp.add(current.handle); } return tmp.toArray(new String[tmp.size()]); } else { return Utils.EMPTY_STRING_ARRAY; } } public NodeContext<N> getDescendant(NodeContext<N> node, String handle) { return node.getDescendant(handle); } public NodeData getData(NodeContext<N> node) { return node.data; } public NodeState getState(NodeContext<N> node) { return node.state; } public String getName(NodeContext<N> node) { return node.name; } } private static class NodeDataUpdateAdapter implements TreeUpdateAdapter<NodeData> { static NodeDataUpdateAdapter create(DataCache dataCache, POMSession session) { return new NodeDataUpdateAdapter(dataCache, session); } /** . */ private final DataCache dataCache; /** . */ private final POMSession session; private NodeDataUpdateAdapter(DataCache dataCache, POMSession session) { this.dataCache = dataCache; this.session = session; } public String getHandle(NodeData node) { return node.id; } public String[] getChildren(NodeData node) { return node.children; } public NodeData getDescendant(NodeData node, String handle) { NodeData data = dataCache.getNodeData(session, handle); NodeData current = data; while (current != null) { if (node.id.equals(current.id)) { return data; } else { if (current.parentId != null) { current = dataCache.getNodeData(session, current.parentId); } else { current = null; } } } return null; } public NodeData getData(NodeData node) { return node; } public NodeState getState(NodeData node) { return null; } public String getName(NodeData node) { return null; } } private static class NavigationPersister<N> extends NodeChangeListener.Base<NodeContext<N>> { /** The persisted handles to assign. */ private final Map<String, String> toPersist; /** The handles to update. */ private final Set<String> toUpdate; /** The handles to evict. */ private final Set<String> toEvict; /** . */ private final POMSession session; private NavigationPersister(POMSession session) { this.toPersist = new HashMap<String, String>(); this.toUpdate = new HashSet<String>(); this.session = session; this.toEvict = new HashSet<String>(); } @Override public void onCreate(NodeContext<N> target, NodeContext<N> parent, NodeContext<N> previous, String name) throws NavigationServiceException { Navigation parentNav = session.findObjectById(ObjectType.NAVIGATION, parent.data.id); toEvict.add(parentNav.getObjectId()); int index = 0; if (previous != null) { Navigation previousNav = session.findObjectById(ObjectType.NAVIGATION, previous.data.id); index = previousNav.getIndex() + 1; } // Navigation sourceNav = parentNav.addChild(index, name); // parent.data = new NodeData(parentNav); // Save the handle toPersist.put(target.handle, sourceNav.getObjectId()); // target.data = new NodeData(sourceNav); target.handle = target.data.id; target.name = null; target.state = null; // toUpdate.add(parent.handle); toUpdate.add(target.handle); } @Override public void onDestroy(NodeContext<N> target, NodeContext<N> parent) { Navigation parentNav = session.findObjectById(ObjectType.NAVIGATION, parent.data.id); Navigation sourceNav = session.findObjectById(ObjectType.NAVIGATION, target.data.id); // toEvict.add(parentNav.getObjectId()); sourceNav.destroy(); // parent.data = new NodeData(parentNav); toUpdate.add(parent.handle); // destroy(target); } private void destroy(NodeContext<N> ctx) { toPersist.values().remove(ctx.handle); // toUpdate.remove(ctx.handle); // toEvict.add(ctx.handle); // Recurse if (ctx.isExpanded()) { for (NodeContext<N> child = ctx.getFirst(); child != null; child = child.getNext()) { destroy(child); } } } @Override public void onUpdate(NodeContext<N> source, NodeState state) throws NavigationServiceException { Navigation sourceNav = session.findObjectById(ObjectType.NAVIGATION, source.data.id); // toEvict.add(sourceNav.getObjectId()); Workspace workspace = sourceNav.getSite().getWorkspace(); PageKey reference = state.getPageRef(); if (reference != null) { ObjectType<? extends Site> siteType = Utils.objectType(reference.getSite().getType()); Site site = workspace.getSite(siteType, reference.getSite().getName()); org.gatein.mop.api.workspace.Page target = site.getRootPage().getChild("pages").getChild(reference.getName()); PageLink link = sourceNav.linkTo(ObjectType.PAGE_LINK); link.setPage(target); } else { PageLink link = sourceNav.linkTo(ObjectType.PAGE_LINK); link.setPage(null); } // Described described = sourceNav.adapt(Described.class); described.setName(state.getLabel()); // if (!sourceNav.isAdapted(RestrictAccess.class)) { // if RestrictAccess is not on the node yet, then it has a legacy Visible // so, we remove the Visible and replace with a RestrictAccess ChromatticSession chromatticSession = session.getManager().getLifeCycle().getContext().getSession(); if (sourceNav.isAdapted(Visible.class)) { chromatticSession.remove(sourceNav.adapt(Visible.class)); } RestrictAccess restrictAccess = chromatticSession.create(RestrictAccess.class); chromatticSession.setEmbedded(sourceNav, RestrictAccess.class, restrictAccess); } // RestrictAccess restrictAccess = sourceNav.adapt(RestrictAccess.class); restrictAccess.setVisibility(state.getVisibility()); restrictAccess.setStartPublicationDate(state.getStartPublicationDate()); restrictAccess.setEndPublicationDate(state.getEndPublicationDate()); restrictAccess.setRestrictOutsidePublicationWindow(state.isRestrictOutsidePublicationWindow()); // Sync the attributes Attributes attrs = sourceNav.getAttributes(); AttributesState.sync(state.getAttributesState(), CUSTOM_NODE_ATTRIBUTE_PREFIX, attrs); attrs.setValue(MappedAttributes.ICON, state.getIcon()); // source.data = new NodeData(sourceNav); source.state = null; // toUpdate.add(source.handle); } @Override public void onMove(NodeContext<N> target, NodeContext<N> from, NodeContext<N> to, NodeContext<N> previous) throws NavigationServiceException { Navigation sourceNav = session.findObjectById(ObjectType.NAVIGATION, target.data.id); Navigation fromNav = session.findObjectById(ObjectType.NAVIGATION, from.data.id); Navigation toNav = session.findObjectById(ObjectType.NAVIGATION, to.data.id); // toEvict.add(sourceNav.getObjectId()); toEvict.add(fromNav.getObjectId()); toEvict.add(toNav.getObjectId()); int index; if (previous != null) { Navigation previousNav = session.findObjectById(ObjectType.NAVIGATION, previous.data.id); index = previousNav.getIndex() + 1; } else { index = 0; } toNav.getChildren().add(index, sourceNav); // from.data = new NodeData(fromNav); // to.data = new NodeData(toNav); // target.data = new NodeData(sourceNav); // toUpdate.add(target.handle); toUpdate.add(from.handle); toUpdate.add(to.handle); } public void onRename(NodeContext<N> target, NodeContext<N> parent, String name) throws NavigationServiceException { Navigation sourceNav = session.findObjectById(ObjectType.NAVIGATION, target.data.id); Navigation parentNav = session.findObjectById(ObjectType.NAVIGATION, parent.data.id); // toEvict.add(sourceNav.getObjectId()); toEvict.add(parentNav.getObjectId()); sourceNav.setName(name); // target.data = new NodeData(sourceNav); target.name = null; // parent.data = new NodeData(parentNav); // toUpdate.add(parent.handle); toUpdate.add(target.handle); } } }