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: package org.apache.lenya.cms.site.tree2;
019:
020: import java.util.HashMap;
021: import java.util.Iterator;
022: import java.util.Map;
023: import java.util.Set;
024:
025: import org.apache.avalon.framework.container.ContainerUtil;
026: import org.apache.avalon.framework.logger.AbstractLogEnabled;
027: import org.apache.avalon.framework.logger.Logger;
028: import org.apache.avalon.framework.service.ServiceException;
029: import org.apache.avalon.framework.service.ServiceManager;
030: import org.apache.lenya.cms.publication.Area;
031: import org.apache.lenya.cms.publication.Document;
032: import org.apache.lenya.cms.publication.Publication;
033: import org.apache.lenya.cms.repository.Node;
034: import org.apache.lenya.cms.repository.NodeFactory;
035: import org.apache.lenya.cms.repository.Persistable;
036: import org.apache.lenya.cms.repository.RepositoryException;
037: import org.apache.lenya.cms.repository.Session;
038: import org.apache.lenya.cms.site.Link;
039: import org.apache.lenya.cms.site.SiteException;
040: import org.apache.lenya.cms.site.SiteNode;
041: import org.apache.lenya.cms.site.SiteStructure;
042: import org.apache.lenya.cms.site.tree.SiteTree;
043: import org.apache.lenya.util.Assert;
044: import org.apache.lenya.xml.DocumentHelper;
045: import org.apache.lenya.xml.NamespaceHelper;
046: import org.w3c.dom.Element;
047:
048: /**
049: * Simple site tree implementation.
050: */
051: public class SiteTreeImpl extends AbstractLogEnabled implements
052: SiteStructure, SiteTree, Persistable {
053:
054: private Area area;
055: protected ServiceManager manager;
056: private RootNode root;
057:
058: /**
059: * @param manager The service manager.
060: * @param area The area.
061: * @param logger The logger.
062: */
063: public SiteTreeImpl(ServiceManager manager, Area area, Logger logger) {
064: ContainerUtil.enableLogging(this , logger);
065: this .area = area;
066: this .manager = manager;
067: initRoot();
068: }
069:
070: protected void initRoot() {
071: this .root = new RootNode(this , getLogger());
072: nodeAdded(root);
073: }
074:
075: private String sourceUri;
076:
077: protected String getSourceUri() {
078: if (this .sourceUri == null) {
079: String baseUri = this .area.getPublication().getContentURI(
080: this .area.getName());
081: this .sourceUri = baseUri + "/sitetree.xml";
082: }
083: return this .sourceUri;
084: }
085:
086: private long lastModified = -1;
087: private boolean loading = false;
088:
089: protected static final String NAMESPACE = "http://apache.org/cocoon/lenya/sitetree/1.0";
090:
091: private static final boolean DEFAULT_VISIBILITY = true;
092:
093: private boolean loaded = false;
094:
095: protected synchronized void load() {
096:
097: if (this .loaded) {
098: return;
099: }
100:
101: Node repoNode = getRepositoryNode();
102:
103: try {
104: repoNode.setPersistable(this );
105:
106: // lastModified check is necessary for clustering, but can cause 404s
107: // because of the 1s file system last modification granularity
108: if (repoNode.exists() /* && repoNode.getLastModified() > this.lastModified */) {
109: long lastModified = repoNode.getLastModified();
110: org.w3c.dom.Document xml = DocumentHelper
111: .readDocument(repoNode.getInputStream());
112:
113: NamespaceHelper helper = new NamespaceHelper(NAMESPACE,
114: "", xml);
115: Assert.isTrue("document element is site", xml
116: .getDocumentElement().getLocalName().equals(
117: "site"));
118: this .loading = true;
119: reset();
120: loadNodes(this .root, helper, xml.getDocumentElement());
121: this .loading = false;
122: this .lastModified = lastModified;
123: }
124:
125: if (!repoNode.exists() && this .lastModified > -1) {
126: reset();
127: this .lastModified = -1;
128: }
129:
130: this .loaded = true;
131:
132: } catch (Exception e) {
133: throw new RuntimeException(e);
134: }
135: checkInvariants();
136: }
137:
138: protected void reset() {
139: this .path2node.clear();
140: this .uuidLanguage2link.clear();
141: initRoot();
142: }
143:
144: protected RootNode getRoot() {
145: load();
146: return this .root;
147: }
148:
149: protected void loadNodes(TreeNode parent, NamespaceHelper helper,
150: Element element) {
151: Element[] nodeElements = helper.getChildren(element, "node");
152: for (int n = 0; n < nodeElements.length; n++) {
153: String name = nodeElements[n].getAttribute("id");
154: boolean visible = DEFAULT_VISIBILITY;
155: if (nodeElements[n].hasAttribute("visibleinnav")) {
156: String visibleString = nodeElements[n]
157: .getAttribute("visibleinnav");
158: visible = Boolean.valueOf(visibleString).booleanValue();
159: }
160: TreeNodeImpl node = (TreeNodeImpl) parent.addChild(name,
161: visible);
162: if (nodeElements[n].hasAttribute("uuid")) {
163: String uuid = nodeElements[n].getAttribute("uuid");
164: node.setUuid(uuid);
165: }
166: loadLinks(node, helper, nodeElements[n]);
167: loadNodes(node, helper, nodeElements[n]);
168: }
169: }
170:
171: protected void loadLinks(TreeNodeImpl node, NamespaceHelper helper,
172: Element element) {
173: Element[] linkElements = helper.getChildren(element, "label");
174: for (int l = 0; l < linkElements.length; l++) {
175: String lang = linkElements[l].getAttribute("xml:lang");
176: String label = DocumentHelper
177: .getSimpleElementText(linkElements[l]);
178: node.addLink(lang, label);
179: }
180: }
181:
182: public synchronized void save() throws RepositoryException {
183: if (loading || !changed) {
184: return;
185: }
186: try {
187: Node repoNode = getRepositoryNode();
188: NamespaceHelper helper = new NamespaceHelper(NAMESPACE, "",
189: "site");
190:
191: int revision = getRevision(repoNode) + 1;
192: helper.getDocument().getDocumentElement().setAttribute(
193: "revision", Integer.toString(revision));
194:
195: saveNodes(getRoot(), helper, helper.getDocument()
196: .getDocumentElement());
197: helper.save(repoNode.getOutputStream());
198: this .lastModified = repoNode.getLastModified();
199: } catch (RuntimeException e) {
200: throw e;
201: } catch (Exception e) {
202: throw new RepositoryException(e);
203: }
204:
205: }
206:
207: protected int getRevision(Node repoNode) {
208: int revision = 0;
209: if (repoNode.getHistory().getRevisionNumbers().length > 0) {
210: revision = repoNode.getHistory().getLatestRevision()
211: .getNumber();
212: }
213: return revision;
214: }
215:
216: protected void saveNodes(TreeNode parent, NamespaceHelper helper,
217: Element parentElement) throws SiteException {
218: SiteNode[] children = parent.getChildren();
219: for (int i = 0; i < children.length; i++) {
220: Element nodeElement = helper.createElement("node");
221: nodeElement.setAttribute("id", children[i].getName());
222: String uuid = children[i].getUuid();
223: if (uuid != null) {
224: nodeElement.setAttribute("uuid", uuid);
225: }
226: nodeElement.setAttribute("visibleinnav", Boolean
227: .toString(children[i].isVisible()));
228: saveLinks(children[i], helper, nodeElement);
229: saveNodes((TreeNode) children[i], helper, nodeElement);
230: parentElement.appendChild(nodeElement);
231: }
232: }
233:
234: protected void saveLinks(SiteNode node, NamespaceHelper helper,
235: Element nodeElement) throws SiteException {
236: String[] languages = node.getLanguages();
237: for (int i = 0; i < languages.length; i++) {
238: Link link = node.getLink(languages[i]);
239: Element linkElement = helper.createElement("label", link
240: .getLabel());
241: linkElement.setAttribute("xml:lang", languages[i]);
242: nodeElement.appendChild(linkElement);
243: }
244: }
245:
246: public Link add(String path, Document doc) throws SiteException {
247:
248: if (containsByUuid(doc.getUUID(), doc.getLanguage())) {
249: throw new SiteException("The document [" + doc
250: + "] is already contained!");
251: }
252:
253: TreeNodeImpl node;
254: if (contains(path)) {
255: node = getTreeNode(path);
256: if (node.getUuid() == null) {
257: node.setUuid(doc.getUUID());
258: } else if (!node.getUuid().equals(doc.getUUID())) {
259: throw new SiteException(
260: "The node already has a different UUID!");
261: }
262: } else {
263: node = (TreeNodeImpl) add(path);
264: node.setUuid(doc.getUUID());
265: }
266: return node.addLink(doc.getLanguage(), "");
267: }
268:
269: protected TreeNodeImpl getTreeNode(String path)
270: throws SiteException {
271: return (TreeNodeImpl) getNode(path);
272: }
273:
274: public SiteNode add(String path) throws SiteException {
275: String parentPath = getParentPath(path);
276: String nodeName = path.substring(parentPath.length() + 1);
277: if (!contains(parentPath)) {
278: add(parentPath);
279: }
280: return getTreeNode(parentPath).addChild(nodeName,
281: DEFAULT_VISIBILITY);
282: }
283:
284: public SiteNode add(String path, String followingSiblingPath)
285: throws SiteException {
286: String parentPath = getParentPath(path);
287: String nodeName = path.substring(parentPath.length() + 1);
288:
289: if (!followingSiblingPath.startsWith(parentPath + "/")) {
290: throw new SiteException("Invalid following sibling path ["
291: + followingSiblingPath + "]");
292: }
293:
294: String followingNodeName = followingSiblingPath
295: .substring(parentPath.length() + 1);
296:
297: if (!contains(parentPath)) {
298: add(parentPath);
299: }
300: return getTreeNode(parentPath).addChild(nodeName,
301: followingNodeName, DEFAULT_VISIBILITY);
302: }
303:
304: protected String getParentPath(String path) {
305: int lastIndex = path.lastIndexOf("/");
306: String parentPath = path.substring(0, lastIndex);
307: return parentPath;
308: }
309:
310: private Map path2node = new HashMap();
311: private Map uuidLanguage2link = new HashMap();
312:
313: protected void nodeAdded(SiteNode node) {
314: String path = node.getPath();
315: Assert.notNull("path", path);
316: if (node != this .root) {
317: Assert.isTrue("path not empty", path.length() > 0);
318: }
319: this .path2node.put(path, node);
320: }
321:
322: protected void linkAdded(Link link) {
323: if (link.getNode().getUuid() != null) {
324: this .uuidLanguage2link.put(getKey(link), link);
325: }
326: }
327:
328: protected String getKey(Link link) {
329: String uuid = link.getDocument().getUUID();
330: Assert.notNull("uuid", uuid);
331: String language = link.getLanguage();
332: Assert.notNull("language", language);
333: return getKey(uuid, language);
334: }
335:
336: protected String getKey(String uuid, String language) {
337: Assert.notNull("uuid", uuid);
338: Assert.notNull("language", language);
339: return uuid + ":" + language;
340: }
341:
342: protected void nodeRemoved(String path) {
343: Assert.notNull("path", path);
344: Assert.isTrue("path [" + path + "] contained", this .path2node
345: .containsKey(path));
346: this .path2node.remove(path);
347: }
348:
349: protected Map getUuidLanguage2Link() {
350: load();
351: return this .uuidLanguage2link;
352: }
353:
354: protected Map getPath2Node() {
355: load();
356: return this .path2node;
357: }
358:
359: public boolean contains(String path) {
360: load();
361: Assert.notNull("path", path);
362: return this .path2node.containsKey(path);
363: }
364:
365: public boolean containsByUuid(String uuid, String language) {
366: Assert.notNull("uuid", uuid);
367: Assert.notNull("language", language);
368: return getUuidLanguage2Link().containsKey(
369: getKey(uuid, language));
370: }
371:
372: public boolean containsInAnyLanguage(String uuid) {
373: Assert.notNull("uuid", uuid);
374: Set set = getUuidLanguage2Link().keySet();
375: String[] keys = (String[]) set.toArray(new String[set.size()]);
376: for (int i = 0; i < keys.length; i++) {
377: if (keys[i].startsWith(uuid + ":")) {
378: return true;
379: }
380: }
381: return false;
382: }
383:
384: public String getArea() {
385: return this .area.getName();
386: }
387:
388: public Link getByUuid(String uuid, String language)
389: throws SiteException {
390: Assert.notNull("uuid", uuid);
391: Assert.notNull("language", language);
392: String key = getKey(uuid, language);
393: if (!getUuidLanguage2Link().containsKey(key)) {
394: throw new SiteException("No link for [" + key + "]");
395: }
396: return (Link) getUuidLanguage2Link().get(key);
397: }
398:
399: protected void checkInvariants() {
400: if (true) {
401: return;
402: }
403: for (Iterator paths = this .path2node.keySet().iterator(); paths
404: .hasNext();) {
405: String path = (String) paths.next();
406: SiteNode node = (SiteNode) this .path2node.get(path);
407: String uuid = node.getUuid();
408: if (uuid != null) {
409: String[] langs = node.getLanguages();
410: for (int i = 0; i < langs.length; i++) {
411: String key = getKey(uuid, langs[i]);
412: Assert.isTrue("contains link for [" + key + "]",
413: this .uuidLanguage2link.containsKey(key));
414: }
415: }
416: }
417: for (Iterator keys = this .uuidLanguage2link.keySet().iterator(); keys
418: .hasNext();) {
419: String key = (String) keys.next();
420: Link link = (Link) this .uuidLanguage2link.get(key);
421: Assert.isTrue("contains path for [" + key + "]",
422: this .path2node
423: .containsKey(link.getNode().getPath()));
424: }
425:
426: }
427:
428: public SiteNode getNode(String path) throws SiteException {
429: Assert.notNull("path", path);
430: if (!getPath2Node().containsKey(path)) {
431: throw new SiteException("No node for path [" + path + "]");
432: }
433: return (SiteNode) this .path2node.get(path);
434: }
435:
436: public SiteNode[] getNodes() {
437: return getRoot().preOrder();
438: }
439:
440: public Publication getPublication() {
441: return this .area.getPublication();
442: }
443:
444: public Session getSession() {
445: return this .area.getPublication().getFactory().getSession();
446: }
447:
448: private NodeFactory nodeFactory;
449: private boolean changed = false;
450:
451: protected NodeFactory getNodeFactory() {
452: if (this .nodeFactory == null) {
453: try {
454: this .nodeFactory = (NodeFactory) manager
455: .lookup(NodeFactory.ROLE);
456: } catch (ServiceException e) {
457: throw new RuntimeException(
458: "Creating repository node failed: ", e);
459: }
460: }
461: return this .nodeFactory;
462: }
463:
464: public Node getRepositoryNode() {
465: try {
466: return (Node) getSession().getRepositoryItem(
467: getNodeFactory(), getSourceUri());
468: } catch (RepositoryException e) {
469: throw new RuntimeException(
470: "Creating repository node failed: ", e);
471: }
472: }
473:
474: public SiteNode[] getTopLevelNodes() {
475: return getRoot().getChildren();
476: }
477:
478: protected void linkRemoved(String uuid, String language) {
479: Assert.notNull("uuid", uuid);
480: Assert.notNull("language", language);
481: String key = getKey(uuid, language);
482: Assert.isTrue("contained", this .uuidLanguage2link
483: .containsKey(key));
484: this .uuidLanguage2link.remove(key);
485: }
486:
487: protected String getPath() {
488: return "";
489: }
490:
491: /**
492: * @return The nodes in pre-order enumeration.
493: */
494: public SiteNode[] preOrder() {
495: return getRoot().preOrder();
496: }
497:
498: public void moveDown(String path) throws SiteException {
499: TreeNode node = getTreeNode(path);
500: TreeNode parent = getParent(node);
501: parent.moveDown(node.getName());
502:
503: }
504:
505: public void moveUp(String path) throws SiteException {
506: TreeNode node = getTreeNode(path);
507: TreeNode parent = getParent(node);
508: parent.moveUp(node.getName());
509: }
510:
511: /**
512: * @param node A node.
513: * @return The parent of the node, which is the root node for top level nodes.
514: * @throws SiteException if an error occurs.
515: */
516: protected TreeNode getParent(TreeNode node) throws SiteException {
517: TreeNode parent;
518: if (node.isTopLevel()) {
519: parent = getRoot();
520: } else {
521: parent = (TreeNode) node.getParent();
522: }
523: return parent;
524: }
525:
526: public boolean contains(String path, String language) {
527: if (contains(path)) {
528: SiteNode node;
529: try {
530: node = getNode(path);
531: } catch (SiteException e) {
532: throw new RuntimeException(e);
533: }
534: return node.hasLink(language);
535: }
536: return false;
537: }
538:
539: protected void changed() {
540: if (!this .loading) {
541: this .changed = true;
542: }
543: }
544:
545: public boolean isModified() {
546: return this.changed;
547: }
548: }
|