Skip to content

Commit c3f2531

Browse files
committed
jsr-333: implementation for generating node name
1 parent 71e45d6 commit c3f2531

File tree

2 files changed

+202
-1
lines changed

2 files changed

+202
-1
lines changed

src/PHPCR/Util/NodeHelper.php

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
use PHPCR\SessionInterface;
66
use PHPCR\ItemInterface;
7+
use PHPCR\RepositoryException;
8+
use PHPCR\NamespaceException;
79

810
/**
911
* Helper with only static methods to work with PHPCR nodes
@@ -26,7 +28,7 @@ private function __construct()
2628
* @param SessionInterface $session the phpcr session to create the path
2729
* @param string $path full path, like /content/jobs/data
2830
*
29-
* @return PHPCR\NodeInterface the last node of the path, i.e. data
31+
* @return \PHPCR\NodeInterface the last node of the path, i.e. data
3032
*/
3133
public static function createPath(SessionInterface $session, $path)
3234
{
@@ -82,4 +84,140 @@ public static function isSystemItem(ItemInterface $item)
8284

8385
return strpos($name, 'jcr:') === 0 || strpos($name, 'rep:') === 0;
8486
}
87+
88+
/**
89+
* Helper method to implement NodeInterface::addNodeAutoNamed
90+
*
91+
* This method only checks for valid namespaces. All other exceptions must
92+
* be thrown by the addNodeAutoNamed implementation.
93+
*
94+
* @param string[] $usedNames list of child names that is currently used and may not be chosen.
95+
* @param string[] $namespaces namespace prefix to uri map of all currently known namespaces.
96+
* @param string $defaultNamespace namespace prefix to use if the hint does not specify.
97+
* @param string $nameHint the name hint according to the API definition
98+
*
99+
* @return string A valid node name for this node
100+
*
101+
* @throws NamespaceException if a namespace prefix is provided in the
102+
* $nameHint which does not exist and this implementation performs
103+
* this validation immediately.
104+
*/
105+
public static function generateAutoNodeName($usedNames, $namespaces, $defaultNamespace, $nameHint = null)
106+
{
107+
$usedNames = array_flip($usedNames);
108+
109+
/*
110+
* null: The new node name will be generated entirely by the repository.
111+
*/
112+
if (null === $nameHint) {
113+
114+
return self::generateWithPrefix($usedNames, $defaultNamespace . ':');
115+
}
116+
117+
/*
118+
* "" (the empty string), ":" (colon) or "{}": The new node name will
119+
* be in the empty namespace and the local part of the name will be
120+
* generated by the repository.
121+
*/
122+
if ('' === $nameHint || ':' == $nameHint || '{}' == $nameHint) {
123+
124+
return self::generateWithPrefix($usedNames, '');
125+
}
126+
127+
/*
128+
* "<i>somePrefix</i>:" where <i>somePrefix</i> is a syntactically
129+
* valid namespace prefix
130+
*/
131+
if (':' == $nameHint[strlen($nameHint)-1]
132+
&& substr_count($nameHint, ':') === 1
133+
&& preg_match('#^[a-zA-Z][a-zA-Z0-9]*:$#', $nameHint)
134+
) {
135+
$prefix = substr($nameHint, 0, -1);
136+
if (! isset($namespaces[$prefix])) {
137+
throw new NamespaceException("Invalid nameHint '$nameHint'");
138+
}
139+
140+
return self::generateWithPrefix($usedNames, $prefix . ':');
141+
}
142+
143+
/*
144+
* "{<i>someURI</i>}" where <i>someURI</i> is a syntactically valid
145+
* namespace URI
146+
*/
147+
if (strlen($nameHint) > 2
148+
&& '{' == $nameHint[0]
149+
&& '}' == $nameHint[strlen($nameHint)-1]
150+
&& filter_var(substr($nameHint, 1, -1), FILTER_VALIDATE_URL)
151+
) {
152+
$prefix = array_search(substr($nameHint, 1, -1), $namespaces);
153+
if (! $prefix) {
154+
throw new NamespaceException("Invalid nameHint '$nameHint'");
155+
}
156+
157+
return self::generateWithPrefix($usedNames, $prefix . ':');
158+
}
159+
160+
/*
161+
* "<i>somePrefix</i>:<i>localNameHint</i>" where <i>somePrefix</i> is
162+
* a syntactically valid namespace prefix and <i>localNameHint</i> is
163+
* syntactically valid local name: The repository will attempt to create a
164+
* name in the namespace represented by that prefix as described in (3),
165+
* above. The local part of the name is generated by the repository using
166+
* <i>localNameHint</i> as a basis. The way in which the local name is
167+
* constructed from the hint may vary across implementations.
168+
*/
169+
if (1 === substr_count($nameHint, ':')) {
170+
list($prefix, $name) = explode(':', $nameHint);
171+
if (preg_match('#^[a-zA-Z][a-zA-Z0-9]*$#', $prefix)
172+
&& preg_match('#^[a-zA-Z][a-zA-Z0-9]*$#', $name)
173+
) {
174+
if (! isset($namespaces[$prefix])) {
175+
throw new NamespaceException("Invalid nameHint '$nameHint'");
176+
}
177+
178+
return self::generateWithPrefix($usedNames, $prefix . ':', $name);
179+
}
180+
}
181+
182+
/*
183+
* "{<i>someURI</i>}<i>localNameHint</i>" where <i>someURI</i> is a
184+
* syntactically valid namespace URI and <i>localNameHint</i> is
185+
* syntactically valid local name: The repository will attempt to create a
186+
* name in the namespace specified as described in (4), above. The local
187+
* part of the name is generated by the repository using <i>localNameHint</i>
188+
* as a basis. The way in which the local name is constructed from the hint
189+
* may vary across implementations.
190+
*/
191+
$matches = array();
192+
//if (preg_match('#^\\{([^\\}]+)\\}([a-zA-Z][a-zA-Z0-9]*)$}#', $nameHint, $matches)) {
193+
if (preg_match('#^\\{([^\\}]+)\\}([a-zA-Z][a-zA-Z0-9]*)$#', $nameHint, $matches)) {
194+
$ns = $matches[1];
195+
$name = $matches[2];
196+
197+
$prefix = array_search($ns, $namespaces);
198+
if (! $prefix) {
199+
throw new NamespaceException("Invalid nameHint '$nameHint'");
200+
}
201+
202+
return self::generateWithPrefix($usedNames, $prefix . ':', $name);
203+
}
204+
205+
throw new RepositoryException("Invalid nameHint '$nameHint'");
206+
}
207+
208+
/**
209+
* @param string[] $usedNames names that are forbidden
210+
* @param string $prefix the prefix including the colon at the end
211+
* @param string $namepart start for the localname
212+
*
213+
* @return string
214+
*/
215+
private static function generateWithPrefix($usedNames, $prefix, $namepart = '')
216+
{
217+
do {
218+
$name = $prefix . $namepart . mt_rand();
219+
} while (isset($usedNames[$name]));
220+
221+
return $name;
222+
}
85223
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace PHPCR\Tests\Util;
4+
5+
use PHPCR\Util\NodeHelper;
6+
7+
class NodeHelperTest extends \PHPUnit_Framework_TestCase
8+
{
9+
private $namespaces = array('a' => 'http://phpcr', 'b' => 'http://jcr');
10+
private $usedNames = array('a:x', 'b:y', 'c');
11+
12+
public static function hints() {
13+
return array(
14+
array('', true),
15+
array(':', true),
16+
array('{}', true),
17+
array('b:', 'b:'),
18+
array('{http://jcr}', 'b:'),
19+
array('b:z', 'b:z'),
20+
array('{http://phpcr}bar', 'a:bar'),
21+
);
22+
}
23+
24+
public static function invalidHints() {
25+
return array(
26+
array('::'),
27+
array('a'), // no colon
28+
array('a:foo:'),
29+
array('{foo'),
30+
array('x'), // not an existing namespace
31+
);
32+
}
33+
34+
public function testGenerateAutoNodeNameNoHint()
35+
{
36+
$result = NodeHelper::generateAutoNodeName($this->usedNames, $this->namespaces, 'a');
37+
$this->assertEquals('a:', substr($result, 0, 2));
38+
}
39+
40+
/**
41+
* @dataProvider hints
42+
*/
43+
public function testGenerateAutoNodeName($hint, $expect)
44+
{
45+
$result = NodeHelper::generateAutoNodeName($this->usedNames, $this->namespaces, 'a', $hint);
46+
if (true === $expect) {
47+
$this->assertFalse(strpos($result, ':'));
48+
} else {
49+
$this->assertEquals($expect, substr($result, 0, strlen($expect)));
50+
}
51+
}
52+
53+
/**
54+
* @dataProvider invalidHints
55+
* @expectedException \PHPCR\RepositoryException
56+
*/
57+
public function testGenerateAutoNodeNameInvalid($hint)
58+
{
59+
NodeHelper::generateAutoNodeName($this->usedNames, $this->namespaces, 'a', $hint);
60+
}
61+
62+
63+
}

0 commit comments

Comments
 (0)