1<?php
2
3/**
4 * Hoa
5 *
6 *
7 * @license
8 *
9 * New BSD License
10 *
11 * Copyright © 2007-2017, Hoa community. All rights reserved.
12 *
13 * Redistribution and use in source and binary forms, with or without
14 * modification, are permitted provided that the following conditions are met:
15 *     * Redistributions of source code must retain the above copyright
16 *       notice, this list of conditions and the following disclaimer.
17 *     * Redistributions in binary form must reproduce the above copyright
18 *       notice, this list of conditions and the following disclaimer in the
19 *       documentation and/or other materials provided with the distribution.
20 *     * Neither the name of the Hoa nor the names of its contributors may be
21 *       used to endorse or promote products derived from this software without
22 *       specific prior written permission.
23 *
24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
27 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE
28 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
29 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
30 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
31 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
32 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
33 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
34 * POSSIBILITY OF SUCH DAMAGE.
35 */
36
37namespace Hoa\Protocol\Node;
38
39use Hoa\Consistency;
40use Hoa\Protocol;
41
42/**
43 * Class \Hoa\Protocol\Node\Node.
44 *
45 * Abstract class for all `hoa://`'s nodes.
46 *
47 * @copyright  Copyright © 2007-2017 Hoa community
48 * @license    New BSD License
49 */
50class Node implements \ArrayAccess, \IteratorAggregate
51{
52    /**
53     * Node's name.
54     *
55     * @var string
56     */
57    protected $_name       = null;
58
59    /**
60     * Path for the `reach` method.
61     *
62     * @var string
63     */
64    protected $_reach      = null;
65
66    /**
67     * Children of the node.
68     *
69     * @var array
70     */
71    private $_children     = [];
72
73
74
75    /**
76     * Construct a protocol's node.
77     * If it is not a data object (i.e. if it does not extend this class to
78     * overload the `$_name` attribute), we can set the `$_name` attribute
79     * dynamically. This is useful to create a node on-the-fly.
80     *
81     * @param   string  $name        Node's name.
82     * @param   string  $reach       Path for the `reach` method.
83     * @param   array   $children    Node's children.
84     */
85    public function __construct($name = null, $reach = null, array $children = [])
86    {
87        if (null !== $name) {
88            $this->_name = $name;
89        }
90
91        if (null !== $reach) {
92            $this->_reach = $reach;
93        }
94
95        foreach ($children as $child) {
96            $this[] = $child;
97        }
98
99        return;
100    }
101
102    /**
103     * Add a node.
104     *
105     * @param   string                  $name    Node's name. If null, will be
106     *                                           set to name of `$node`.
107     * @param   \Hoa\Protocol\Protocol  $node    Node to add.
108     * @return  \Hoa\Protocol\Protocol
109     * @throws  \Hoa\Protocol\Exception
110     */
111    public function offsetSet($name, $node)
112    {
113        if (!($node instanceof self)) {
114            throw new Protocol\Exception(
115                'Protocol node must extend %s.',
116                0,
117                __CLASS__
118            );
119        }
120
121        if (empty($name)) {
122            $name = $node->getName();
123        }
124
125        if (empty($name)) {
126            throw new Protocol\Exception(
127                'Cannot add a node to the `hoa://` protocol without a name.',
128                1
129            );
130        }
131
132        $this->_children[$name] = $node;
133
134        return;
135    }
136
137    /**
138     * Get a specific node.
139     *
140     * @param   string  $name    Node's name.
141     * @return  \Hoa\Protocol\Protocol
142     * @throws  \Hoa\Protocol\Exception
143     */
144    public function offsetGet($name)
145    {
146        if (!isset($this[$name])) {
147            throw new Protocol\Exception(
148                'Node %s does not exist.',
149                2,
150                $name
151            );
152        }
153
154        return $this->_children[$name];
155    }
156
157    /**
158     * Check if a node exists.
159     *
160     * @param   string  $name    Node's name.
161     * @return  bool
162     */
163    public function offsetExists($name)
164    {
165        return true === array_key_exists($name, $this->_children);
166    }
167
168    /**
169     * Remove a node.
170     *
171     * @param   string  $name    Node's name to remove.
172     * @return  void
173     */
174    public function offsetUnset($name)
175    {
176        unset($this->_children[$name]);
177
178        return;
179    }
180
181    /**
182     * Resolve a path, i.e. iterate the nodes tree and reach the queue of
183     * the path.
184     *
185     * @param   string  $path            Path to resolve.
186     * @param   array   &$accumulator    Combination of all possibles paths.
187     * @param   string  $id              ID.
188     * @return  mixed
189     */
190    protected function _resolve($path, &$accumulator, $id = null)
191    {
192        if (substr($path, 0, 6) == 'hoa://') {
193            $path = substr($path, 6);
194        }
195
196        if (empty($path)) {
197            return null;
198        }
199
200        if (null === $accumulator) {
201            $accumulator = [];
202            $posId       = strpos($path, '#');
203
204            if (false !== $posId) {
205                $id   = substr($path, $posId + 1);
206                $path = substr($path, 0, $posId);
207            } else {
208                $id   = null;
209            }
210        }
211
212        $path = trim($path, '/');
213        $pos  = strpos($path, '/');
214
215        if (false !== $pos) {
216            $next = substr($path, 0, $pos);
217        } else {
218            $next = $path;
219        }
220
221        if (isset($this[$next])) {
222            if (false === $pos) {
223                if (null === $id) {
224                    $this->_resolveChoice($this[$next]->reach(), $accumulator);
225
226                    return true;
227                }
228
229                $accumulator = null;
230
231                return $this[$next]->reachId($id);
232            }
233
234            $tnext = $this[$next];
235            $this->_resolveChoice($tnext->reach(), $accumulator);
236
237            return $tnext->_resolve(substr($path, $pos + 1), $accumulator, $id);
238        }
239
240        $this->_resolveChoice($this->reach($path), $accumulator);
241
242        return true;
243    }
244
245    /**
246     * Resolve choices, i.e. a reach value has a “;”.
247     *
248     * @param   string  $reach           Reach value.
249     * @param   array   &$accumulator    Combination of all possibles paths.
250     * @return  void
251     */
252    protected function _resolveChoice($reach, array &$accumulator)
253    {
254        if (empty($accumulator)) {
255            $accumulator = explode(RS, $reach);
256
257            return;
258        }
259
260        if (false === strpos($reach, RS)) {
261            if (false !== $pos = strrpos($reach, "\r")) {
262                $reach = substr($reach, $pos + 1);
263
264                foreach ($accumulator as &$entry) {
265                    $entry = null;
266                }
267            }
268
269            foreach ($accumulator as &$entry) {
270                $entry .= $reach;
271            }
272
273            return;
274        }
275
276        $choices     = explode(RS, $reach);
277        $ref         = $accumulator;
278        $accumulator = [];
279
280        foreach ($choices as $choice) {
281            if (false !== $pos = strrpos($choice, "\r")) {
282                $choice = substr($choice, $pos + 1);
283
284                foreach ($ref as $entry) {
285                    $accumulator[] = $choice;
286                }
287            } else {
288                foreach ($ref as $entry) {
289                    $accumulator[] = $entry . $choice;
290                }
291            }
292        }
293
294        unset($ref);
295
296        return;
297    }
298
299    /**
300     * Queue of the node.
301     * Generic one. Must be overrided in children classes.
302     *
303     * @param   string  $queue    Queue of the node (generally a filename,
304     *                            with probably a query).
305     * @return  mixed
306     */
307    public function reach($queue = null)
308    {
309        return empty($queue) ? $this->_reach : $queue;
310    }
311
312    /**
313     * ID of the component.
314     * Generic one. Should be overrided in children classes.
315     *
316     * @param   string  $id    ID of the component.
317     * @return  mixed
318     * @throws  \Hoa\Protocol\Exception
319     */
320    public function reachId($id)
321    {
322        throw new Protocol\Exception(
323            'The node %s has no ID support (tried to reach #%s).',
324            4,
325            [$this->getName(), $id]
326        );
327    }
328
329    /**
330     * Set a new reach value.
331     *
332     * @param   string  $reach    Reach value.
333     * @return  string
334     */
335    public function setReach($reach)
336    {
337        $old          = $this->_reach;
338        $this->_reach = $reach;
339
340        return $old;
341    }
342
343    /**
344     * Get node's name.
345     *
346     * @return  string
347     */
348    public function getName()
349    {
350        return $this->_name;
351    }
352
353    /**
354     * Get reach's root.
355     *
356     * @return  string
357     */
358    protected function getReach()
359    {
360        return $this->_reach;
361    }
362
363    /**
364     * Get an iterator.
365     *
366     * @return  \ArrayIterator
367     */
368    public function getIterator()
369    {
370        return new \ArrayIterator($this->_children);
371    }
372
373    /**
374     * Get root the protocol.
375     *
376     * @return  \Hoa\Protocol\Protocol
377     */
378    public static function getRoot()
379    {
380        return Protocol::getInstance();
381    }
382
383    /**
384     * Print a tree of component.
385     *
386     * @return  string
387     */
388    public function __toString()
389    {
390        static $i = 0;
391
392        $out = str_repeat('  ', $i) . $this->getName() . "\n";
393
394        foreach ($this as $node) {
395            ++$i;
396            $out .= $node;
397            --$i;
398        }
399
400        return $out;
401    }
402}
403
404/**
405 * Flex entity.
406 */
407Consistency::flexEntity('Hoa\Protocol\Node\Node');
408