001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018:
019: package org.apache.lenya.cms.site.tree;
020:
021: import java.util.ArrayList;
022: import java.util.List;
023: import java.util.StringTokenizer;
024:
025: import javax.xml.parsers.ParserConfigurationException;
026: import javax.xml.transform.TransformerException;
027:
028: import org.apache.avalon.framework.container.ContainerUtil;
029: import org.apache.avalon.framework.logger.AbstractLogEnabled;
030: import org.apache.avalon.framework.logger.Logger;
031: import org.apache.avalon.framework.service.ServiceManager;
032: import org.apache.lenya.cms.publication.DocumentFactory;
033: import org.apache.lenya.cms.publication.Publication;
034: import org.apache.lenya.cms.repository.NodeFactory;
035: import org.apache.lenya.cms.repository.RepositoryException;
036: import org.apache.lenya.cms.repository.Session;
037: import org.apache.lenya.cms.site.Link;
038: import org.apache.lenya.cms.site.SiteException;
039: import org.apache.lenya.cms.site.SiteNode;
040: import org.apache.lenya.util.Assert;
041: import org.apache.lenya.xml.DocumentHelper;
042: import org.apache.lenya.xml.NamespaceHelper;
043: import org.apache.xpath.XPathAPI;
044: import org.w3c.dom.Document;
045: import org.w3c.dom.Element;
046: import org.w3c.dom.NamedNodeMap;
047: import org.w3c.dom.Node;
048: import org.w3c.dom.NodeList;
049:
050: /**
051: * Default sitetree implementation.
052: *
053: * @version $Id: DefaultSiteTree.java 208764 2005-07-01 15:57:21Z andreas $
054: */
055: public class DefaultSiteTree extends AbstractLogEnabled implements
056: SiteTree {
057:
058: /**
059: * The sitetree namespace.
060: */
061: public static final String NAMESPACE_URI = "http://apache.org/cocoon/lenya/sitetree/1.0";
062:
063: /**
064: * The name of the sitetree file.
065: */
066: public static final String SITE_TREE_FILENAME = "sitetree.xml";
067:
068: private String sourceUri;
069: // the area is only retained to provide some more info when raising an
070: // exception.
071: private String area = "";
072: private Publication pub;
073: protected ServiceManager manager;
074: private Document document;
075: private DocumentFactory factory;
076:
077: private org.apache.lenya.cms.repository.Node repositoryNode;
078:
079: private boolean changed;
080:
081: /**
082: * Create a DefaultSiteTree
083: * @param factory The document factory.
084: * @param publication The publication.
085: * @param _area The area.
086: * @param manager The service manager.
087: * @param logger The logger.
088: * @throws SiteException if an error occurs.
089: */
090: protected DefaultSiteTree(DocumentFactory factory,
091: Publication publication, String _area,
092: ServiceManager manager, Logger logger) throws SiteException {
093:
094: ContainerUtil.enableLogging(this , logger);
095:
096: this .factory = factory;
097: this .pub = publication;
098: this .sourceUri = publication.getSourceURI() + "/content/"
099: + _area + "/" + SITE_TREE_FILENAME;
100: this .area = _area;
101: this .manager = manager;
102: try {
103: if (getRepositoryNode().exists()) {
104: this .document = DocumentHelper
105: .readDocument(getRepositoryNode()
106: .getInputStream());
107: } else {
108: getLogger().info(
109: "Empty sitetree will be created/initialized!");
110: this .document = createDocument();
111: }
112: } catch (Exception e) {
113: throw new SiteException(e);
114: }
115: }
116:
117: protected void saveDocument() throws SiteException {
118: try {
119: DocumentHelper.writeDocument(this .document,
120: getRepositoryNode().getOutputStream());
121: } catch (Exception e) {
122: throw new SiteException(e);
123: }
124: }
125:
126: /**
127: * Checks if the tree file has been modified externally and reloads the site
128: * tree. protected synchronized void checkModified() { if
129: * (this.area.equals(Publication.LIVE_AREA) && this.treefile.lastModified() >
130: * this.lastModified) {
131: *
132: * if (getLogger().isDebugEnabled()) { getLogger().debug("Sitetree [" +
133: * this.treefile + "] has changed: reloading."); }
134: *
135: * try { this.document = DocumentHelper.readDocument(this.treefile); } catch
136: * (Exception e) { throw new IllegalStateException(e.getMessage()); }
137: * this.lastModified = this.treefile.lastModified(); } }
138: */
139:
140: /**
141: * Create a new DefaultSiteTree xml document.
142: * @return the new site document
143: * @throws ParserConfigurationException if an error occurs
144: */
145: public synchronized Document createDocument()
146: throws ParserConfigurationException {
147: Document document = DocumentHelper.createDocument(
148: NAMESPACE_URI, "site", null);
149:
150: Element root = document.getDocumentElement();
151: root.setAttribute("xmlns:xsi",
152: "http://www.w3.org/2001/XMLSchema-instance");
153: root
154: .setAttribute(
155: "xsi:schemaLocation",
156: "http://apache.org/cocoon/lenya/sitetree/1.0 ../../../../resources/entities/sitetree.xsd");
157:
158: return document;
159: }
160:
161: /**
162: * Find a node in a subtree. The search is started at the given node. The
163: * list of ids contains the document-id split by "/".
164: * @param node where to start the search
165: * @param ids list of node ids
166: * @return the node that matches the path given in the list of ids
167: */
168: protected synchronized Node findNode(Node node, List ids) {
169: if (ids.size() < 1) {
170: return node;
171: }
172: NodeList nodes = node.getChildNodes();
173:
174: for (int i = 0; i < nodes.getLength(); i++) {
175: NamedNodeMap attributes = nodes.item(i).getAttributes();
176:
177: if (attributes != null) {
178: Node idAttribute = attributes.getNamedItem("id");
179:
180: if (idAttribute != null
181: && !"".equals(idAttribute.getNodeValue())
182: && idAttribute.getNodeValue()
183: .equals(ids.get(0))) {
184: return findNode(nodes.item(i), ids.subList(1, ids
185: .size()));
186: }
187: }
188: }
189:
190: // node wasn't found
191: return null;
192: }
193:
194: protected synchronized void addNode(SiteTreeNode node,
195: String refpath) throws SiteException {
196: SiteTreeNode target = addNode(node.getParent().getPath(), node
197: .getName(), node.getUuid(), node.isVisible(), node
198: .getHref(), node.getSuffix(), node.hasLink(), refpath);
199: copyLinks(node, target);
200: }
201:
202: protected void copyLinks(SiteTreeNode source, SiteTreeNode target)
203: throws SiteException {
204: String[] languages = source.getLanguages();
205: for (int i = 0; i < languages.length; i++) {
206: addLabel(target.getPath(), languages[i], source.getLink(
207: languages[i]).getLabel());
208: }
209: }
210:
211: protected synchronized void addNode(String parentid, String id,
212: String uuid, boolean visibleInNav) throws SiteException {
213: addNode(parentid, id, uuid, visibleInNav, null, null, false);
214: }
215:
216: protected synchronized void addNode(SiteTreeNode node)
217: throws SiteException {
218: addNode(node, null);
219: }
220:
221: protected synchronized SiteTreeNodeImpl addNode(String path,
222: String uuid, boolean visibleInNav, String href,
223: String suffix, boolean link, String refpath)
224: throws SiteException {
225: StringBuffer buf = new StringBuffer();
226: StringTokenizer st = new StringTokenizer(path, "/");
227: int length = st.countTokens();
228:
229: for (int i = 0; i < (length - 1); i++) {
230: buf.append("/" + st.nextToken());
231: }
232: String parentid = buf.toString();
233: String id = st.nextToken();
234: return addNode(parentid, id, uuid, visibleInNav, href, suffix,
235: link, refpath);
236: }
237:
238: protected synchronized SiteTreeNodeImpl addNode(String path,
239: String uuid, boolean visibleInNav, String href,
240: String suffix, boolean link) throws SiteException {
241: return addNode(path, uuid, visibleInNav, href, suffix, link,
242: null);
243: }
244:
245: protected synchronized SiteTreeNodeImpl addNode(String parentid,
246: String id, String uuid, boolean visibleInNav, String href,
247: String suffix, boolean link) throws SiteException {
248: return addNode(parentid + "/" + id, uuid, visibleInNav, href,
249: suffix, link, null);
250: }
251:
252: protected void createParents(final String path)
253: throws SiteException {
254: String[] steps = path.substring(1).split("/");
255: int s = 0;
256: String ancestorPath = "";
257: while (s < steps.length) {
258: if (!contains(ancestorPath)) {
259: add(ancestorPath);
260: }
261: ancestorPath += "/" + steps[s];
262: s++;
263: }
264: }
265:
266: protected synchronized SiteTreeNodeImpl addNode(String parentPath,
267: String name, String uuid, boolean visibleInNav,
268: String href, String suffix, boolean link, String refpath)
269: throws SiteException {
270:
271: String path = parentPath + "/" + name;
272: createParents(path);
273:
274: Node parentNode = getNodeInternal(parentPath);
275:
276: getLogger().debug("PARENT ELEMENT: " + parentNode);
277: getLogger().debug("VISIBLEINNAV IS: " + visibleInNav);
278:
279: // Check if child already exists
280: Node childNode = getNodeInternal(path);
281:
282: if (childNode != null) {
283: getLogger()
284: .info(
285: "This node: " + path
286: + " has already been inserted");
287: return (SiteTreeNodeImpl) getNode(path);
288: }
289:
290: // Create node
291: NamespaceHelper helper = new NamespaceHelper(NAMESPACE_URI, "",
292: this .document);
293: Element child = helper
294: .createElement(SiteTreeNodeImpl.NODE_NAME);
295: child.setAttribute(SiteTreeNodeImpl.ID_ATTRIBUTE_NAME, name);
296: if (uuid != null) {
297: child.setAttribute(SiteTreeNodeImpl.UUID_ATTRIBUTE_NAME,
298: uuid);
299: }
300:
301: if (visibleInNav) {
302: child.setAttribute(
303: SiteTreeNodeImpl.VISIBLEINNAV_ATTRIBUTE_NAME,
304: "true");
305: } else {
306: child.setAttribute(
307: SiteTreeNodeImpl.VISIBLEINNAV_ATTRIBUTE_NAME,
308: "false");
309: }
310:
311: if ((href != null) && (href.length() > 0)) {
312: child.setAttribute(SiteTreeNodeImpl.HREF_ATTRIBUTE_NAME,
313: href);
314: }
315:
316: if ((suffix != null) && (suffix.length() > 0)) {
317: child.setAttribute(SiteTreeNodeImpl.SUFFIX_ATTRIBUTE_NAME,
318: suffix);
319: }
320:
321: if (link) {
322: child.setAttribute(SiteTreeNodeImpl.LINK_ATTRIBUTE_NAME,
323: "true");
324: }
325:
326: // Add Node
327: if (refpath != null && !refpath.equals("")) {
328: Node nextSibling = getNodeInternal(refpath);
329: if (nextSibling != null) {
330: parentNode.insertBefore(child, nextSibling);
331: } else {
332: parentNode.appendChild(child);
333: }
334: } else {
335: parentNode.appendChild(child);
336: }
337: getLogger().debug(
338: "Tree has been modified: "
339: + document.getDocumentElement());
340: saveDocument();
341: return (SiteTreeNodeImpl) getNode(path);
342: }
343:
344: protected synchronized void addLabel(String path, String language,
345: String label) {
346: try {
347: SiteTreeNodeImpl node = (SiteTreeNodeImpl) getNode(path);
348: if (node != null) {
349: node.addLabel(language, label);
350: }
351: saveDocument();
352: } catch (SiteException e) {
353: throw new RuntimeException(e);
354: }
355: }
356:
357: protected synchronized void removeLabel(String path, String language) {
358: try {
359: SiteTreeNodeImpl node = (SiteTreeNodeImpl) getNode(path);
360: node.removeLabel(language);
361: saveDocument();
362: } catch (SiteException e) {
363: throw new RuntimeException(e);
364: }
365: }
366:
367: protected synchronized SiteTreeNode removeNode(String path) {
368: assert path != null;
369:
370: Node node;
371: try {
372: node = removeNodeInternal(path);
373: } catch (SiteException e) {
374: throw new RuntimeException(e);
375: }
376: if (node == null) {
377: return null;
378: }
379:
380: SiteTreeNode newNode = new SiteTreeNodeImpl(this .factory, this ,
381: (Element) node, getLogger());
382: ContainerUtil.enableLogging(newNode, getLogger());
383: return newNode;
384: }
385:
386: /**
387: * removes the node corresponding to the given document-id and returns it
388: * @param path the document-id of the Node to be removed
389: * @return the <code>Node</code> that was removed
390: * @throws SiteException
391: */
392: private synchronized Node removeNodeInternal(String path)
393: throws SiteException {
394: Assert.isTrue("contains " + path, contains(path));
395: Node node = this .getNodeInternal(path);
396: Node parentNode = node.getParentNode();
397: Node newNode = parentNode.removeChild(node);
398: try {
399: saveDocument();
400: } catch (SiteException e) {
401: throw new RuntimeException(e);
402: }
403:
404: return newNode;
405: }
406:
407: /**
408: * Find a node for a given document-id
409: *
410: * @param path the document-id of the Node that we're trying to get
411: *
412: * @return the Node if there is a Node for the given document-id, null
413: * otherwise
414: * @throws SiteException
415: */
416: private synchronized Node getNodeInternal(String path)
417: throws SiteException {
418: StringTokenizer st = new StringTokenizer(path, "/");
419: ArrayList ids = new ArrayList();
420:
421: while (st.hasMoreTokens()) {
422: ids.add(st.nextToken());
423: }
424:
425: Node node = findNode(this .document.getDocumentElement(), ids);
426: return node;
427: }
428:
429: /**
430: * @see org.apache.lenya.cms.site.tree.SiteTree#getNode(java.lang.String)
431: */
432: public synchronized SiteNode getNode(String path)
433: throws SiteException {
434: assert path != null;
435:
436: SiteTreeNode treeNode = null;
437:
438: Node node;
439: try {
440: node = getNodeInternal(path);
441: } catch (SiteException e) {
442: throw new RuntimeException(e);
443: }
444: if (node != null) {
445: treeNode = new SiteTreeNodeImpl(this .factory, this ,
446: (Element) node, getLogger());
447: ContainerUtil.enableLogging(treeNode, getLogger());
448: } else {
449: throw new SiteException("No node contained for path ["
450: + path + "]!");
451: }
452:
453: return treeNode;
454: }
455:
456: /**
457: * Move up the node amongst its siblings.
458: * @param path The document id for the node.
459: * @throws SiteException if the moving failed.
460: */
461: public synchronized void moveUp(String path) throws SiteException {
462: Node node = this .getNodeInternal(path);
463: if (node == null) {
464: throw new SiteException("Node to move: " + path
465: + " not found");
466: }
467: Node parentNode = node.getParentNode();
468: if (parentNode == null) {
469: throw new SiteException("Parentid of node with path: "
470: + path + " not found");
471: }
472:
473: Node previousNode;
474: try {
475: previousNode = XPathAPI
476: .selectSingleNode(node,
477: "(preceding-sibling::*[local-name() = 'node'])[last()]");
478: } catch (TransformerException e) {
479: throw new SiteException(e);
480: }
481:
482: if (previousNode == null) {
483: getLogger().warn("Couldn't found a preceding sibling");
484: return;
485: }
486: Node insertNode = parentNode.removeChild(node);
487: parentNode.insertBefore(insertNode, previousNode);
488: saveDocument();
489: }
490:
491: /**
492: * Move down the node amongst its siblings.
493: *
494: * @param path The document id for the node.
495: * @throws SiteException if the moving failed.
496: */
497: public synchronized void moveDown(String path) throws SiteException {
498: Node node = this .getNodeInternal(path);
499: if (node == null) {
500: throw new SiteException("Node to move: " + path
501: + " not found");
502: }
503: Node parentNode = node.getParentNode();
504: if (parentNode == null) {
505: throw new SiteException("Parentid of node with path: "
506: + path + " not found");
507: }
508: Node nextNode;
509: try {
510: nextNode = XPathAPI
511: .selectSingleNode(node,
512: "following-sibling::*[local-name() = 'node'][position()=2]");
513: } catch (TransformerException e) {
514: throw new SiteException(e);
515: }
516:
517: Node insertNode = parentNode.removeChild(node);
518:
519: if (nextNode == null) {
520: getLogger().debug(
521: "Couldn't found the second following sibling");
522: parentNode.appendChild(insertNode);
523: } else {
524: parentNode.insertBefore(insertNode, nextNode);
525: }
526: saveDocument();
527: }
528:
529: protected synchronized void setLabel(String path, String language,
530: String label) {
531: try {
532: SiteTreeNode node = (SiteTreeNode) getNode(path);
533: node.getLink(language).setLabel(label);
534: } catch (SiteException e) {
535: throw new RuntimeException(e);
536: }
537: }
538:
539: /**
540: * @see org.apache.lenya.cms.site.SiteStructure#getRepositoryNode()
541: */
542: public org.apache.lenya.cms.repository.Node getRepositoryNode() {
543: if (this .repositoryNode == null) {
544: Session session = this .getPublication().getFactory()
545: .getSession();
546: NodeFactory factory = null;
547: try {
548: factory = (NodeFactory) manager
549: .lookup(NodeFactory.ROLE);
550: this .repositoryNode = (org.apache.lenya.cms.repository.Node) session
551: .getRepositoryItem(factory, this .sourceUri);
552: } catch (Exception e) {
553: throw new RuntimeException(
554: "Creating repository node failed: ", e);
555: } finally {
556: if (factory != null) {
557: manager.release(factory);
558: }
559: }
560: }
561: return this .repositoryNode;
562: }
563:
564: public void save() throws RepositoryException {
565: try {
566: saveDocument();
567: } catch (SiteException e) {
568: throw new RepositoryException(e);
569: }
570: }
571:
572: public String getArea() {
573: return this .area;
574: }
575:
576: public Publication getPublication() {
577: return this .pub;
578: }
579:
580: public boolean contains(String path) {
581: try {
582: return getNodeInternal(path) != null;
583: } catch (SiteException e) {
584: throw new RuntimeException(e);
585: }
586: }
587:
588: public boolean containsByUuid(String uuid, String language) {
589: return getByUuidInternal(uuid, language) != null;
590: }
591:
592: protected SiteNode getByUuidInternal(String uuid, String language) {
593: String xPath = "//*[@uuid = '" + uuid + "']";
594: SiteNode[] nodes = getNodesByXpath(xPath);
595: for (int i = 0; i < nodes.length; i++) {
596: if (nodes[i].hasLink(language)) {
597: return nodes[i];
598: }
599: }
600: return null;
601: }
602:
603: protected SiteNode getNodeByXpath(String xPath) {
604: try {
605: Element element = (Element) XPathAPI.selectSingleNode(
606: this .document, xPath);
607: if (element == null) {
608: return null;
609: } else {
610: return new SiteTreeNodeImpl(this .factory, this ,
611: element, getLogger());
612: }
613: } catch (Exception e) {
614: throw new RuntimeException(e);
615: }
616: }
617:
618: protected SiteNode[] getNodesByXpath(String xPath) {
619: try {
620: NodeList list = XPathAPI.selectNodeList(this .document,
621: xPath);
622: SiteNode[] nodes = new SiteNode[list.getLength()];
623: for (int i = 0; i < nodes.length; i++) {
624: Element element = (Element) list.item(i);
625: nodes[i] = new SiteTreeNodeImpl(this .factory, this ,
626: element, getLogger());
627: }
628: return nodes;
629: } catch (Exception e) {
630: throw new RuntimeException(e);
631: }
632: }
633:
634: public Link getByUuid(String uuid, String language)
635: throws SiteException {
636: SiteNode node = getByUuidInternal(uuid, language);
637: if (node == null) {
638: throw new SiteException("The link for [" + uuid + ":"
639: + language + "] is not contained!");
640: }
641: return node.getLink(language);
642: }
643:
644: protected DocumentFactory getFactory() {
645: return this .factory;
646: }
647:
648: public Link add(String path,
649: org.apache.lenya.cms.publication.Document doc)
650: throws SiteException {
651:
652: if (contains(path)) {
653: SiteNode node = getNode(path);
654: if (node.getLanguages().length > 0
655: && !node.getUuid().equals(doc.getUUID())) {
656: throw new SiteException("Node for path [" + path
657: + "] exists with different UUID!");
658: }
659: }
660:
661: SiteTreeNodeImpl node = addNode(path, doc.getUUID(), true,
662: null, "", false);
663: node.addLabel(doc.getLanguage(), "");
664:
665: if (node.getLanguages().length == 1) {
666: node.setUUID(doc.getUUID());
667: }
668:
669: return node.getLink(doc.getLanguage());
670: }
671:
672: public SiteNode add(String path) throws SiteException {
673: SiteTreeNode node = addNode(path, null, true, null, "", false);
674: return node;
675: }
676:
677: public boolean containsInAnyLanguage(String uuid) {
678: String xPath = "//*[@uuid = '" + uuid + "']";
679: return getNodeByXpath(xPath) != null;
680: }
681:
682: public SiteNode[] getNodes() {
683: List nodes = getRootNode().preOrder();
684: nodes.remove(getRootNode());
685: return (SiteNode[]) nodes.toArray(new SiteNode[nodes.size()]);
686: }
687:
688: public SiteNode add(String path, String followingSiblingPath)
689: throws SiteException {
690: SiteTreeNode node = addNode(path, null, true, null, "", false,
691: followingSiblingPath);
692: return node;
693: }
694:
695: public SiteNode[] getTopLevelNodes() {
696: return getRootNode().getChildren();
697: }
698:
699: protected SiteTreeNodeImpl getRootNode() {
700: SiteTreeNodeImpl root;
701: try {
702: root = (SiteTreeNodeImpl) getNode("/");
703: } catch (SiteException e) {
704: throw new RuntimeException(e);
705: }
706: return root;
707: }
708:
709: public boolean contains(String path, String language) {
710: if (contains(path)) {
711: SiteNode node;
712: try {
713: node = getNode(path);
714: } catch (SiteException e) {
715: throw new RuntimeException(e);
716: }
717: return node.hasLink(language);
718: }
719: return false;
720: }
721:
722: public Session getSession() {
723: return getRepositoryNode().getSession();
724: }
725:
726: public SiteNode[] preOrder() {
727: List preOrder = getRootNode().preOrder();
728: return (SiteNode[]) preOrder.toArray(new SiteNode[preOrder
729: .size()]);
730: }
731:
732: public void changed() {
733: this .changed = true;
734: }
735:
736: }
|