001// Licensed under the Apache License, Version 2.0 (the "License");
002// you may not use this file except in compliance with the License.
003// You may obtain a copy of the License at
004//
005// http://www.apache.org/licenses/LICENSE-2.0
006//
007// Unless required by applicable law or agreed to in writing, software
008// distributed under the License is distributed on an "AS IS" BASIS,
009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
010// See the License for the specific language governing permissions and
011// limitations under the License.
012
013package org.apache.tapestry5.tree;
014
015import org.apache.tapestry5.ValueEncoder;
016import org.apache.tapestry5.commons.util.CollectionFactory;
017import org.apache.tapestry5.func.F;
018import org.apache.tapestry5.func.Mapper;
019
020import java.util.Collections;
021import java.util.LinkedList;
022import java.util.List;
023import java.util.Map;
024
025/**
026 * A default implementation of TreeModel that starts with a {@link ValueEncoder} (for the element to string conversion),
027 * a {@link TreeModelAdapter}, and a list of root nodes.
028 *
029 * This implementation is <em>not</em> thread safe.
030 *
031 * @param <T> the type of data in the tree
032 * @since 5.3
033 */
034@SuppressWarnings({"UnusedDeclaration"})
035public class DefaultTreeModel<T> implements TreeModel<T>
036{
037    private final ValueEncoder<T> encoder;
038
039    private final TreeModelAdapter<T> adapter;
040
041    private final List<TreeNode<T>> roots;
042
043    private final Map<String, TreeNode<T>> cache = CollectionFactory.newMap();
044
045    private final Mapper<T, TreeNode<T>> toTreeNode = new Mapper<T, TreeNode<T>>()
046    {
047        public TreeNode<T> map(T value)
048        {
049            return new DefaultTreeNode(value);
050        }
051    };
052
053    private class DefaultTreeNode implements TreeNode<T>
054    {
055        private final T value;
056
057        private List<TreeNode<T>> children;
058
059        DefaultTreeNode(T value)
060        {
061            this.value = value;
062        }
063
064        public String getId()
065        {
066            return encoder.toClient(value);
067        }
068
069        public T getValue()
070        {
071            return value;
072        }
073
074        public boolean isLeaf()
075        {
076            return adapter.isLeaf(value);
077        }
078
079        public boolean getHasChildren()
080        {
081            return adapter.hasChildren(value);
082        }
083
084        public List<TreeNode<T>> getChildren()
085        {
086            if (children == null)
087            {
088                List<T> childValues = adapter.getChildren(value);
089
090                boolean empty = childValues == null || childValues.isEmpty();
091
092                children = empty
093                        ? emptyTreeNodeList()
094                        : F.flow(childValues).map(toTreeNode).toList();
095            }
096
097            return children;
098        }
099
100        public String getLabel()
101        {
102            return adapter.getLabel(value);
103        }
104
105        private List<TreeNode<T>> emptyTreeNodeList()
106        {
107            return Collections.emptyList();
108        }
109
110    }
111
112    /**
113     * Creates a new model starting from a single root element.
114     *
115     * @param encoder used to convert values to strings and vice-versa
116     * @param adapter adapts elements to the tree
117     * @param root    defines the root node of the model
118     */
119    public DefaultTreeModel(ValueEncoder<T> encoder, TreeModelAdapter<T> adapter, T root)
120    {
121        this(encoder, adapter, Collections.singletonList(root));
122    }
123
124    /**
125     * Standard constructor.
126     *
127     * @param encoder used to convert values to strings and vice-versa
128     * @param adapter adapts elements to the tree
129     * @param roots   defines the root nodes of the model
130     */
131    public DefaultTreeModel(ValueEncoder<T> encoder, TreeModelAdapter<T> adapter, List<T> roots)
132    {
133        assert encoder != null;
134        assert adapter != null;
135        assert roots != null;
136
137        this.encoder = encoder;
138        this.adapter = adapter;
139        this.roots = F.flow(roots).map(toTreeNode).toList();
140    }
141
142    public List<TreeNode<T>> getRootNodes()
143    {
144        return roots;
145    }
146
147    public TreeNode<T> getById(String id)
148    {
149        assert id != null;
150
151        TreeNode<T> result = findById(id);
152
153        if (result == null)
154            throw new IllegalArgumentException(String.format("Could not locate TreeNode '%s'.", id));
155
156        return result;
157    }
158
159    private TreeNode<T> findById(String id)
160    {
161        TreeNode<T> result = cache.get(id);
162
163        if (result != null)
164            return result;
165
166        LinkedList<TreeNode<T>> queue = new LinkedList<TreeNode<T>>(roots);
167
168        while (!queue.isEmpty())
169        {
170            TreeNode<T> node = queue.removeFirst();
171
172            String nodeId = node.getId();
173
174            cache.put(nodeId, node);
175
176            if (nodeId.equals(id))
177                return node;
178
179            if (!node.isLeaf() && node.getHasChildren())
180            {
181                for (TreeNode<T> child : node.getChildren())
182                {
183                    queue.addFirst(child);
184                }
185            }
186        }
187
188        return null;
189    }
190
191    public TreeNode<T> find(T element)
192    {
193        return findById(encoder.toClient(element));
194    }
195
196}