PageRenderTime 58ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/core/src/main/java/com/github/jsonldjava/core/JSONLDUtils.java

http://github.com/tristan/jsonld-java
Java | 1858 lines | 1140 code | 175 blank | 543 comment | 448 complexity | a8ee54b51b2c46b56304155f63b60f70 MD5 | raw file
Possible License(s): BSD-3-Clause

Large files files are truncated, but you can click here to view the full file

  1. package com.github.jsonldjava.core;
  2. import java.io.IOException;
  3. import java.net.MalformedURLException;
  4. import java.util.ArrayList;
  5. import java.util.Arrays;
  6. import java.util.Collection;
  7. import java.util.Collections;
  8. import java.util.LinkedHashMap;
  9. import java.util.List;
  10. import java.util.Map;
  11. import java.util.regex.Pattern;
  12. import com.fasterxml.jackson.core.JsonParseException;
  13. import com.github.jsonldjava.utils.JSONUtils;
  14. import com.github.jsonldjava.utils.Obj;
  15. import com.github.jsonldjava.utils.URL;
  16. public class JSONLDUtils {
  17. private static final int MAX_CONTEXT_URLS = 10;
  18. /**
  19. * Returns whether or not the given value is a keyword (or a keyword alias).
  20. *
  21. * @param v
  22. * the value to check.
  23. * @param [ctx] the active context to check against.
  24. *
  25. * @return true if the value is a keyword, false if not.
  26. */
  27. static boolean isKeyword(Object key) {
  28. if (!isString(key)) {
  29. return false;
  30. }
  31. return "@base".equals(key) || "@context".equals(key) || "@container".equals(key)
  32. || "@default".equals(key) || "@embed".equals(key) || "@explicit".equals(key)
  33. || "@graph".equals(key) || "@id".equals(key) || "@index".equals(key)
  34. || "@language".equals(key) || "@list".equals(key) || "@omitDefault".equals(key)
  35. || "@reverse".equals(key) || "@preserve".equals(key) || "@set".equals(key)
  36. || "@type".equals(key) || "@value".equals(key) || "@vocab".equals(key);
  37. }
  38. static boolean isAbsoluteIri(String value) {
  39. return value.contains(":");
  40. }
  41. /**
  42. * Adds a value to a subject. If the value is an array, all values in the
  43. * array will be added.
  44. *
  45. * Note: If the value is a subject that already exists as a property of the
  46. * given subject, this method makes no attempt to deeply merge properties.
  47. * Instead, the value will not be added.
  48. *
  49. * @param subject
  50. * the subject to add the value to.
  51. * @param property
  52. * the property that relates the value to the subject.
  53. * @param value
  54. * the value to add.
  55. * @param [propertyIsArray] true if the property is always an array, false
  56. * if not (default: false).
  57. * @param [allowDuplicate] true if the property is a @list, false if not
  58. * (default: false).
  59. */
  60. static void addValue(Map<String, Object> subject, String property, Object value,
  61. boolean propertyIsArray, boolean allowDuplicate) {
  62. if (isArray(value)) {
  63. if (((List) value).size() == 0 && propertyIsArray && !subject.containsKey(property)) {
  64. subject.put(property, new ArrayList<Object>());
  65. }
  66. for (final Object val : (List) value) {
  67. addValue(subject, property, val, propertyIsArray, allowDuplicate);
  68. }
  69. } else if (subject.containsKey(property)) {
  70. // check if subject already has the value if duplicates not allowed
  71. final boolean hasValue = !allowDuplicate && hasValue(subject, property, value);
  72. // make property an array if value not present or always an array
  73. if (!isArray(subject.get(property)) && (!hasValue || propertyIsArray)) {
  74. final List<Object> tmp = new ArrayList<Object>();
  75. tmp.add(subject.get(property));
  76. subject.put(property, tmp);
  77. }
  78. // add new value
  79. if (!hasValue) {
  80. ((List<Object>) subject.get(property)).add(value);
  81. }
  82. } else {
  83. // add new value as a set or single value
  84. Object tmp;
  85. if (propertyIsArray) {
  86. tmp = new ArrayList<Object>();
  87. ((List<Object>) tmp).add(value);
  88. } else {
  89. tmp = value;
  90. }
  91. subject.put(property, tmp);
  92. }
  93. }
  94. static void addValue(Map<String, Object> subject, String property, Object value,
  95. boolean propertyIsArray) {
  96. addValue(subject, property, value, propertyIsArray, true);
  97. }
  98. static void addValue(Map<String, Object> subject, String property, Object value) {
  99. addValue(subject, property, value, false, true);
  100. }
  101. /**
  102. * Creates a term definition during context processing.
  103. *
  104. * @param activeCtx
  105. * the current active context.
  106. * @param localCtx
  107. * the local context being processed.
  108. * @param term
  109. * the term in the local context to define the mapping for.
  110. * @param defined
  111. * a map of defining/defined keys to detect cycles and prevent
  112. * double definitions.
  113. * @throws JSONLDProcessingError
  114. */
  115. static void createTermDefinition(ActiveContext activeCtx, Map<String, Object> localCtx,
  116. String term, Map<String, Boolean> defined) throws JSONLDProcessingError {
  117. if (defined.containsKey(term)) {
  118. // term already defined
  119. if (defined.get(term)) {
  120. return;
  121. }
  122. // cycle detected
  123. throw new JSONLDProcessingError("Cyclical context definition detected.")
  124. .setType(JSONLDProcessingError.Error.CYCLICAL_CONTEXT)
  125. .setDetail("context", localCtx).setDetail("term", term);
  126. }
  127. // now defining term
  128. defined.put(term, false);
  129. if (isKeyword(term)) {
  130. throw new JSONLDProcessingError(
  131. "Invalid JSON-LD syntax; keywords cannot be overridden.").setType(
  132. JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail("context", localCtx);
  133. }
  134. // remove old mapping
  135. activeCtx.mappings.remove(term);
  136. // get context term value
  137. Object value = localCtx.get(term);
  138. // clean context entry
  139. if (value == null
  140. || (isObject(value) && // NOTE: object[key] === null will return
  141. // false if the key doesn't exist in the
  142. // object
  143. ((Map<String, Object>) value).containsKey("@id") && ((Map<String, Object>) value)
  144. .get("@id") == null)) {
  145. activeCtx.mappings.put(term, null);
  146. defined.put(term, true);
  147. return;
  148. }
  149. // convert short-hand value to object w/@id
  150. if (isString(value)) {
  151. final Map<String, Object> tmp = new LinkedHashMap<String, Object>();
  152. tmp.put("@id", value);
  153. value = tmp;
  154. }
  155. if (!isObject(value)) {
  156. throw new JSONLDProcessingError(
  157. "Invalid JSON-LD syntax; @context property values must be string or objects.")
  158. .setDetail("context", localCtx).setType(
  159. JSONLDProcessingError.Error.SYNTAX_ERROR);
  160. }
  161. final Map<String, Object> val = (Map<String, Object>) value;
  162. // create new mapping
  163. final Map<String, Object> mapping = new LinkedHashMap<String, Object>();
  164. activeCtx.mappings.put(term, mapping);
  165. mapping.put("reverse", false);
  166. if (val.containsKey("@reverse")) {
  167. if (val.containsKey("@id") || val.containsKey("@type") || val.containsKey("@language")) {
  168. throw new JSONLDProcessingError(
  169. "Invalid JSON-LD syntax; a @reverse term definition must not contain @id, @type or @language.")
  170. .setDetail("context", localCtx).setType(
  171. JSONLDProcessingError.Error.SYNTAX_ERROR);
  172. }
  173. if (!isString(val.get("@reverse"))) {
  174. throw new JSONLDProcessingError(
  175. "Invalid JSON-LD syntax; a @context @reverse value must be a string.")
  176. .setDetail("context", localCtx).setType(
  177. JSONLDProcessingError.Error.SYNTAX_ERROR);
  178. }
  179. final String reverse = (String) val.get("@reverse");
  180. // expand and add @id mapping, set @type to @id
  181. mapping.put("@id", expandIri(activeCtx, reverse, false, true, localCtx, defined));
  182. mapping.put("@type", "@id");
  183. mapping.put("reverse", true);
  184. } else if (val.containsKey("@id")) {
  185. if (!isString(val.get("@id"))) {
  186. throw new JSONLDProcessingError(
  187. "Invalid JSON-LD syntax; a @context @id value must be an array of strings or a string.")
  188. .setDetail("context", localCtx).setType(
  189. JSONLDProcessingError.Error.SYNTAX_ERROR);
  190. }
  191. final String id = (String) val.get("@id");
  192. if (id != null && !id.equals(term)) {
  193. // expand and add @id mapping
  194. mapping.put("@id", expandIri(activeCtx, id, false, true, localCtx, defined));
  195. }
  196. }
  197. if (!mapping.containsKey("@id")) {
  198. // see if the term has a prefix
  199. final int colon = term.indexOf(':');
  200. if (colon != -1) {
  201. final String prefix = term.substring(0, colon);
  202. if (localCtx.containsKey(prefix)) {
  203. // define parent prefix
  204. createTermDefinition(activeCtx, localCtx, prefix, defined);
  205. }
  206. // set @id based on prefix parent
  207. if (activeCtx.mappings.containsKey(prefix)) {
  208. final String suffix = term.substring(colon + 1);
  209. mapping.put(
  210. "@id",
  211. (String) ((Map<String, Object>) activeCtx.mappings.get(prefix))
  212. .get("@id") + suffix);
  213. }
  214. // term is an absolute IRI
  215. else {
  216. mapping.put("@id", term);
  217. }
  218. } else {
  219. // non-IRIs *must* define @ids if @vocab is not available
  220. if (!activeCtx.containsKey("@vocab")) {
  221. throw new JSONLDProcessingError(
  222. "Invalid JSON-LD syntax; @context terms must define an @id.")
  223. .setDetail("context", localCtx).setDetail("term", term)
  224. .setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
  225. }
  226. // prepend vocab to term
  227. mapping.put("@id", (String) activeCtx.get("@vocab") + term);
  228. }
  229. }
  230. // IRI mapping now defined
  231. defined.put(term, true);
  232. if (val.containsKey("@type")) {
  233. if (!isString(val.get("@type"))) {
  234. throw new JSONLDProcessingError(
  235. "Invalid JSON-LD syntax; a @context @type values must be strings.")
  236. .setDetail("context", localCtx).setType(
  237. JSONLDProcessingError.Error.SYNTAX_ERROR);
  238. }
  239. String type = (String) val.get("@type");
  240. if (!"@id".equals(type)) {
  241. // expand @type to full IRI
  242. type = expandIri(activeCtx, type, true, true, localCtx, defined);
  243. }
  244. mapping.put("@type", type);
  245. }
  246. if (val.containsKey("@container")) {
  247. final String container = (String) val.get("@container");
  248. if (!"@list".equals(container) && !"@set".equals(container)
  249. && !"@index".equals(container) && !"@language".equals(container)) {
  250. throw new JSONLDProcessingError(
  251. "Invalid JSON-LD syntax; @context @container value must be one of the following: "
  252. + "@list, @set, @index or @language.").setDetail("context",
  253. localCtx).setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
  254. }
  255. if ((Boolean) mapping.get("reverse") && !"@index".equals(container)) {
  256. throw new JSONLDProcessingError(
  257. "Invalid JSON-LD syntax; @context @container value for a @reverse type "
  258. + "definition must be @index.").setDetail("context", localCtx)
  259. .setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
  260. }
  261. // add @container to mapping
  262. mapping.put("@container", container);
  263. }
  264. if (val.containsKey("@language") && !val.containsKey("@type")) {
  265. if (val.get("@language") != null && !isString(val.get("@language"))) {
  266. throw new JSONLDProcessingError(
  267. "Invalid JSON-LD syntax; @context @language value value must be a string or null.")
  268. .setDetail("context", localCtx).setType(
  269. JSONLDProcessingError.Error.SYNTAX_ERROR);
  270. }
  271. String language = (String) val.get("@language");
  272. // add @language to mapping
  273. if (language != null) {
  274. language = language.toLowerCase();
  275. }
  276. mapping.put("@language", language);
  277. }
  278. // disallow aliasing @context and @preserve
  279. final String id = (String) mapping.get("@id");
  280. if ("@context".equals(id) || "@preserve".equals(id)) {
  281. throw new JSONLDProcessingError(
  282. "Invalid JSON-LD syntax; @context and @preserve cannot be aliased.")
  283. .setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
  284. }
  285. }
  286. /**
  287. * Expands a string to a full IRI. The string may be a term, a prefix, a
  288. * relative IRI, or an absolute IRI. The associated absolute IRI will be
  289. * returned.
  290. *
  291. * @param activeCtx
  292. * the current active context.
  293. * @param value
  294. * the string to expand.
  295. * @param relativeTo
  296. * options for how to resolve relative IRIs: base: true to
  297. * resolve against the base IRI, false not to. vocab: true to
  298. * concatenate after @vocab, false not to.
  299. * @param localCtx
  300. * the local context being processed (only given if called during
  301. * context processing).
  302. * @param defined
  303. * a map for tracking cycles in context definitions (only given
  304. * if called during context processing).
  305. *
  306. * @return the expanded value.
  307. * @throws JSONLDProcessingError
  308. */
  309. static String expandIri(ActiveContext activeCtx, String value, Boolean relativeToBase,
  310. Boolean relativeToVocab, Map<String, Object> localCtx, Map<String, Boolean> defined)
  311. throws JSONLDProcessingError {
  312. // already expanded
  313. if (value == null || isKeyword(value)) {
  314. return value;
  315. }
  316. // define term dependency if not defined
  317. if (localCtx != null && localCtx.containsKey(value)
  318. && !Boolean.TRUE.equals(defined.get(value))) {
  319. createTermDefinition(activeCtx, localCtx, value, defined);
  320. }
  321. if (relativeToVocab) {
  322. final Map<String, Object> mapping = (Map<String, Object>) activeCtx.mappings.get(value);
  323. // value is explicitly ignored with a null mapping
  324. if (mapping == null && activeCtx.mappings.containsKey(value)) {
  325. return null;
  326. }
  327. if (mapping != null) {
  328. // value is a term
  329. return (String) mapping.get("@id");
  330. }
  331. }
  332. // split value into prefix:suffix
  333. final int colon = value.indexOf(':');
  334. if (colon != -1) {
  335. final String prefix = value.substring(0, colon);
  336. final String suffix = value.substring(colon + 1);
  337. // do not expand blank nodes (prefix of '_') or already-absolute
  338. // IRIs (suffix of '//')
  339. if ("_".equals(prefix) || suffix.startsWith("//")) {
  340. return value;
  341. }
  342. // prefix dependency not defined, define it
  343. if (localCtx != null && localCtx.containsKey(prefix)) {
  344. createTermDefinition(activeCtx, localCtx, prefix, defined);
  345. }
  346. // use mapping if prefix is defined
  347. if (activeCtx.mappings.containsKey(prefix)) {
  348. final String id = ((Map<String, String>) activeCtx.mappings.get(prefix)).get("@id");
  349. return id + suffix;
  350. }
  351. // already absolute IRI
  352. return value;
  353. }
  354. // prepend vocab
  355. if (relativeToVocab && activeCtx.containsKey("@vocab")) {
  356. return activeCtx.get("@vocab") + value;
  357. }
  358. // prepend base
  359. String rval = value;
  360. if (relativeToBase) {
  361. rval = prependBase(activeCtx.get("@base"), rval);
  362. }
  363. if (localCtx != null) {
  364. // value must now be an absolute IRI
  365. if (!isAbsoluteIri(rval)) {
  366. throw new JSONLDProcessingError(
  367. "Invalid JSON-LD syntax; a @context value does not expand to an absolue IRI.")
  368. .setDetail("context", localCtx).setDetail("value", value)
  369. .setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
  370. }
  371. }
  372. return rval;
  373. }
  374. /**
  375. * Prepends a base IRI to the given relative IRI.
  376. *
  377. * @param base
  378. * the base IRI.
  379. * @param iri
  380. * the relative IRI.
  381. *
  382. * @return the absolute IRI.
  383. *
  384. * TODO: the URL class isn't as forgiving as the Node.js url parser,
  385. * we may need to re-implement the parser here to support the
  386. * flexibility required
  387. */
  388. private static String prependBase(Object baseobj, String iri) {
  389. // already an absolute IRI
  390. if (iri.indexOf(":") != -1) {
  391. return iri;
  392. }
  393. // parse base if it is a string
  394. URL base;
  395. if (isString(baseobj)) {
  396. base = URL.parse((String) baseobj);
  397. } else {
  398. // assume base is already a URL
  399. base = (URL) baseobj;
  400. }
  401. final URL rel = URL.parse(iri);
  402. // start hierarchical part
  403. String hierPart = base.protocol;
  404. if (!"".equals(rel.authority)) {
  405. hierPart += "//" + rel.authority;
  406. } else if (!"".equals(base.href)) {
  407. hierPart += "//" + base.authority;
  408. }
  409. // per RFC3986 normalize
  410. String path;
  411. // IRI represents an absolute path
  412. if (rel.pathname.indexOf("/") == 0) {
  413. path = rel.pathname;
  414. } else {
  415. path = base.pathname;
  416. // append relative path to the end of the last directory from base
  417. if (!"".equals(rel.pathname)) {
  418. path = path.substring(0, path.lastIndexOf("/") + 1);
  419. if (path.length() > 0 && !path.endsWith("/")) {
  420. path += "/";
  421. }
  422. path += rel.pathname;
  423. }
  424. }
  425. // remove slashes anddots in path
  426. path = URL.removeDotSegments(path, !"".equals(hierPart));
  427. // add query and hash
  428. if (!"".equals(rel.query)) {
  429. path += "?" + rel.query;
  430. }
  431. if (!"".equals(rel.hash)) {
  432. path += rel.hash;
  433. }
  434. final String rval = hierPart + path;
  435. if ("".equals(rval)) {
  436. return "./";
  437. }
  438. return rval;
  439. }
  440. /**
  441. * Expands a language map.
  442. *
  443. * @param languageMap
  444. * the language map to expand.
  445. *
  446. * @return the expanded language map.
  447. * @throws JSONLDProcessingError
  448. */
  449. static List<Object> expandLanguageMap(Map<String, Object> languageMap)
  450. throws JSONLDProcessingError {
  451. final List<Object> rval = new ArrayList<Object>();
  452. final List<String> keys = new ArrayList<String>(languageMap.keySet());
  453. Collections.sort(keys); // lexicographically sort languages
  454. for (final String key : keys) {
  455. List<Object> val;
  456. if (!isArray(languageMap.get(key))) {
  457. val = new ArrayList<Object>();
  458. val.add(languageMap.get(key));
  459. } else {
  460. val = (List<Object>) languageMap.get(key);
  461. }
  462. for (final Object item : val) {
  463. if (!isString(item)) {
  464. throw new JSONLDProcessingError(
  465. "Invalid JSON-LD syntax; language map values must be strings.")
  466. .setDetail("languageMap", languageMap).setType(
  467. JSONLDProcessingError.Error.SYNTAX_ERROR);
  468. }
  469. final Map<String, Object> tmp = new LinkedHashMap<String, Object>();
  470. tmp.put("@value", item);
  471. tmp.put("@language", key.toLowerCase());
  472. rval.add(tmp);
  473. }
  474. }
  475. return rval;
  476. }
  477. /**
  478. * Expands the given value by using the coercion and keyword rules in the
  479. * given context.
  480. *
  481. * @param ctx
  482. * the active context to use.
  483. * @param property
  484. * the property the value is associated with.
  485. * @param value
  486. * the value to expand.
  487. * @param base
  488. * the base IRI to use.
  489. *
  490. * @return the expanded value.
  491. * @throws JSONLDProcessingError
  492. */
  493. static Object expandValue(ActiveContext activeCtx, String activeProperty, Object value)
  494. throws JSONLDProcessingError {
  495. // nothing to expand
  496. if (value == null) {
  497. return null;
  498. }
  499. // special-case expand @id and @type (skips '@id' expansion)
  500. final String expandedProperty = expandIri(activeCtx, activeProperty, false, true, null,
  501. null);
  502. if ("@id".equals(expandedProperty)) {
  503. return expandIri(activeCtx, (String) value, true, false, null, null);
  504. } else if ("@type".equals(expandedProperty)) {
  505. return expandIri(activeCtx, (String) value, true, true, null, null);
  506. }
  507. // get type definition from context
  508. final Object type = activeCtx.getContextValue(activeProperty, "@type");
  509. // do @id expansion (automatic for @graph)
  510. if ("@id".equals(type) || ("@graph".equals(expandedProperty) && isString(value))) {
  511. final Map<String, Object> tmp = new LinkedHashMap<String, Object>();
  512. tmp.put("@id", expandIri(activeCtx, (String) value, true, false, null, null));
  513. return tmp;
  514. }
  515. // do @id expansion w/vocab
  516. if ("@vocab".equals(type)) {
  517. final Map<String, Object> tmp = new LinkedHashMap<String, Object>();
  518. tmp.put("@id", expandIri(activeCtx, (String) value, true, true, null, null));
  519. return tmp;
  520. }
  521. // do not expand keyword values
  522. if (isKeyword(expandedProperty)) {
  523. return value;
  524. }
  525. final Map<String, Object> rval = new LinkedHashMap<String, Object>();
  526. // other type
  527. if (type != null) {
  528. rval.put("@type", type);
  529. }
  530. // check for language tagging
  531. else if (isString(value)) {
  532. final Object language = activeCtx.getContextValue(activeProperty, "@language");
  533. if (language != null) {
  534. rval.put("@language", language);
  535. }
  536. }
  537. rval.put("@value", value);
  538. return rval;
  539. }
  540. /**
  541. * Throws an exception if the given value is not a valid @type value.
  542. *
  543. * @param v
  544. * the value to check.
  545. * @throws JSONLDProcessingError
  546. */
  547. static boolean validateTypeValue(Object v) throws JSONLDProcessingError {
  548. if (v == null) {
  549. throw new NullPointerException("\"@type\" value cannot be null");
  550. }
  551. // must be a string, subject reference, or empty object
  552. if (v instanceof String
  553. || (v instanceof Map && (((Map<String, Object>) v).containsKey("@id") || ((Map<String, Object>) v)
  554. .size() == 0))) {
  555. return true;
  556. }
  557. // must be an array
  558. boolean isValid = false;
  559. if (v instanceof List) {
  560. isValid = true;
  561. for (final Object i : (List) v) {
  562. if (!(i instanceof String || i instanceof Map
  563. && ((Map<String, Object>) i).containsKey("@id"))) {
  564. isValid = false;
  565. break;
  566. }
  567. }
  568. }
  569. if (!isValid) {
  570. throw new JSONLDProcessingError(
  571. "Invalid JSON-LD syntax; \"@type\" value must a string, a subject reference, an array of strings or subject references, or an empty object.")
  572. .setType(JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail("value", v);
  573. }
  574. return true;
  575. }
  576. /**
  577. * Compacts an IRI or keyword into a term or prefix if it can be. If the IRI
  578. * has an associated value it may be passed.
  579. *
  580. * @param activeCtx
  581. * the active context to use.
  582. * @param iri
  583. * the IRI to compact.
  584. * @param value
  585. * the value to check or null.
  586. * @param relativeTo
  587. * options for how to compact IRIs: vocab: true to split after
  588. * @vocab, false not to.
  589. * @param reverse
  590. * true if a reverse property is being compacted, false if not.
  591. *
  592. * @return the compacted term, prefix, keyword alias, or the original IRI.
  593. */
  594. static String compactIri(ActiveContext activeCtx, String iri, Object value,
  595. boolean relativeToVocab, boolean reverse) {
  596. // can't compact null
  597. if (iri == null) {
  598. return iri;
  599. }
  600. // term is a keyword, default vocab to true
  601. if (isKeyword(iri)) {
  602. relativeToVocab = true;
  603. }
  604. // use inverse context to pick a term if iri is relative to vocab
  605. if (relativeToVocab && activeCtx.getInverse().containsKey(iri)) {
  606. String defaultLanguage = (String) activeCtx.get("@language");
  607. if (defaultLanguage == null) {
  608. defaultLanguage = "@none";
  609. }
  610. // prefer @index if available in value
  611. final List<String> containers = new ArrayList<String>();
  612. if (isObject(value) && ((Map<String, Object>) value).containsKey("@index")) {
  613. containers.add("@index");
  614. }
  615. // defaults for term selection based on type/language
  616. String typeOrLanguage = "@language";
  617. String typeOrLanguageValue = "@null";
  618. if (reverse) {
  619. typeOrLanguage = "@type";
  620. typeOrLanguageValue = "@reverse";
  621. containers.add("@set");
  622. }
  623. // choose the most specific term that works for all elements in
  624. // @list
  625. else if (isList(value)) {
  626. // only select @list containers if @index is NOT in value
  627. if (!((Map<String, Object>) value).containsKey("@index")) {
  628. containers.add("@list");
  629. }
  630. final List<Object> list = (List<Object>) ((Map<String, Object>) value).get("@list");
  631. String commonLanguage = (list.size() == 0) ? defaultLanguage : null;
  632. String commonType = null;
  633. for (final Object item : list) {
  634. String itemLanguage = "@none";
  635. String itemType = "@none";
  636. if (isValue(item)) {
  637. if (((Map<String, Object>) item).containsKey("@language")) {
  638. itemLanguage = (String) ((Map<String, Object>) item).get("@language");
  639. } else if (((Map<String, Object>) item).containsKey("@type")) {
  640. itemType = (String) ((Map<String, Object>) item).get("@type");
  641. }
  642. // plain literal
  643. else {
  644. itemLanguage = "@null";
  645. }
  646. } else {
  647. itemType = "@id";
  648. }
  649. if (commonLanguage == null) {
  650. commonLanguage = itemLanguage;
  651. } else if (!itemLanguage.equals(commonLanguage) && isValue(item)) {
  652. commonLanguage = "@none";
  653. }
  654. if (commonType == null) {
  655. commonType = itemType;
  656. } else if (!itemType.equals(commonType)) {
  657. commonType = "@none";
  658. }
  659. // there are different languages and types in the list, so
  660. // choose
  661. // the most generic term, no need to keep iterating the list
  662. if ("@none".equals(commonLanguage) && "@none".equals(commonType)) {
  663. break;
  664. }
  665. }
  666. commonLanguage = (commonLanguage != null) ? commonLanguage : "@none";
  667. commonType = (commonType != null) ? commonType : "@none";
  668. if (!"@none".equals(commonType)) {
  669. typeOrLanguage = "@type";
  670. typeOrLanguageValue = commonType;
  671. } else {
  672. typeOrLanguageValue = commonLanguage;
  673. }
  674. } else {
  675. if (isValue(value)) {
  676. if (((Map<String, Object>) value).containsKey("@language")
  677. && !((Map<String, Object>) value).containsKey("@index")) {
  678. containers.add("@language");
  679. typeOrLanguageValue = (String) ((Map<String, Object>) value)
  680. .get("@language");
  681. } else if (((Map<String, Object>) value).containsKey("@type")) {
  682. typeOrLanguage = "@type";
  683. typeOrLanguageValue = (String) ((Map<String, Object>) value).get("@type");
  684. }
  685. } else {
  686. typeOrLanguage = "@type";
  687. typeOrLanguageValue = "@id";
  688. }
  689. containers.add("@set");
  690. }
  691. // do term selection
  692. containers.add("@none");
  693. final String term = selectTerm(activeCtx, iri, value, containers, typeOrLanguage,
  694. typeOrLanguageValue);
  695. if (term != null) {
  696. return term;
  697. }
  698. }
  699. // no term match, use @vocab if available
  700. if (relativeToVocab) {
  701. if (activeCtx.containsKey("@vocab")) {
  702. // determine if vocab is a prefix of the iri
  703. final String vocab = (String) activeCtx.get("@vocab");
  704. if (iri.indexOf(vocab) == 0 && !iri.equals(vocab)) {
  705. // use suffix as relative iri if it is not a term in the
  706. // active context
  707. final String suffix = iri.substring(vocab.length());
  708. if (!activeCtx.mappings.containsKey(suffix)) {
  709. return suffix;
  710. }
  711. }
  712. }
  713. }
  714. // no term of @vocab match, check for possible CURIEs
  715. String choice = null;
  716. for (final String term : activeCtx.mappings.keySet()) {
  717. // skip terms with colons, they can't be prefixes
  718. if (term.indexOf(":") != -1) {
  719. continue;
  720. }
  721. // skip entries with @ids that are not partial matches
  722. final Map<String, Object> definition = (Map<String, Object>) activeCtx.mappings
  723. .get(term);
  724. if (definition == null || iri.equals(definition.get("@id"))
  725. || iri.indexOf((String) definition.get("@id")) != 0) {
  726. continue;
  727. }
  728. // a CURIE is usable if:
  729. // 1. it has no mapping, OR
  730. // 2. value is null, which means we're not compacting an @value, AND
  731. // the mapping matches the IRI
  732. final String curie = term + ":"
  733. + iri.substring(((String) definition.get("@id")).length());
  734. final Boolean isUsableCurie = (!activeCtx.mappings.containsKey(curie) || (value == null
  735. && activeCtx.mappings.get(curie) != null && iri
  736. .equals(((Map<String, Object>) activeCtx.mappings.get(curie)).get("@id"))));
  737. // select curie if it is shorter or the same length but
  738. // lexicographically
  739. // less than the current choice
  740. if (isUsableCurie && (choice == null || compareShortestLeast(curie, choice) < 0)) {
  741. choice = curie;
  742. }
  743. }
  744. // return chosen curie
  745. if (choice != null) {
  746. return choice;
  747. }
  748. // compact IRI relative to base
  749. if (!relativeToVocab) {
  750. return removeBase(activeCtx.get("@base"), iri);
  751. }
  752. // return IRI as is
  753. return iri;
  754. }
  755. static String compactIri(ActiveContext ctx, String iri) {
  756. return compactIri(ctx, iri, null, false, false);
  757. }
  758. /**
  759. * Removes a base IRI from the given absolute IRI.
  760. *
  761. * @param base
  762. * the base IRI.
  763. * @param iri
  764. * the absolute IRI.
  765. *
  766. * @return the relative IRI if relative to base, otherwise the absolute IRI.
  767. */
  768. private static String removeBase(Object baseobj, String iri) {
  769. URL base;
  770. if (isString(baseobj)) {
  771. base = URL.parse((String) baseobj);
  772. } else {
  773. base = (URL) baseobj;
  774. }
  775. // establish base root
  776. String root = "";
  777. if (!"".equals(base.href)) {
  778. root += (base.protocol) + "//" + base.authority;
  779. }
  780. // support network-path reference with empty base
  781. else if (iri.indexOf("//") != 0) {
  782. root += "//";
  783. }
  784. // IRI not relative to base
  785. if (iri.indexOf(root) != 0) {
  786. return iri;
  787. }
  788. // remove root from IRI and parse remainder
  789. final URL rel = URL.parse(iri.substring(root.length()));
  790. // remove path segments that match
  791. final List<String> baseSegments = _split(base.normalizedPath, "/");
  792. final List<String> iriSegments = _split(rel.normalizedPath, "/");
  793. while (baseSegments.size() > 0 && iriSegments.size() > 0) {
  794. if (!baseSegments.get(0).equals(iriSegments.get(0))) {
  795. break;
  796. }
  797. if (baseSegments.size() > 0) {
  798. baseSegments.remove(0);
  799. }
  800. if (iriSegments.size() > 0) {
  801. iriSegments.remove(0);
  802. }
  803. }
  804. // use '../' for each non-matching base segment
  805. String rval = "";
  806. if (baseSegments.size() > 0) {
  807. // don't count the last segment if it isn't a path (doesn't end in
  808. // '/')
  809. // don't count empty first segment, it means base began with '/'
  810. if (!base.normalizedPath.endsWith("/") || "".equals(baseSegments.get(0))) {
  811. baseSegments.remove(baseSegments.size() - 1);
  812. }
  813. for (int i = 0; i < baseSegments.size(); ++i) {
  814. rval += "../";
  815. }
  816. }
  817. // prepend remaining segments
  818. rval += _join(iriSegments, "/");
  819. // add query and hash
  820. if (!"".equals(rel.query)) {
  821. rval += "?" + rel.query;
  822. }
  823. if (!"".equals(rel.hash)) {
  824. rval += rel.hash;
  825. }
  826. if ("".equals(rval)) {
  827. rval = "./";
  828. }
  829. return rval;
  830. }
  831. /**
  832. * Removes the @preserve keywords as the last step of the framing algorithm.
  833. *
  834. * @param ctx
  835. * the active context used to compact the input.
  836. * @param input
  837. * the framed, compacted output.
  838. * @param options
  839. * the compaction options used.
  840. *
  841. * @return the resulting output.
  842. */
  843. static Object removePreserve(ActiveContext ctx, Object input, Options opts) {
  844. // recurse through arrays
  845. if (isArray(input)) {
  846. final List<Object> output = new ArrayList<Object>();
  847. for (final Object i : (List<Object>) input) {
  848. final Object result = removePreserve(ctx, i, opts);
  849. // drop nulls from arrays
  850. if (result != null) {
  851. output.add(result);
  852. }
  853. }
  854. input = output;
  855. } else if (isObject(input)) {
  856. // remove @preserve
  857. if (((Map<String, Object>) input).containsKey("@preserve")) {
  858. if ("@null".equals(((Map<String, Object>) input).get("@preserve"))) {
  859. return null;
  860. }
  861. return ((Map<String, Object>) input).get("@preserve");
  862. }
  863. // skip @values
  864. if (isValue(input)) {
  865. return input;
  866. }
  867. // recurse through @lists
  868. if (isList(input)) {
  869. ((Map<String, Object>) input).put("@list",
  870. removePreserve(ctx, ((Map<String, Object>) input).get("@list"), opts));
  871. return input;
  872. }
  873. // recurse through properties
  874. for (final String prop : ((Map<String, Object>) input).keySet()) {
  875. Object result = removePreserve(ctx, ((Map<String, Object>) input).get(prop), opts);
  876. final String container = (String) ctx.getContextValue(prop, "@container");
  877. if (opts.compactArrays && isArray(result) && ((List<Object>) result).size() == 1
  878. && container == null) {
  879. result = ((List<Object>) result).get(0);
  880. }
  881. ((Map<String, Object>) input).put(prop, result);
  882. }
  883. }
  884. return input;
  885. }
  886. /**
  887. * replicate javascript .join because i'm too lazy to keep doing it manually
  888. *
  889. * @param iriSegments
  890. * @param string
  891. * @return
  892. */
  893. private static String _join(List<String> list, String joiner) {
  894. String rval = "";
  895. if (list.size() > 0) {
  896. rval += list.get(0);
  897. }
  898. for (int i = 1; i < list.size(); i++) {
  899. rval += joiner + list.get(i);
  900. }
  901. return rval;
  902. }
  903. /**
  904. * replicates the functionality of javascript .split, which has different
  905. * results to java's String.split if there is a trailing /
  906. *
  907. * @param string
  908. * @param delim
  909. * @return
  910. */
  911. private static List<String> _split(String string, String delim) {
  912. final List<String> rval = new ArrayList<String>(Arrays.asList(string.split(delim)));
  913. if (string.endsWith("/")) {
  914. // javascript .split includes a blank entry if the string ends with
  915. // the delimiter, java .split does not so we need to add it manually
  916. rval.add("");
  917. }
  918. return rval;
  919. }
  920. /**
  921. * Compares two strings first based on length and then lexicographically.
  922. *
  923. * @param a
  924. * the first string.
  925. * @param b
  926. * the second string.
  927. *
  928. * @return -1 if a < b, 1 if a > b, 0 if a == b.
  929. */
  930. static int compareShortestLeast(String a, String b) {
  931. if (a.length() < b.length()) {
  932. return -1;
  933. } else if (b.length() < a.length()) {
  934. return 1;
  935. }
  936. return Integer.signum(a.compareTo(b));
  937. }
  938. /**
  939. * Picks the preferred compaction term from the given inverse context entry.
  940. *
  941. * @param activeCtx
  942. * the active context.
  943. * @param iri
  944. * the IRI to pick the term for.
  945. * @param value
  946. * the value to pick the term for.
  947. * @param containers
  948. * the preferred containers.
  949. * @param typeOrLanguage
  950. * either '@type' or '@language'.
  951. * @param typeOrLanguageValue
  952. * the preferred value for '@type' or '@language'.
  953. *
  954. * @return the preferred term.
  955. */
  956. private static String selectTerm(ActiveContext activeCtx, String iri, Object value,
  957. List<String> containers, String typeOrLanguage, String typeOrLanguageValue) {
  958. if (typeOrLanguageValue == null) {
  959. typeOrLanguageValue = "@null";
  960. }
  961. // preferences for the value of @type or @language
  962. final List<String> prefs = new ArrayList<String>();
  963. // determine prefs for @id based on whether or not value compacts to a
  964. // term
  965. if ((("@id").equals(typeOrLanguageValue) || "@reverse".equals(typeOrLanguageValue))
  966. && isSubjectReference(value)) {
  967. // prefer @reverse first
  968. if ("@reverse".equals(typeOrLanguageValue)) {
  969. prefs.add("@reverse");
  970. }
  971. // try to compact value to a term
  972. final String term = compactIri(activeCtx,
  973. (String) ((Map<String, Object>) value).get("@id"), null, true, false);
  974. if (activeCtx.mappings.containsKey(term)
  975. && activeCtx.mappings.get(term) != null
  976. && ((Map<String, Object>) value).get("@id").equals(
  977. ((Map<String, Object>) activeCtx.mappings.get(term)).get("@id"))) {
  978. // prefer @vocab
  979. prefs.add("@vocab");
  980. prefs.add("@id");
  981. } else {
  982. // prefer @id
  983. prefs.add("@id");
  984. prefs.add("@vocab");
  985. }
  986. } else {
  987. prefs.add(typeOrLanguageValue);
  988. }
  989. prefs.add("@none");
  990. final Map<String, Object> containerMap = (Map<String, Object>) activeCtx.inverse.get(iri);
  991. for (final String container : containers) {
  992. // if container not available in the map, continue
  993. if (!containerMap.containsKey(container)) {
  994. continue;
  995. }
  996. final Map<String, Object> typeOrLanguageValueMap = (Map<String, Object>) Obj.get(
  997. containerMap, container, typeOrLanguage);
  998. for (final String pref : prefs) {
  999. // if type/language option not available in the map, continue
  1000. if (!typeOrLanguageValueMap.containsKey(pref)) {
  1001. continue;
  1002. }
  1003. // select term
  1004. return (String) typeOrLanguageValueMap.get(pref);
  1005. }
  1006. }
  1007. return null;
  1008. }
  1009. /**
  1010. * Performs value compaction on an object with '@value' or '@id' as the only
  1011. * property.
  1012. *
  1013. * @param activeCtx
  1014. * the active context.
  1015. * @param activeProperty
  1016. * the active property that points to the value.
  1017. * @param value
  1018. * the value to compact.
  1019. *
  1020. * @return the compaction result.
  1021. * @throws JSONLDProcessingError
  1022. */
  1023. static Object compactValue(ActiveContext activeCtx, String activeProperty, Object value)
  1024. throws JSONLDProcessingError {
  1025. // value is a @value
  1026. if (isValue(value)) {
  1027. // get context rules
  1028. final String type = (String) activeCtx.getContextValue(activeProperty, "@type");
  1029. final String language = (String) activeCtx.getContextValue(activeProperty, "@language");
  1030. final String container = (String) activeCtx.getContextValue(activeProperty,
  1031. "@container");
  1032. // whether or not the value has an @index that must be preserved
  1033. final Boolean preserveIndex = (((Map<String, Object>) value).containsKey("@index") && !"@index"
  1034. .equals(container));
  1035. // if there's no @index to preserve ...
  1036. if (!preserveIndex) {
  1037. // matching @type or @language specified in context, compact
  1038. // value
  1039. if ((((Map<String, Object>) value).containsKey("@type") && JSONUtils.equals(
  1040. ((Map<String, Object>) value).get("@type"), type))
  1041. || (((Map<String, Object>) value).containsKey("@language") && JSONUtils
  1042. .equals(((Map<String, Object>) value).get("@language"), language))) {
  1043. // NOTE: have to check containsKey here as javascript
  1044. // version relies on undefined !== null
  1045. return ((Map<String, Object>) value).get("@value");
  1046. }
  1047. }
  1048. // return just the value of @value if all are true:
  1049. // 1. @value is the only key or @index isn't being preserved
  1050. // 2. there is no default language or @value is not a string or
  1051. // the key has a mapping with a null @language
  1052. final int keyCount = ((Map<String, Object>) value).size();
  1053. final Boolean isValueOnlyKey = (keyCount == 1 || (keyCount == 2
  1054. && ((Map<String, Object>) value).containsKey("@index") && !preserveIndex));
  1055. final Boolean hasDefaultLanguage = activeCtx.containsKey("@language");
  1056. final Boolean isValueString = isString(((Map<String, Object>) value).get("@value"));
  1057. final Boolean hasNullMapping = activeCtx.mappings.containsKey(activeProperty)
  1058. && ((Map<String, Object>) activeCtx.mappings.get(activeProperty))
  1059. .containsKey("@language")
  1060. && Obj.get(activeCtx.mappings, activeProperty, "@language") == null;
  1061. if (isValueOnlyKey && (!hasDefaultLanguage || !isValueString || hasNullMapping)) {
  1062. return ((Map<String, Object>) value).get("@value");
  1063. }
  1064. final Map<String, Object> rval = new LinkedHashMap<String, Object>();
  1065. // preserve @index
  1066. if (preserveIndex) {
  1067. rval.put(compactIri(activeCtx, "@index"),
  1068. ((Map<String, Object>) value).get("@index"));
  1069. }
  1070. // compact @type IRI
  1071. if (((Map<String, Object>) value).containsKey("@type")) {
  1072. rval.put(
  1073. compactIri(activeCtx, "@type"),
  1074. compactIri(activeCtx, (String) ((Map<String, Object>) value).get("@type"),
  1075. null, true, false));
  1076. }
  1077. // alias @language
  1078. else if (((Map<String, Object>) value).containsKey("@language")) {
  1079. rval.put(compactIri(activeCtx, "@language"),
  1080. ((Map<String, Object>) value).get("@language"));
  1081. }
  1082. // alias @value
  1083. rval.put(compactIri(activeCtx, "@value"), ((Map<String, Object>) value).get("@value"));
  1084. return rval;
  1085. }
  1086. // value is a subject reference
  1087. final String expandedProperty = expandIri(activeCtx, activeProperty, false, true, null,
  1088. null);
  1089. final String type = (String) activeCtx.getContextValue(activeProperty, "@type");
  1090. final Object compacted = compactIri(activeCtx,
  1091. (String) ((Map<String, Object>) value).get("@id"), null, "@vocab".equals(type),
  1092. false);
  1093. if ("@id".equals(type) || "@vocab".equals(type) || "@graph".equals(type)) {
  1094. return compacted;
  1095. }
  1096. final Map<String, Object> rval = new LinkedHashMap<String, Object>();
  1097. rval.put(compactIri(activeCtx, "@id"), compacted);
  1098. return rval;
  1099. }
  1100. /**
  1101. * Recursively flattens the subjects in the given JSON-LD expanded input
  1102. * into a node map.
  1103. *
  1104. * @param input
  1105. * the JSON-LD expanded input.
  1106. * @param graphs
  1107. * a map of graph name to subject map.
  1108. * @param graph
  1109. * the name of the current graph.
  1110. * @param namer
  1111. * the blank node namer.
  1112. * @param name
  1113. * the name assigned to the current input if it is a bnode.
  1114. * @param list
  1115. * the list to append to, null for none.
  1116. * @throws JSONLDProcessingError
  1117. */
  1118. static void createNodeMap(Object input, Map<String, Object> graphs, String graph,
  1119. UniqueNamer namer, String name, List<Object> list) throws JSONLDProcessingError {
  1120. // recurce through array
  1121. if (isArray(input)) {
  1122. for (final Object i : (List<Object>) input) {
  1123. createNodeMap(i, graphs, graph, namer, null, list);
  1124. }
  1125. return;
  1126. }
  1127. // add non-object to list
  1128. if (!isObject(input)) {
  1129. if (list != null) {
  1130. list.add(input);
  1131. }
  1132. return;
  1133. }
  1134. // add value to list
  1135. if (isValue(input)) {
  1136. if (((Map<String, Object>) input).containsKey("@type")) {
  1137. String type = (String) ((Map<String, Object>) input).get("@type");
  1138. // rename @type blank node
  1139. if (type.indexOf("_:") == 0) {
  1140. type = namer.getName(type);
  1141. ((Map<String, Object>) input).put("@type", type);
  1142. }
  1143. if (!((Map<String, Object>) graphs.get(graph)).containsKey(type)) {
  1144. final Map<String, Object> tmp = new LinkedHashMap<String, Object>();
  1145. tmp.put("@id", type);
  1146. ((Map<String, Object>) graphs.get(graph)).put(type, tmp);
  1147. }
  1148. }

Large files files are truncated, but you can click here to view the full file