/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* 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.gatein.api.navigation;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.exoplatform.commons.utils.ExpressionUtil;
import org.exoplatform.portal.mop.Described;
import org.exoplatform.portal.mop.navigation.NodeChange;
import org.exoplatform.portal.mop.navigation.NodeContext;
import org.exoplatform.portal.mop.navigation.NodeState;
import org.exoplatform.portal.mop.navigation.NodeState.Builder;
import org.gatein.api.EntityAlreadyExistsException;
import org.gatein.api.EntityNotFoundException;
import org.gatein.api.Portal;
import org.gatein.api.PortalRequest;
import org.gatein.api.Util;
import org.gatein.api.common.Attributes;
import org.gatein.api.common.i18n.LocalizedString;
import org.gatein.api.internal.ObjectToStringBuilder;
import org.gatein.api.internal.Parameters;
import org.gatein.api.navigation.Visibility.Status;
import org.gatein.api.page.PageId;
import org.gatein.api.site.SiteId;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
* @author <a href="mailto:nscavell@redhat.com">Nick Scavelli</a>
*/
public class ApiNode implements Node {
transient NodeContext<ApiNode> context;
transient NavigationImpl navigation;
private LocalizedString displayName;
private String resolvedDisplayName;
private String resolvedURI;
private boolean displayNameChanged;
private Attributes attributes;
private final SiteId siteId;
ApiNode(NavigationImpl navigation, NodeContext<ApiNode> context) {
this.navigation = navigation;
this.siteId = navigation.getSiteId();
this.context = context;
}
@Override
public Node addChild(int index, String childName) {
checkChildrenLoaded();
Parameters.requireNonNull(childName, "childName");
return context.add(index, childName).getNode();
}
@Override
public Node addChild(String childName) {
checkChildrenLoaded();
Parameters.requireNonNull(childName, "childName");
if (hasChild(childName)) {
throw new EntityAlreadyExistsException("child " + childName + " already exists");
}
return context.add(null, childName).getNode();
}
@Override
public FilteredNode filter() {
return new ApiFilteredNode(navigation, context);
}
@Override
public Node getChild(int index) {
checkChildrenLoaded();
return context.isExpanded() ? context.getNode(index) : null;
}
@Override
public Node getChild(String childName) {
Parameters.requireNonNull(childName, "childName");
checkChildrenLoaded();
return context.isExpanded() ? context.getNode(childName) : null;
}
@Override
public int getChildCount() {
checkChildrenLoaded();
return context.getNodeSize();
}
@Override
public Node getNode(String... nodePath) {
return getNode(NodePath.path(nodePath));
}
@Override
public Node getNode(NodePath nodePath) {
checkChildrenLoaded();
Parameters.requireNonNull(nodePath, "nodePath");
Node node = this;
for (String name : nodePath) {
node = node.getChild(name);
if (node == null)
return null;
}
return node;
}
@Override
public String getIconName() {
return context.getState().getIcon();
}
@Override
public String getDisplayName() {
if (resolvedDisplayName == null) {
// Avoid any resolving if we're dealing with a simple non resource-bundle label
String simple = context.getState().getLabel();
if (simple != null && !ExpressionUtil.isResourceBindingExpression(simple)) {
resolvedDisplayName = simple;
} else {
resolvedDisplayName = navigation.resolve(context);
}
}
return resolvedDisplayName;
}
@Override
public LocalizedString getDisplayNames() {
if (displayName == null) {
String simple = context.getState().getLabel();
if (simple != null) {
displayName = new LocalizedString(simple);
} else if (context.getId() != null) {
Map<Locale, Described.State> descriptions = navigation.loadDescriptions(context.getId());
displayName = ObjectFactory.createLocalizedString(descriptions);
}
}
return displayName;
}
@Override
public String getName() {
return isRoot() ? null : context.getName();
}
@Override
public NodePath getNodePath() {
String name = getName();
ApiNode parent = context.getParentNode();
NodePath path = isRoot() ? NodePath.root() : NodePath.path(name);
if (parent != null) {
path = parent.getNodePath().append(path);
}
return path;
}
@Override
public PageId getPageId() {
return Util.from(context.getState().getPageRef());
}
@Override
public Node getParent() {
return context.getParentNode();
}
/**
* @see org.gatein.api.navigation.Node#getAttributes()
*/
@Override
public Attributes getAttributes() {
if (attributes == null) {
attributes = new ApiNodeAttributes(context);
}
return attributes;
}
@Override
public String getURI() {
if (resolvedURI == null) {
if (isRoot()) {
resolvedURI = PortalRequest.getInstance().getURIResolver().resolveURI(siteId);
} else {
resolvedURI = getParent().getURI() + "/" + getName();
}
}
return resolvedURI;
}
void clearCached() {
resolvedDisplayName = null;
resolvedURI = null;
}
@Override
public Visibility getVisibility() {
return ObjectFactory.createVisibility(context.getState());
}
@Override
public boolean hasChild(String childName) {
return getChild(childName) != null;
}
@Override
public int indexOf(String childName) {
ApiNode node = (ApiNode) getChild(childName);
return node == null ? -1 : node.context.getIndex();
}
@Override
public boolean isChildrenLoaded() {
return context.isExpanded();
}
@Override
public boolean isRoot() {
return context.getParent() == null;
}
@Override
public boolean isVisible() {
return getVisibility().isVisible();
}
@Override
public Iterator<Node> iterator() {
return new ApiNodeModelIterator();
}
@Override
public void moveTo(int index) {
checkNonRoot();
if (context.getIndex() < index) {
index++;
}
context.getParent().add(index, context);
}
@Override
public void moveTo(int index, Node parent) {
moveTo(new Integer(index), parent);
}
@Override
public void moveTo(Node parent) {
moveTo(null, parent);
}
private void moveTo(Integer index, Node parent) {
checkNonRoot();
((ApiNode) parent).checkChildrenLoaded();
if (this.getNodePath().isParent(parent.getNodePath())) {
throw new IllegalArgumentException("Can't move node to a child node of itself");
}
Node root = this;
while (root.getParent() != null) {
root = root.getParent();
}
if (((ApiNode) root.getNode(parent.getNodePath())).getContext() != ((ApiNode) parent).getContext()) {
throw new IllegalArgumentException("Can't move node to a different branch");
}
((ApiNode) parent).context.add(index, context);
}
@Override
public boolean removeChild(String childName) {
checkChildrenLoaded();
if (!hasChild(childName)) {
throw new EntityNotFoundException("Cannot remove child '" + childName + "' because it does not exist for parent node " + getNodePath());
}
return context.removeNode(childName);
}
@Override
public void setName(String name) throws IllegalArgumentException {
Parameters.requireNonNull(name, "name");
context.setName(name);
}
@Override
public void setIconName(String iconName) {
checkNonRoot();
setState(getStateBuilder().icon(iconName));
}
@Override
public void setDisplayName(String displayName) {
Parameters.requireNonNull(displayName, "displayName");
setDisplayNames(new LocalizedString(displayName));
}
@Override
public void setDisplayNames(LocalizedString displayName) {
checkNonRoot();
if (displayName == null && this.displayName == null)
return;
if (displayName != null || !this.displayName.equals(displayName)) {
if (displayName != null && !displayName.isLocalized()) {
setState(getStateBuilder().label(displayName.getValue()));
} else {
setState(getStateBuilder().label(null));
}
this.displayName = displayName;
this.resolvedDisplayName = null;
displayNameChanged = true;
}
}
@Override
public void setPageId(PageId pageId) {
checkNonRoot();
setState(getStateBuilder().pageRef(Util.from(pageId)));
}
@Override
public void setVisibility(boolean visible) {
checkNonRoot();
Builder b = getStateBuilder();
if (visible) {
b.visibility(org.exoplatform.portal.mop.Visibility.DISPLAYED);
} else {
b.visibility(org.exoplatform.portal.mop.Visibility.HIDDEN);
}
setState(b);
}
@Override
public void setVisibility(PublicationDate publicationDate) {
checkNonRoot();
Parameters.requireNonNull(publicationDate, "publicationDate");
long start = publicationDate.getStart() != null ? publicationDate.getStart().getTime() : -1;
long end = publicationDate.getEnd() != null ? publicationDate.getEnd().getTime() : -1;
setState(getStateBuilder().startPublicationTime(start).endPublicationTime(end)
.visibility(org.exoplatform.portal.mop.Visibility.TEMPORAL));
}
@Override
public void setVisibility(Visibility visibility) {
checkNonRoot();
Parameters.requireNonNull(visibility, "visibility");
if (visibility.getStatus() == Status.PUBLICATION) {
setVisibility(visibility.getPublicationDate());
} else {
setState(getStateBuilder().startPublicationTime(-1).endPublicationTime(-1)
.visibility(ObjectFactory.createVisibility(visibility.getStatus())));
}
}
@Override
public void sort(Comparator<Node> comparator) {
Parameters.requireNonNull(comparator, "comparator");
if (context.isExpanded()) {
ApiNode[] a = new ApiNode[context.getNodeSize()];
for (NodeContext<ApiNode> c = context.getFirst(); c != null; c = c.getNext()) {
a[c.getIndex()] = c.getNode();
}
Arrays.sort(a, comparator);
for (int i = 0; i < a.length; i++) {
ApiNode n = a[i];
NodeContext<ApiNode> c = n.getContext();
if (c.getIndex() != i) {
c.getNode().moveTo(i);
}
}
}
}
@Override
public String toString() {
return ObjectToStringBuilder.toStringBuilder(getClass()).add("name", getName()).add("path", getNodePath())
.add("visibility", getVisibility()).add("iconName", getIconName()).add("pageId", getPageId()).toString();
}
NodeContext<ApiNode> getContext() {
return context;
}
SiteId getSiteId() {
return siteId;
}
boolean isDisplayNameChanged() {
return displayNameChanged;
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// deserialize serialization only fields
NodePath nodePath = (NodePath) in.readObject();
ApiNode parent = (ApiNode) in.readObject();
boolean expanded = in.readBoolean();
PortalRequest request = PortalRequest.getInstance();
Portal portal = (request == null) ? null : request.getPortal();
if (portal != null) {
navigation = (NavigationImpl) portal.getNavigation(siteId);
if (navigation == null)
throw new IOException("Could not retrieve navigation for site " + siteId);
} else {
throw new IOException("Could not retrieve portal API during deserialization.");
}
if (parent != null) {
context = parent.context.get(nodePath.getLastSegment());
if (expanded) {
navigation.rebaseNodeContext(context, new NodeVisitorScope(Nodes.visitChildren()), null);
}
} else {
NodeVisitor visitor = (expanded) ? Nodes.visitChildren() : Nodes.visitNone();
context = navigation.getNodeContext(nodePath, visitor);
}
if (expanded && parent == null) {
MultiPathNodeVisitor visitor = new MultiPathNodeVisitor();
readTree(visitor, in);
navigation.rebaseNodeContext(context, new NodeVisitorScope(visitor), null);
}
boolean hasChanges = in.readBoolean();
if (hasChanges && parent == null) { // re-apply changes from root node
@SuppressWarnings("unchecked")
List<ApiNodeChange> changes = (List<ApiNodeChange>) in.readObject();
for (ApiNodeChange change : changes) {
change.apply(this);
}
}
}
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
// write serialization only fields
out.writeObject(getNodePath());
ApiNode parent = (context.getParent() != null) ? context.getParent().getNode() : null;
out.writeObject(parent);
boolean expanded = context.isExpanded();
out.writeBoolean(expanded);
if (expanded && parent == null) { // rebuild tree (using node paths)
writeTree(this, out);
}
// serialize uncommitted changes
boolean hasChanges = context.hasChanges() || displayNameChanged;
out.writeBoolean(hasChanges);
if (hasChanges && parent == null) // ensures we only do this once since the changes are for the entire tree
{
List<ApiNodeChange> changes = new ArrayList<ApiNodeChange>();
for (NodeChange<NodeContext<ApiNode>> change : context.getChanges()) {
if (change instanceof NodeChange.Created) {
NodeChange.Created<NodeContext<ApiNode>> created = (NodeChange.Created<NodeContext<ApiNode>>) change;
changes.add(new ApiNodeChange.Created(created));
} else if (change instanceof NodeChange.Destroyed) {
NodeChange.Destroyed<NodeContext<ApiNode>> destroyed = (NodeChange.Destroyed<NodeContext<ApiNode>>) change;
changes.add(new ApiNodeChange.Destroyed(destroyed));
} else if (change instanceof NodeChange.Moved) {
NodeChange.Moved<NodeContext<ApiNode>> moved = (NodeChange.Moved<NodeContext<ApiNode>>) change;
changes.add(new ApiNodeChange.Moved(moved));
} else if (change instanceof NodeChange.Renamed) {
NodeChange.Renamed<NodeContext<ApiNode>> renamed = (NodeChange.Renamed<NodeContext<ApiNode>>) change;
changes.add(new ApiNodeChange.Renamed(renamed));
} else if (change instanceof NodeChange.Updated) {
NodeChange.Updated<NodeContext<ApiNode>> updated = (NodeChange.Updated<NodeContext<ApiNode>>) change;
changes.add(new ApiNodeChange.Updated(updated));
} else {
throw new IOException("Cannot serialize: Non-compatible node change object " + change);
}
}
out.writeObject(changes);
}
}
private void readTree(MultiPathNodeVisitor visitor, ObjectInputStream in) throws IOException, ClassNotFoundException {
NodePath path = (NodePath) in.readObject();
visitor.add(path);
int count = in.readInt();
for (int i = 0; i < count; i++) {
readTree(visitor, in);
}
}
private void writeTree(Node node, ObjectOutputStream out) throws IOException {
out.writeObject(node.getNodePath());
if (node.isChildrenLoaded()) {
out.writeInt(node.getChildCount());
for (Node child : node) {
writeTree(child, out);
}
} else {
out.writeInt(0);
}
}
private void checkNonRoot() {
if (isRoot()) {
throw new UnsupportedOperationException("Operation not supported on root node");
}
}
private void checkChildrenLoaded() {
if (!context.isExpanded()) {
throw new IllegalStateException("Children are not loaded");
}
}
private Builder getStateBuilder() {
return new NodeState.Builder(context.getState());
}
private void setState(Builder builder) {
context.setState(builder.build());
}
private class ApiNodeModelIterator implements Iterator<Node> {
private Iterator<ApiNode> itr = context.iterator();
private ApiNode last;
@Override
public boolean hasNext() {
return itr.hasNext();
}
@Override
public Node next() {
last = itr.next();
return last;
}
@Override
public void remove() {
if (last == null) {
throw new IllegalStateException();
}
last.context.remove();
}
}
}