| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| */ |
| package org.apache.lenya.cms.site.tree2; |
| |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.apache.avalon.framework.container.ContainerUtil; |
| import org.apache.avalon.framework.logger.AbstractLogEnabled; |
| import org.apache.avalon.framework.logger.Logger; |
| import org.apache.avalon.framework.service.ServiceException; |
| import org.apache.avalon.framework.service.ServiceManager; |
| import org.apache.lenya.cms.publication.Area; |
| import org.apache.lenya.cms.publication.Document; |
| import org.apache.lenya.cms.publication.Publication; |
| import org.apache.lenya.cms.repository.Node; |
| import org.apache.lenya.cms.repository.NodeFactory; |
| import org.apache.lenya.cms.repository.Persistable; |
| import org.apache.lenya.cms.repository.RepositoryException; |
| import org.apache.lenya.cms.repository.Session; |
| import org.apache.lenya.cms.site.Link; |
| import org.apache.lenya.cms.site.SiteException; |
| import org.apache.lenya.cms.site.SiteNode; |
| import org.apache.lenya.cms.site.SiteStructure; |
| import org.apache.lenya.cms.site.tree.SiteTree; |
| import org.apache.lenya.util.Assert; |
| import org.apache.lenya.xml.DocumentHelper; |
| import org.apache.lenya.xml.NamespaceHelper; |
| import org.w3c.dom.Element; |
| |
| /** |
| * Simple site tree implementation. |
| */ |
| public class SiteTreeImpl extends AbstractLogEnabled implements SiteStructure, SiteTree, Persistable { |
| |
| private Area area; |
| protected ServiceManager manager; |
| private RootNode root; |
| |
| /** |
| * @param manager The service manager. |
| * @param area The area. |
| * @param logger The logger. |
| */ |
| public SiteTreeImpl(ServiceManager manager, Area area, Logger logger) { |
| ContainerUtil.enableLogging(this, logger); |
| this.area = area; |
| this.manager = manager; |
| initRoot(); |
| } |
| |
| protected void initRoot() { |
| this.root = new RootNode(this, getLogger()); |
| nodeAdded(root); |
| } |
| |
| private String sourceUri; |
| |
| protected String getSourceUri() { |
| if (this.sourceUri == null) { |
| String baseUri = this.area.getPublication().getContentURI(this.area.getName()); |
| this.sourceUri = baseUri + "/sitetree.xml"; |
| } |
| return this.sourceUri; |
| } |
| |
| private long lastModified = -1; |
| private boolean loading = false; |
| |
| protected static final String NAMESPACE = "http://apache.org/cocoon/lenya/sitetree/1.0"; |
| |
| private static final boolean DEFAULT_VISIBILITY = true; |
| |
| private boolean loaded = false; |
| |
| protected synchronized void load() { |
| |
| if (this.loaded) { |
| return; |
| } |
| |
| Node repoNode = getRepositoryNode(); |
| |
| try { |
| repoNode.setPersistable(this); |
| |
| // lastModified check is necessary for clustering, but can cause 404s |
| // because of the 1s file system last modification granularity |
| if (repoNode.exists() /* && repoNode.getLastModified() > this.lastModified */) { |
| long lastModified = repoNode.getLastModified(); |
| org.w3c.dom.Document xml = DocumentHelper.readDocument(repoNode.getInputStream()); |
| |
| NamespaceHelper helper = new NamespaceHelper(NAMESPACE, "", xml); |
| Assert.isTrue("document element is site", xml.getDocumentElement().getLocalName() |
| .equals("site")); |
| this.loading = true; |
| reset(); |
| loadNodes(this.root, helper, xml.getDocumentElement()); |
| this.loading = false; |
| this.lastModified = lastModified; |
| } |
| |
| if (!repoNode.exists() && this.lastModified > -1) { |
| reset(); |
| this.lastModified = -1; |
| } |
| |
| this.loaded = true; |
| |
| } catch (Exception e) { |
| throw new RuntimeException(e); |
| } |
| checkInvariants(); |
| } |
| |
| protected void reset() { |
| this.path2node.clear(); |
| this.uuidLanguage2link.clear(); |
| initRoot(); |
| } |
| |
| protected RootNode getRoot() { |
| load(); |
| return this.root; |
| } |
| |
| protected void loadNodes(TreeNode parent, NamespaceHelper helper, Element element) { |
| Element[] nodeElements = helper.getChildren(element, "node"); |
| for (int n = 0; n < nodeElements.length; n++) { |
| String name = nodeElements[n].getAttribute("id"); |
| boolean visible = DEFAULT_VISIBILITY; |
| if (nodeElements[n].hasAttribute("visibleinnav")) { |
| String visibleString = nodeElements[n].getAttribute("visibleinnav"); |
| visible = Boolean.valueOf(visibleString).booleanValue(); |
| } |
| TreeNodeImpl node = (TreeNodeImpl) parent.addChild(name, visible); |
| if (nodeElements[n].hasAttribute("uuid")) { |
| String uuid = nodeElements[n].getAttribute("uuid"); |
| node.setUuid(uuid); |
| } |
| loadLinks(node, helper, nodeElements[n]); |
| loadNodes(node, helper, nodeElements[n]); |
| } |
| } |
| |
| protected void loadLinks(TreeNodeImpl node, NamespaceHelper helper, Element element) { |
| Element[] linkElements = helper.getChildren(element, "label"); |
| for (int l = 0; l < linkElements.length; l++) { |
| String lang = linkElements[l].getAttribute("xml:lang"); |
| String label = DocumentHelper.getSimpleElementText(linkElements[l]); |
| node.addLink(lang, label); |
| } |
| } |
| |
| public synchronized void save() throws RepositoryException { |
| if (loading || !changed) { |
| return; |
| } |
| try { |
| Node repoNode = getRepositoryNode(); |
| NamespaceHelper helper = new NamespaceHelper(NAMESPACE, "", "site"); |
| |
| int revision = getRevision(repoNode) + 1; |
| helper.getDocument().getDocumentElement().setAttribute("revision", |
| Integer.toString(revision)); |
| |
| saveNodes(getRoot(), helper, helper.getDocument().getDocumentElement()); |
| helper.save(repoNode.getOutputStream()); |
| this.lastModified = repoNode.getLastModified(); |
| } catch (RuntimeException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new RepositoryException(e); |
| } |
| |
| } |
| |
| protected int getRevision(Node repoNode) { |
| int revision = 0; |
| if (repoNode.getHistory().getRevisionNumbers().length > 0) { |
| revision = repoNode.getHistory().getLatestRevision().getNumber(); |
| } |
| return revision; |
| } |
| |
| protected void saveNodes(TreeNode parent, NamespaceHelper helper, Element parentElement) |
| throws SiteException { |
| SiteNode[] children = parent.getChildren(); |
| for (int i = 0; i < children.length; i++) { |
| Element nodeElement = helper.createElement("node"); |
| nodeElement.setAttribute("id", children[i].getName()); |
| String uuid = children[i].getUuid(); |
| if (uuid != null) { |
| nodeElement.setAttribute("uuid", uuid); |
| } |
| nodeElement.setAttribute("visibleinnav", Boolean.toString(children[i].isVisible())); |
| saveLinks(children[i], helper, nodeElement); |
| saveNodes((TreeNode) children[i], helper, nodeElement); |
| parentElement.appendChild(nodeElement); |
| } |
| } |
| |
| protected void saveLinks(SiteNode node, NamespaceHelper helper, Element nodeElement) |
| throws SiteException { |
| String[] languages = node.getLanguages(); |
| for (int i = 0; i < languages.length; i++) { |
| Link link = node.getLink(languages[i]); |
| Element linkElement = helper.createElement("label", link.getLabel()); |
| linkElement.setAttribute("xml:lang", languages[i]); |
| nodeElement.appendChild(linkElement); |
| } |
| } |
| |
| public Link add(String path, Document doc) throws SiteException { |
| |
| if (containsByUuid(doc.getUUID(), doc.getLanguage())) { |
| throw new SiteException("The document [" + doc + "] is already contained!"); |
| } |
| |
| TreeNodeImpl node; |
| if (contains(path)) { |
| node = getTreeNode(path); |
| if (node.getUuid() == null) { |
| node.setUuid(doc.getUUID()); |
| } else if (!node.getUuid().equals(doc.getUUID())) { |
| throw new SiteException("The node already has a different UUID!"); |
| } |
| } else { |
| node = (TreeNodeImpl) add(path); |
| node.setUuid(doc.getUUID()); |
| } |
| return node.addLink(doc.getLanguage(), ""); |
| } |
| |
| protected TreeNodeImpl getTreeNode(String path) throws SiteException { |
| return (TreeNodeImpl) getNode(path); |
| } |
| |
| public SiteNode add(String path) throws SiteException { |
| String parentPath = getParentPath(path); |
| String nodeName = path.substring(parentPath.length() + 1); |
| if (!contains(parentPath)) { |
| add(parentPath); |
| } |
| return getTreeNode(parentPath).addChild(nodeName, DEFAULT_VISIBILITY); |
| } |
| |
| public SiteNode add(String path, String followingSiblingPath) throws SiteException { |
| String parentPath = getParentPath(path); |
| String nodeName = path.substring(parentPath.length() + 1); |
| |
| if (!followingSiblingPath.startsWith(parentPath + "/")) { |
| throw new SiteException("Invalid following sibling path [" + followingSiblingPath + "]"); |
| } |
| |
| String followingNodeName = followingSiblingPath.substring(parentPath.length() + 1); |
| |
| if (!contains(parentPath)) { |
| add(parentPath); |
| } |
| return getTreeNode(parentPath).addChild(nodeName, followingNodeName, DEFAULT_VISIBILITY); |
| } |
| |
| protected String getParentPath(String path) { |
| int lastIndex = path.lastIndexOf("/"); |
| String parentPath = path.substring(0, lastIndex); |
| return parentPath; |
| } |
| |
| private Map path2node = new HashMap(); |
| private Map uuidLanguage2link = new HashMap(); |
| |
| protected void nodeAdded(SiteNode node) { |
| String path = node.getPath(); |
| Assert.notNull("path", path); |
| if (node != this.root) { |
| Assert.isTrue("path not empty", path.length() > 0); |
| } |
| this.path2node.put(path, node); |
| } |
| |
| protected void linkAdded(Link link) { |
| if (link.getNode().getUuid() != null) { |
| this.uuidLanguage2link.put(getKey(link), link); |
| } |
| } |
| |
| protected String getKey(Link link) { |
| String uuid = link.getDocument().getUUID(); |
| Assert.notNull("uuid", uuid); |
| String language = link.getLanguage(); |
| Assert.notNull("language", language); |
| return getKey(uuid, language); |
| } |
| |
| protected String getKey(String uuid, String language) { |
| Assert.notNull("uuid", uuid); |
| Assert.notNull("language", language); |
| return uuid + ":" + language; |
| } |
| |
| protected void nodeRemoved(String path) { |
| Assert.notNull("path", path); |
| Assert.isTrue("path [" + path + "] contained", this.path2node.containsKey(path)); |
| this.path2node.remove(path); |
| } |
| |
| protected Map getUuidLanguage2Link() { |
| load(); |
| return this.uuidLanguage2link; |
| } |
| |
| protected Map getPath2Node() { |
| load(); |
| return this.path2node; |
| } |
| |
| public boolean contains(String path) { |
| load(); |
| Assert.notNull("path", path); |
| return this.path2node.containsKey(path); |
| } |
| |
| public boolean containsByUuid(String uuid, String language) { |
| Assert.notNull("uuid", uuid); |
| Assert.notNull("language", language); |
| return getUuidLanguage2Link().containsKey(getKey(uuid, language)); |
| } |
| |
| public boolean containsInAnyLanguage(String uuid) { |
| Assert.notNull("uuid", uuid); |
| Set set = getUuidLanguage2Link().keySet(); |
| String[] keys = (String[]) set.toArray(new String[set.size()]); |
| for (int i = 0; i < keys.length; i++) { |
| if (keys[i].startsWith(uuid + ":")) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public String getArea() { |
| return this.area.getName(); |
| } |
| |
| public Link getByUuid(String uuid, String language) throws SiteException { |
| Assert.notNull("uuid", uuid); |
| Assert.notNull("language", language); |
| String key = getKey(uuid, language); |
| if (!getUuidLanguage2Link().containsKey(key)) { |
| throw new SiteException("No link for [" + key + "]"); |
| } |
| return (Link) getUuidLanguage2Link().get(key); |
| } |
| |
| protected void checkInvariants() { |
| if (true) { |
| return; |
| } |
| for (Iterator paths = this.path2node.keySet().iterator(); paths.hasNext();) { |
| String path = (String) paths.next(); |
| SiteNode node = (SiteNode) this.path2node.get(path); |
| String uuid = node.getUuid(); |
| if (uuid != null) { |
| String[] langs = node.getLanguages(); |
| for (int i = 0; i < langs.length; i++) { |
| String key = getKey(uuid, langs[i]); |
| Assert.isTrue("contains link for [" + key + "]", this.uuidLanguage2link |
| .containsKey(key)); |
| } |
| } |
| } |
| for (Iterator keys = this.uuidLanguage2link.keySet().iterator(); keys.hasNext();) { |
| String key = (String) keys.next(); |
| Link link = (Link) this.uuidLanguage2link.get(key); |
| Assert.isTrue("contains path for [" + key + "]", this.path2node.containsKey(link |
| .getNode().getPath())); |
| } |
| |
| } |
| |
| public SiteNode getNode(String path) throws SiteException { |
| Assert.notNull("path", path); |
| if (!getPath2Node().containsKey(path)) { |
| throw new SiteException("No node for path [" + path + "]"); |
| } |
| return (SiteNode) this.path2node.get(path); |
| } |
| |
| public SiteNode[] getNodes() { |
| return getRoot().preOrder(); |
| } |
| |
| public Publication getPublication() { |
| return this.area.getPublication(); |
| } |
| |
| public Session getSession() { |
| return this.area.getPublication().getFactory().getSession(); |
| } |
| |
| private NodeFactory nodeFactory; |
| private boolean changed = false; |
| |
| protected NodeFactory getNodeFactory() { |
| if (this.nodeFactory == null) { |
| try { |
| this.nodeFactory = (NodeFactory) manager.lookup(NodeFactory.ROLE); |
| } catch (ServiceException e) { |
| throw new RuntimeException("Creating repository node failed: ", e); |
| } |
| } |
| return this.nodeFactory; |
| } |
| |
| public Node getRepositoryNode() { |
| try { |
| return (Node) getSession().getRepositoryItem(getNodeFactory(), getSourceUri()); |
| } catch (RepositoryException e) { |
| throw new RuntimeException("Creating repository node failed: ", e); |
| } |
| } |
| |
| public SiteNode[] getTopLevelNodes() { |
| return getRoot().getChildren(); |
| } |
| |
| protected void linkRemoved(String uuid, String language) { |
| Assert.notNull("uuid", uuid); |
| Assert.notNull("language", language); |
| String key = getKey(uuid, language); |
| Assert.isTrue("contained", this.uuidLanguage2link.containsKey(key)); |
| this.uuidLanguage2link.remove(key); |
| } |
| |
| protected String getPath() { |
| return ""; |
| } |
| |
| /** |
| * @return The nodes in pre-order enumeration. |
| */ |
| public SiteNode[] preOrder() { |
| return getRoot().preOrder(); |
| } |
| |
| public void moveDown(String path) throws SiteException { |
| TreeNode node = getTreeNode(path); |
| TreeNode parent = getParent(node); |
| parent.moveDown(node.getName()); |
| |
| } |
| |
| public void moveUp(String path) throws SiteException { |
| TreeNode node = getTreeNode(path); |
| TreeNode parent = getParent(node); |
| parent.moveUp(node.getName()); |
| } |
| |
| /** |
| * @param node A node. |
| * @return The parent of the node, which is the root node for top level nodes. |
| * @throws SiteException if an error occurs. |
| */ |
| protected TreeNode getParent(TreeNode node) throws SiteException { |
| TreeNode parent; |
| if (node.isTopLevel()) { |
| parent = getRoot(); |
| } else { |
| parent = (TreeNode) node.getParent(); |
| } |
| return parent; |
| } |
| |
| public boolean contains(String path, String language) { |
| if (contains(path)) { |
| SiteNode node; |
| try { |
| node = getNode(path); |
| } catch (SiteException e) { |
| throw new RuntimeException(e); |
| } |
| return node.hasLink(language); |
| } |
| return false; |
| } |
| |
| protected void changed() { |
| if (!this.loading) { |
| this.changed = true; |
| } |
| } |
| |
| public boolean isModified() { |
| return this.changed; |
| } |
| } |