0
|
1 |
/****************************************************************************
|
|
2 |
**
|
|
3 |
** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies).
|
|
4 |
** All rights reserved.
|
|
5 |
** Contact: Nokia Corporation (qt-info@nokia.com)
|
|
6 |
**
|
|
7 |
** This file is part of the documentation of the Qt Toolkit.
|
|
8 |
**
|
|
9 |
** $QT_BEGIN_LICENSE:LGPL$
|
|
10 |
** No Commercial Usage
|
|
11 |
** This file contains pre-release code and may not be distributed.
|
|
12 |
** You may use this file in accordance with the terms and conditions
|
|
13 |
** contained in the Technology Preview License Agreement accompanying
|
|
14 |
** this package.
|
|
15 |
**
|
|
16 |
** GNU Lesser General Public License Usage
|
|
17 |
** Alternatively, this file may be used under the terms of the GNU Lesser
|
|
18 |
** General Public License version 2.1 as published by the Free Software
|
|
19 |
** Foundation and appearing in the file LICENSE.LGPL included in the
|
|
20 |
** packaging of this file. Please review the following information to
|
|
21 |
** ensure the GNU Lesser General Public License version 2.1 requirements
|
|
22 |
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
|
|
23 |
**
|
|
24 |
** In addition, as a special exception, Nokia gives you certain additional
|
|
25 |
** rights. These rights are described in the Nokia Qt LGPL Exception
|
|
26 |
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
|
|
27 |
**
|
|
28 |
** If you have questions regarding the use of this file, please contact
|
|
29 |
** Nokia at qt-info@nokia.com.
|
|
30 |
**
|
|
31 |
**
|
|
32 |
**
|
|
33 |
**
|
|
34 |
**
|
|
35 |
**
|
|
36 |
**
|
|
37 |
**
|
|
38 |
** $QT_END_LICENSE$
|
|
39 |
**
|
|
40 |
****************************************************************************/
|
|
41 |
|
|
42 |
/*!
|
|
43 |
\example xmlpatterns/filetree
|
|
44 |
\title File System Example
|
|
45 |
|
|
46 |
This example shows how to use QtXmlPatterns for querying non-XML
|
|
47 |
data that is modeled to look like XML.
|
|
48 |
|
|
49 |
\tableofcontents
|
|
50 |
|
|
51 |
\section1 Introduction
|
|
52 |
|
|
53 |
The example models your computer's file system to look like XML and
|
|
54 |
allows you to query the file system with XQuery. Suppose we want to
|
|
55 |
find all the \c{cpp} files in the subtree beginning at
|
|
56 |
\c{/filetree}:
|
|
57 |
|
|
58 |
\image filetree_1-example.png
|
|
59 |
|
|
60 |
\section2 The User Inteface
|
|
61 |
|
|
62 |
The example is shown below. First, we use \c{File->Open Directory}
|
|
63 |
(not shown) to select the \c{/filetree} directory. Then we use the
|
|
64 |
combobox on the right to select the XQuery that searches for \c{cpp}
|
|
65 |
files (\c{listCPPFiles.xq}). Selecting an XQuery runs the query,
|
|
66 |
which in this case traverses the model looking for all the \c{cpp}
|
|
67 |
files. The XQuery text and the query results are shown on the right:
|
|
68 |
|
|
69 |
\image filetree_2-example.png
|
|
70 |
|
|
71 |
Don't be mislead by the XML representation of the \c{/filetree}
|
|
72 |
directory shown on the left. This is not the node model itself but
|
|
73 |
the XML obtained by traversing the node model and outputting it as
|
|
74 |
XML. Constructing and using the custom node model is explained in
|
|
75 |
the code walk-through.
|
|
76 |
|
|
77 |
\section2 Running your own XQueries
|
|
78 |
|
|
79 |
You can write your own XQuery files and run them in the example
|
|
80 |
program. The file \c{xmlpatterns/filetree/queries.qrc} is the \l{The
|
|
81 |
Qt Resource System} {resource file} for this example. It is used in
|
|
82 |
\c{main.cpp} (\c{Q_INIT_RESOURCE(queries);}). It lists the XQuery
|
|
83 |
files (\c{.xq}) that can be selected in the combobox.
|
|
84 |
|
|
85 |
\quotefromfile examples/xmlpatterns/filetree/queries.qrc
|
|
86 |
\printuntil
|
|
87 |
|
|
88 |
To add your own queries to the example's combobox, store your
|
|
89 |
\c{.xq} files in the \c{examples/xmlpatterns/filetree/queries}
|
|
90 |
directory and add them to \c{queries.qrc} as shown above.
|
|
91 |
|
|
92 |
\section1 Code Walk-Through
|
|
93 |
|
|
94 |
The strategy is to create a custom node model that represents the
|
|
95 |
directory tree of the computer's file system. That tree structure is
|
|
96 |
non-XML data. The custom node model must have the same callback
|
|
97 |
interface as the XML node models that the QtXmlPatterns query engine
|
|
98 |
uses to execute queries. The query engine can then traverse the
|
|
99 |
custom node model as if it were traversing the node model built from
|
|
100 |
an XML document.
|
|
101 |
|
|
102 |
The required callback interface is in QAbstractXmlNodeModel, so we
|
|
103 |
create a custom node model by subclassing QAbstractXmlNodeModel and
|
|
104 |
providing implementations for its pure virtual functions. For many
|
|
105 |
cases, the implementations of several of the virtual functions are
|
|
106 |
always the same, so QtXmlPatterns also provides QSimpleXmlNodeModel,
|
|
107 |
which subclasses QAbstractXmlNodeModel and provides implementations
|
|
108 |
for the callback functions that you can ignore. By subclassing
|
|
109 |
QSimpleXmlNodeModel instead of QAbstractXmlNodeModel, you can reduce
|
|
110 |
development time.
|
|
111 |
|
|
112 |
\section2 The Custom Node Model Class: FileTree
|
|
113 |
|
|
114 |
The custom node model for this example is class \c{FileTree}, which
|
|
115 |
is derived from QSimpleXmlNodeModel. \c{FileTree} implements all the
|
|
116 |
callback functions that don't have standard implementations in
|
|
117 |
QSimpleXmlNodeModel. When you implement your own custom node model,
|
|
118 |
you must provide implementations for these callback functions:
|
|
119 |
|
|
120 |
\snippet examples/xmlpatterns/filetree/filetree.h 0
|
|
121 |
\snippet examples/xmlpatterns/filetree/filetree.h 1
|
|
122 |
|
|
123 |
The \c{FileTree} class declares four data members:
|
|
124 |
|
|
125 |
\snippet examples/xmlpatterns/filetree/filetree.h 2
|
|
126 |
|
|
127 |
The QVector \c{m_fileInfos} will contain the node model. Each
|
|
128 |
QFileInfo in the vector will represent a file or a directory in the
|
|
129 |
file system. At this point it is instructive to note that although
|
|
130 |
the node model class for this example (\c{FileTree}) actually builds
|
|
131 |
and contains the custom node model, building the custom node model
|
|
132 |
isn't always required. The node model class for the \l{QObject XML
|
|
133 |
Model Example} {QObject node model example} does not build its node
|
|
134 |
model but instead uses an already existing QObject tree as its node
|
|
135 |
model and just implements the callback interface for that already
|
|
136 |
existing data structure. In this file system example, however,
|
|
137 |
although we have an already existing data structure, i.e. the file
|
|
138 |
system, that data structure is not in memory and is not in a form we
|
|
139 |
can use. So we must build an analog of the file system in memory
|
|
140 |
from instances of QFileInfo, and we use that analog as the custom
|
|
141 |
node model.
|
|
142 |
|
|
143 |
The two sets of flags, \c{m_filterAllowAll} and \c{m_sortFlags},
|
|
144 |
contain OR'ed flags from QDir::Filters and QDir::SortFlags
|
|
145 |
respectively. They are set by the \c{FileTree} constructor and used
|
|
146 |
in calls to QDir::entryInfoList() for getting the child list for a
|
|
147 |
directory node, i.e. a QFileInfoList containing the file and
|
|
148 |
directory nodes for all the immediate children of a directory.
|
|
149 |
|
|
150 |
The QVector \c{m_names} is an auxiliary component of the node
|
|
151 |
model. It holds the XML element and attribute names (QXmlName) for
|
|
152 |
all the node types that will be found in the node model. \c{m_names}
|
|
153 |
is indexed by the enum \c{FileTree::Type}, which specifies the node
|
|
154 |
types:
|
|
155 |
|
|
156 |
\target Node_Type
|
|
157 |
\snippet examples/xmlpatterns/filetree/filetree.h 4
|
|
158 |
|
|
159 |
\c{Directory} and \c{File} will represent the XML element nodes for
|
|
160 |
directories and files respectively, and the other enum values will
|
|
161 |
represent the XML attribute nodes for a file's path, name, suffix,
|
|
162 |
its size in bytes, and its mime type. The \c{FileTree} constructor
|
|
163 |
initializes \c{m_names} with an appropriate QXmlName for each
|
|
164 |
element and attribute type:
|
|
165 |
|
|
166 |
\snippet examples/xmlpatterns/filetree/filetree.cpp 2
|
|
167 |
|
|
168 |
Note that the constructor does \e{not} pre-build the entire node
|
|
169 |
model. Instead, the node model is built \e{incrementally} as the
|
|
170 |
query engine evaluates a query. To see how the query engine causes
|
|
171 |
the node model to be built incrementally, see \l{Building And
|
|
172 |
Traversing The Node Model}. To see how the query engine accesses the
|
|
173 |
node model, see \l{Accessing the node model}. See also: \l{Node
|
|
174 |
Model Building Strategy}.
|
|
175 |
|
|
176 |
\section3 Accessing The Node Model
|
|
177 |
|
|
178 |
Since the node model is stored outside the query engine in the
|
|
179 |
\c{FileTree} class, the query engine knows nothing about it and can
|
|
180 |
only access it by calling functions in the callback interface. When
|
|
181 |
the query engine calls any callback function to access data in the
|
|
182 |
node model, it passes a QXmlNodeModelIndex to identify the node in
|
|
183 |
the node model that it wants to access. Hence all the virtual
|
|
184 |
functions in the callback interface use a QXmlNodeModelIndex to
|
|
185 |
uniquely identify a node in the model.
|
|
186 |
|
|
187 |
We use the index of a QFileInfo in \c{m_fileInfos} to uniquely
|
|
188 |
identify a node in the node model. To get the QXmlNodeModelIndex for
|
|
189 |
a QFileInfo, the class uses the private function \c{toNodeIndex()}:
|
|
190 |
|
|
191 |
\target main toNodeIndex
|
|
192 |
\snippet examples/xmlpatterns/filetree/filetree.cpp 1
|
|
193 |
|
|
194 |
It searches the \c{m_fileInfos} vector for a QFileInfo that matches
|
|
195 |
\c{fileInfo}. If a match is found, its array index is passed to
|
|
196 |
QAbstractXmlNodeModel::createIndex() as the \c data value for the
|
|
197 |
QXmlNodeIndex. If no match is found, the unmatched QFileInfo is
|
|
198 |
appended to the vector, so this function is also doing the actual
|
|
199 |
incremental model building (see \l{Building And Traversing The Node
|
|
200 |
Model}).
|
|
201 |
|
|
202 |
Note that \c{toNodeIndex()} gets a \l{Node_Type} {node type} as the
|
|
203 |
second parameter, which it just passes on to
|
|
204 |
\l{QAbstractXmlNodeModel::createIndex()} {createIndex()} as the
|
|
205 |
\c{additionalData} value. Logically, this second parameter
|
|
206 |
represents a second dimension in the node model, where the first
|
|
207 |
dimension represents the \e element nodes, and the second dimension
|
|
208 |
represents each element's attribute nodes. The meaning is that each
|
|
209 |
QFileInfo in the \c{m_fileInfos} vector can represent an \e{element}
|
|
210 |
node \e{and} one or more \e{attribute} nodes. In particular, the
|
|
211 |
QFileInfo for a file will contain the values for the attribute nodes
|
|
212 |
path, name, suffix, size, and mime type (see
|
|
213 |
\c{FileTree::attributes()}). Since the attributes are contained in
|
|
214 |
the QFileInfo of the file element, there aren't actually any
|
|
215 |
attribute nodes in the node model. Hence, we can use a QVector for
|
|
216 |
\c{m_fileInfos}.
|
|
217 |
|
|
218 |
A convenience overloading of \l{toNodeIndex of convenience}
|
|
219 |
{toNodeIndex()} is also called in several places, wherever it is
|
|
220 |
known that the QXmlNodeModelIndex being requested is for a directory
|
|
221 |
or a file and not for an attribute. The convenience function takes
|
|
222 |
only the QFileInfo parameter and calls the other \l{main toNodeIndex}
|
|
223 |
{toNodeIndex()}, after obtaining either the Directory or File node
|
|
224 |
type directly from the QFileInfo:
|
|
225 |
|
|
226 |
\target toNodeIndex of convenience
|
|
227 |
\snippet examples/xmlpatterns/filetree/filetree.cpp 0
|
|
228 |
|
|
229 |
Note that the auxiliary vector \c{m_names} is accessed using the
|
|
230 |
\l{Node_Type} {node type}, for example:
|
|
231 |
|
|
232 |
\snippet examples/xmlpatterns/filetree/filetree.cpp 3
|
|
233 |
|
|
234 |
Most of the virtual functions in the callback interface are as
|
|
235 |
simple as the ones described so far, but the callback function used
|
|
236 |
for traversing (and building) the node model is more complex.
|
|
237 |
|
|
238 |
\section3 Building And Traversing The Node Model
|
|
239 |
|
|
240 |
The node model in \c{FileTree} is not fully built before the query
|
|
241 |
engine begins evaluating the query. In fact, when the query engine
|
|
242 |
begins evaluating its first query, the only node in the node model
|
|
243 |
is the one representing the root directory for the selected part of
|
|
244 |
the file system. See \l{The UI Class: MainWindow} below for details
|
|
245 |
about how the UI triggers creation of the model.
|
|
246 |
|
|
247 |
The query engine builds the node model incrementally each time it
|
|
248 |
calls the \l{next node on axis} {nextFromSimpleAxis()} callback
|
|
249 |
function, as it traverses the node model to evaluate a query. Thus
|
|
250 |
the query engine only builds the region of the node model that it
|
|
251 |
needs for evaluating the query.
|
|
252 |
|
|
253 |
\l{next node on axis} {nextFromSimpleAxis()} takes an
|
|
254 |
\l{QAbstractXmlNodeModel::SimpleAxis} {axis identifier} and a
|
|
255 |
\l{QXmlNodeModelIndex} {node identifier} as parameters. The
|
|
256 |
\l{QXmlNodeModelIndex} {node identifier} represents the \e{context
|
|
257 |
node} (i.e. the query engine's current location in the model), and
|
|
258 |
the \l{QAbstractXmlNodeModel::SimpleAxis} {axis identifier}
|
|
259 |
represents the direction we want to move from the context node. The
|
|
260 |
function finds the appropriate next node and returns its
|
|
261 |
QXmlNodeModelIndex.
|
|
262 |
|
|
263 |
\l{next node on axis} {nextFromSimpleAxis()} is where most of the
|
|
264 |
work of implementing a custom node model will be required. The
|
|
265 |
obvious way to do it is to use a switch statement with a case for
|
|
266 |
each \l{QAbstractXmlNodeModel::SimpleAxis} {axis}.
|
|
267 |
|
|
268 |
\target next node on axis
|
|
269 |
\snippet examples/xmlpatterns/filetree/filetree.cpp 4
|
|
270 |
|
|
271 |
The first thing this function does is call \l{to file info}
|
|
272 |
{toFileInfo()} to get the QFileInfo of the context node. The use of
|
|
273 |
QVector::at() here is guaranteed to succeed because the context node
|
|
274 |
must already be in the node model, and hence must have a QFileInfo
|
|
275 |
in \c{m_fileInfos}.
|
|
276 |
|
|
277 |
\target to file info
|
|
278 |
\snippet examples/xmlpatterns/filetree/filetree.cpp 6
|
|
279 |
|
|
280 |
The \l{QAbstractXmlNodeModel::Parent} {Parent} case looks up the
|
|
281 |
context node's parent by constructing a QFileInfo from the context
|
|
282 |
node's \l{QFileInfo::absoluteFilePath()} {path} and passing it to
|
|
283 |
\l{main toNodeIndex} {toNodeIndex()} to find the QFileInfo in
|
|
284 |
\c{m_fileInfos}.
|
|
285 |
|
|
286 |
The \l{QAbstractXmlNodeModel::FirstChild} {FirstChild} case requires
|
|
287 |
that the context node must be a directory, because a file doesn't
|
|
288 |
have children. If the context node is not a directory, a default
|
|
289 |
constructed QXmlNodeModelIndex is returned. Otherwise,
|
|
290 |
QDir::entryInfoList() constructs a QFileInfoList of the context
|
|
291 |
node's children. The first QFileInfo in the list is passed to
|
|
292 |
\l{toNodeIndex of convenience} {toNodeIndex()} to get its
|
|
293 |
QXmlNodeModelIndex. Note that this will add the child to the node
|
|
294 |
model, if it isn't in the model yet.
|
|
295 |
|
|
296 |
The \l{QAbstractXmlNodeModel::PreviousSibling} {PreviousSibling} and
|
|
297 |
\l{QAbstractXmlNodeModel::NextSibling} {NextSibling} cases call the
|
|
298 |
\l{nextSibling helper} {nextSibling() helper function}. It takes the
|
|
299 |
QXmlNodeModelIndex of the context node, the QFileInfo of the context
|
|
300 |
node, and an offest of +1 or -1. The context node is a child of some
|
|
301 |
parent, so the function gets the parent and then gets the child list
|
|
302 |
for the parent. The child list is searched to find the QFileInfo of
|
|
303 |
the context node. It must be there. Then the offset is applied, -1
|
|
304 |
for the previous sibling and +1 for the next sibling. The resulting
|
|
305 |
index is passed to \l{toNodeIndex of convenience} {toNodeIndex()} to
|
|
306 |
get its QXmlNodeModelIndex. Note again that this will add the
|
|
307 |
sibling to the node model, if it isn't in the model yet.
|
|
308 |
|
|
309 |
\target nextSibling helper
|
|
310 |
\snippet examples/xmlpatterns/filetree/filetree.cpp 5
|
|
311 |
|
|
312 |
\section2 The UI Class: MainWindow
|
|
313 |
|
|
314 |
The example's UI is a conventional Qt GUI application inheriting
|
|
315 |
QMainWindow and the Ui_MainWindow base class generated by
|
|
316 |
\l{Qt Designer Manual} {Qt Designer}.
|
|
317 |
|
|
318 |
\snippet examples/xmlpatterns/filetree/mainwindow.h 0
|
|
319 |
|
|
320 |
It contains the custom node model (\c{m_fileTree}) and an instance
|
|
321 |
of QXmlNodeModelIndex (\c{m_fileNode}) used for holding the node
|
|
322 |
index for the root of the file system subtree. \c{m_fileNode} will
|
|
323 |
be bound to a $variable in the XQuery to be evaluated.
|
|
324 |
|
|
325 |
Two actions of interest are handled by slot functions: \l{Selecting
|
|
326 |
A Directory To Model} and \l{Selecting And Running An XQuery}.
|
|
327 |
|
|
328 |
\section3 Selecting A Directory To Model
|
|
329 |
|
|
330 |
The user selects \c{File->Open Directory} to choose a directory to
|
|
331 |
be loaded into the custom node model. Choosing a directory signals
|
|
332 |
the \c{on_actionOpenDirectory_triggered()} slot:
|
|
333 |
|
|
334 |
\snippet examples/xmlpatterns/filetree/mainwindow.cpp 1
|
|
335 |
|
|
336 |
The slot function simply calls the private function
|
|
337 |
\c{loadDirectory()} with the path of the chosen directory:
|
|
338 |
|
|
339 |
\target the standard code pattern
|
|
340 |
\snippet examples/xmlpatterns/filetree/mainwindow.cpp 4
|
|
341 |
|
|
342 |
\c{loadDirectory()} demonstrates a standard code pattern for using
|
|
343 |
QtXmlPatterns programatically. First it gets the node model index
|
|
344 |
for the root of the selected directory. Then it creates an instance
|
|
345 |
of QXmlQuery and calls QXmlQuery::bindVariable() to bind the node
|
|
346 |
index to the XQuery variable \c{$fileTree}. It then calls
|
|
347 |
QXmlQuery::setQuery() to load the XQuery text.
|
|
348 |
|
|
349 |
\note QXmlQuery::bindVariable() must be called \e before calling
|
|
350 |
QXmlQuery::setQuery(), which loads and parses the XQuery text and
|
|
351 |
must have access to the variable binding as the text is parsed.
|
|
352 |
|
|
353 |
The next lines create an output device for outputting the query
|
|
354 |
result, which is then used to create a QXmlFormatter to format the
|
|
355 |
query result as XML. QXmlQuery::evaluateTo() is called to run the
|
|
356 |
query, and the formatted XML output is displayed in the left panel
|
|
357 |
of the UI window.
|
|
358 |
|
|
359 |
Finally, the private function \l{Selecting And Running An XQuery}
|
|
360 |
{evaluateResult()} is called to run the currently selected XQuery
|
|
361 |
over the custom node model.
|
|
362 |
|
|
363 |
\note As described in \l{Building And Traversing The Node Model},
|
|
364 |
the \c FileTree class wants to build the custom node model
|
|
365 |
incrementally as it evaluates the XQuery. But, because the
|
|
366 |
\c{loadDirectory()} function runs the \c{wholeTree.xq} XQuery, it
|
|
367 |
actually builds the entire node model anyway. See \l{Node Model
|
|
368 |
Building Strategy} for a discussion about building your custom node
|
|
369 |
model.
|
|
370 |
|
|
371 |
\section3 Selecting And Running An XQuery
|
|
372 |
|
|
373 |
The user chooses an XQuery from the menu in the combobox on the
|
|
374 |
right. Choosing an XQuery signals the
|
|
375 |
\c{on_queryBox_currentIndexChanged()} slot:
|
|
376 |
|
|
377 |
\snippet examples/xmlpatterns/filetree/mainwindow.cpp 2
|
|
378 |
|
|
379 |
The slot function opens and loads the query file and then calls the
|
|
380 |
private function \c{evaluateResult()} to run the query:
|
|
381 |
|
|
382 |
\snippet examples/xmlpatterns/filetree/mainwindow.cpp 3
|
|
383 |
|
|
384 |
\c{evaluateResult()} is a second example of the same code pattern
|
|
385 |
shown in \l{the standard code pattern} {loadDirectory()}. In this
|
|
386 |
case, it runs the XQuery currently selected in the combobox instead
|
|
387 |
of \c{qrc:/queries/wholeTree.xq}, and it outputs the query result to
|
|
388 |
the panel on the lower right of the UI window.
|
|
389 |
|
|
390 |
\section2 Node Model Building Strategy
|
|
391 |
|
|
392 |
We saw that the \l{The Custom Node Model Class: FileTree} {FileTree}
|
|
393 |
tries to build its custom node model incrementally, but we also saw
|
|
394 |
that the \l{the standard code pattern} {MainWindow::loadDirectory()}
|
|
395 |
function in the UI class immediately subverts the incremental build
|
|
396 |
by running the \c{wholeTree.xq} XQuery, which traverses the entire
|
|
397 |
selected directory, thereby causing the entire node model to be
|
|
398 |
built.
|
|
399 |
|
|
400 |
If we want to preserve the incremental build capability of the
|
|
401 |
\c{FileTree} class, we can strip the running of \c{wholeTree.xq} out
|
|
402 |
of \l{the standard code pattern} {MainWindow::loadDirectory()}:
|
|
403 |
|
|
404 |
\snippet examples/xmlpatterns/filetree/mainwindow.cpp 5
|
|
405 |
\snippet examples/xmlpatterns/filetree/mainwindow.cpp 6
|
|
406 |
|
|
407 |
Note, however, that \c{FileTree} doesn't have the capability of
|
|
408 |
deleting all or part of the node model. The node model, once built,
|
|
409 |
is only deleted when the \c{FileTree} instance goes out of scope.
|
|
410 |
|
|
411 |
In this example, each element node in the node model represents a
|
|
412 |
directory or a file in the computer's file system, and each node is
|
|
413 |
represented by an instance of QFileInfo. An instance of QFileInfo is
|
|
414 |
not costly to produce, but you might imagine a node model where
|
|
415 |
building new nodes is very costly. In such cases, the capability to
|
|
416 |
build the node model incrementally is important, because it allows
|
|
417 |
us to only build the region of the model we need for evaluating the
|
|
418 |
query. In other cases, it will be simpler to just build the entire
|
|
419 |
node model.
|
|
420 |
|
|
421 |
*/
|