Skip to content

Commit 542d9af

Browse files
authored
JCR-5140: Improve support for generating namespace prefixes (#255)
1 parent 413acb9 commit 542d9af

File tree

2 files changed

+306
-63
lines changed

2 files changed

+306
-63
lines changed

jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/NamespaceHelper.java

Lines changed: 134 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,15 @@
1616
*/
1717
package org.apache.jackrabbit.commons;
1818

19+
import java.math.BigInteger;
20+
import java.nio.charset.StandardCharsets;
21+
import java.security.MessageDigest;
22+
import java.security.NoSuchAlgorithmException;
1923
import java.util.HashMap;
24+
import java.util.Locale;
2025
import java.util.Map;
26+
import java.util.UUID;
27+
import java.util.function.UnaryOperator;
2128

2229
import javax.jcr.NamespaceException;
2330
import javax.jcr.NamespaceRegistry;
@@ -220,26 +227,31 @@ public String registerNamespace(String prefix, String uri)
220227
// Check if the namespace is registered
221228
registry.getPrefix(uri);
222229
} catch (NamespaceException e1) {
223-
// Replace troublesome prefix hints
230+
// Throw away Troublesome prefix hints
224231
if (prefix == null || prefix.isEmpty()
225232
|| prefix.toLowerCase().startsWith("xml")
226233
|| !XMLChar.isValidNCName(prefix)) {
227-
prefix = "ns"; // ns, ns2, ns3, ns4, ...
234+
prefix = null;
228235
}
229236

230-
// Loop until an unused prefix is found
231-
try {
232-
String base = prefix;
233-
for (int i = 2; true; i++) {
234-
registry.getURI(prefix);
235-
prefix = base + i;
236-
}
237-
} catch (NamespaceException e2) {
238-
// Exit the loop
239-
}
237+
if (prefix == null) {
238+
prefix = suggestPrefix(uri, pref -> {
239+
// prefix checker
240+
try {
241+
return registry.getURI(pref);
242+
} catch (RepositoryException e) {
243+
return null;
244+
}
245+
});
246+
}
240247

241248
// Register the namespace
242-
registry.registerNamespace(prefix, uri);
249+
try {
250+
registry.registerNamespace(prefix, uri);
251+
} catch (NamespaceException ex) {
252+
// likely prefix is already in use; retry with null prefix
253+
return registerNamespace(null, uri);
254+
}
243255
}
244256

245257
return session.getNamespacePrefix(uri);
@@ -258,4 +270,113 @@ public void registerNamespaces(Map<String,String> namespaces) throws RepositoryE
258270
}
259271
}
260272

273+
// non-public supporting code
274+
275+
// map with 'optimal' prefix mappings; hard-wired should be ok for now
276+
private static final Map<String, String> KNOWN_PREFIXES =
277+
Map.of("http://creativecommons.org/ns#", "cc",
278+
"http://purl.org/dc/terms/", "dc",
279+
"http://ns.adobe.com/DICOM/", "DICOM",
280+
"http://ns.adobe.com/exif/1.0/", "exif",
281+
"http://ns.adobe.com/pdf/1.3/", "pdf",
282+
"http://ns.adobe.com/pdfx/1.3/", "pdfx",
283+
"http://ns.adobe.com/photoshop/1.0/", "photoshop",
284+
"http://www.w3.org/1999/02/22-rdf-syntax-ns#", "rdf",
285+
"http://ns.adobe.com/tiff/1.0/", "tiff",
286+
"http://ns.adobe.com/xap/1.0/", "xmp");
287+
288+
289+
// suggest an available prefix for the provided namespace, based on a random UUID
290+
// (last resort)
291+
private static String devisePrefixByUUID(String namespace, UnaryOperator<String> lookupNamespace) {
292+
String prefix;
293+
294+
do {
295+
prefix = "u-" + UUID.randomUUID();
296+
} while (namespace.equals(lookupNamespace.apply(prefix)));
297+
298+
return prefix;
299+
}
300+
301+
// compute SHA-256, null when NoSuchAlgorithmException
302+
private static String getSha256(String namespace) {
303+
try {
304+
byte[] bytes = MessageDigest.getInstance("SHA-256").digest(namespace.getBytes(StandardCharsets.UTF_8));
305+
return new BigInteger(1, bytes).toString(16);
306+
} catch (NoSuchAlgorithmException e) {
307+
// this really, really should not happen
308+
return null;
309+
}
310+
}
311+
312+
// suggest an available prefix for the provided namespace, based on the sha-256
313+
// of the namespace name, and a final fallback to UUID based (while considering pre-existing mappings)
314+
private static String devisePrefixTrySha256(String namespace, UnaryOperator<String> lookupNamespace) {
315+
316+
String sha = getSha256(namespace);
317+
if (sha != null) {
318+
for (int i = 7; i <= sha.length(); i++) {
319+
String prefix = "s-" + sha.substring(0, i);
320+
String lookedUpPrefix = lookupNamespace.apply(prefix);
321+
if (lookedUpPrefix == null) {
322+
// unused, so go ahead with this prefix
323+
return prefix;
324+
}
325+
}
326+
}
327+
328+
// fallback to UUID
329+
return devisePrefixByUUID(namespace, lookupNamespace);
330+
}
331+
332+
// suggest an available prefix for the provided namespace, based on the characters
333+
// in the namespace name (while considering pre-existing mappings)
334+
private static String devisePrefix(String namespace, UnaryOperator<String> lookupNamespace) {
335+
String prefix = namespace.toLowerCase(Locale.ENGLISH);
336+
337+
// strip scheme when http(s)
338+
if (prefix.startsWith("http://")) {
339+
prefix = prefix.substring("http://".length());
340+
} else if (prefix.startsWith("https://")) {
341+
prefix = prefix.substring("https://".length());
342+
}
343+
344+
// strip common host name prefixes
345+
if (prefix.startsWith("www.")) {
346+
prefix = prefix.substring("www.".length());
347+
} else if (prefix.startsWith("ns.")) {
348+
prefix = prefix.substring("ns.".length());
349+
}
350+
351+
// replace characters not allowed in prefix (here: '\', '/' and :)
352+
prefix = prefix.replaceAll("[\\/:]+", "-");
353+
354+
// strip trailing replacement character
355+
while (prefix.endsWith("-")) {
356+
prefix = prefix.substring(0, prefix.length() - 1);
357+
}
358+
359+
String lookedUpNamespace = lookupNamespace.apply(prefix);
360+
if (lookedUpNamespace == null || lookedUpNamespace.equals(namespace)) {
361+
return prefix;
362+
} else {
363+
return devisePrefixTrySha256(namespace, lookupNamespace);
364+
}
365+
}
366+
367+
// suggest an available prefix for the provided namespace (while considering pre-existing mappings)
368+
private static String suggestPrefix(String namespace, UnaryOperator<String> lookupPrefix) {
369+
// try hard-wired map
370+
String known = KNOWN_PREFIXES.get(namespace);
371+
372+
// lookup using supplied mapper as well
373+
String lookedUpNamespace = known != null ? lookupPrefix.apply(known) : null;
374+
375+
// return hardwired prefix if unused or mapper has the prefix mapped to the same namespace
376+
if (known != null && (lookedUpNamespace == null || namespace.equals(lookedUpNamespace))) {
377+
return known;
378+
} else {
379+
return devisePrefix(namespace, lookupPrefix);
380+
}
381+
}
261382
}

0 commit comments

Comments
 (0)