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        assert !roots.isEmpty();
137
138        this.encoder = encoder;
139        this.adapter = adapter;
140        this.roots = F.flow(roots).map(toTreeNode).toList();
141    }
142
143    public List<TreeNode<T>> getRootNodes()
144    {
145        return roots;
146    }
147
148    public TreeNode<T> getById(String id)
149    {
150        assert id != null;
151
152        TreeNode<T> result = findById(id);
153
154        if (result == null)
155            throw new IllegalArgumentException(String.format("Could not locate TreeNode '%s'.", id));
156
157        return result;
158    }
159
160    private TreeNode<T> findById(String id)
161    {
162        TreeNode<T> result = cache.get(id);
163
164        if (result != null)
165            return result;
166
167        LinkedList<TreeNode<T>> queue = new LinkedList<TreeNode<T>>(roots);
168
169        while (!queue.isEmpty())
170        {
171            TreeNode<T> node = queue.removeFirst();
172
173            String nodeId = node.getId();
174
175            cache.put(nodeId, node);
176
177            if (nodeId.equals(id))
178                return node;
179
180            if (!node.isLeaf() && node.getHasChildren())
181            {
182                for (TreeNode<T> child : node.getChildren())
183                {
184                    queue.addFirst(child);
185                }
186            }
187        }
188
189        return null;
190    }
191
192    public TreeNode<T> find(T element)
193    {
194        return findById(encoder.toClient(element));
195    }
196
197}