PageRenderTime 56ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/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
  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. }
  1149. if (list != null) {
  1150. list.add(input);
  1151. }
  1152. return;
  1153. }
  1154. // NOTE: At this point, input must be a subject.
  1155. // get name for subject
  1156. if (name == null) {
  1157. name = isBlankNode(input) ? namer.getName((String) ((Map<String, Object>) input)
  1158. .get("@id")) : (String) ((Map<String, Object>) input).get("@id");
  1159. }
  1160. // add subject reference to list
  1161. if (list != null) {
  1162. final Map<String, Object> tmp = new LinkedHashMap<String, Object>();
  1163. tmp.put("@id", name);
  1164. list.add(tmp);
  1165. }
  1166. // create new subject or merge into existing one
  1167. final Map<String, Object> subjects = (Map<String, Object>) graphs.get(graph);
  1168. Map<String, Object> subject;
  1169. if (subjects.containsKey(name)) {
  1170. subject = (Map<String, Object>) subjects.get(name);
  1171. } else {
  1172. subject = new LinkedHashMap<String, Object>();
  1173. subjects.put(name, subject);
  1174. }
  1175. subject.put("@id", name);
  1176. final List<String> properties = new ArrayList<String>(
  1177. ((Map<String, Object>) input).keySet());
  1178. Collections.sort(properties);
  1179. for (String property : properties) {
  1180. // skip @id
  1181. if ("@id".equals(property)) {
  1182. continue;
  1183. }
  1184. // handle reverse properties
  1185. if ("@reverse".equals(property)) {
  1186. final Map<String, Object> referencedNode = new LinkedHashMap<String, Object>();
  1187. referencedNode.put("@id", name);
  1188. final Map<String, Object> reverseMap = (Map<String, Object>) ((Map<String, Object>) input)
  1189. .get("@reverse");
  1190. for (final String reverseProperty : reverseMap.keySet()) {
  1191. for (final Object item : (List<Object>) reverseMap.get(reverseProperty)) {
  1192. addValue((Map<String, Object>) item, reverseProperty, referencedNode, true,
  1193. false);
  1194. createNodeMap(item, graphs, graph, namer);
  1195. }
  1196. }
  1197. continue;
  1198. }
  1199. // recurse into graph
  1200. if ("@graph".equals(property)) {
  1201. // add graph subjects map entry
  1202. if (!graphs.containsKey(name)) {
  1203. graphs.put(name, new LinkedHashMap<String, Object>());
  1204. }
  1205. final String g = "@merged".equals(graph) ? graph : name;
  1206. createNodeMap(((Map<String, Object>) input).get(property), graphs, g, namer);
  1207. continue;
  1208. }
  1209. // copy non-@type keywords
  1210. if (!"@type".equals(property) && isKeyword(property)) {
  1211. if ("@index".equals(property) && subjects.containsKey("@index")) {
  1212. throw new JSONLDProcessingError(
  1213. "Invalid JSON-LD syntax; conflicting @index property detected.")
  1214. .setType(JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail("subject",
  1215. subject);
  1216. }
  1217. subject.put(property, ((Map<String, Object>) input).get(property));
  1218. continue;
  1219. }
  1220. // iterate over objects
  1221. final List<Object> objects = (List<Object>) ((Map<String, Object>) input).get(property);
  1222. // if property is a bnode, assign it a new id
  1223. if (property.indexOf("_:") == 0) {
  1224. property = namer.getName(property);
  1225. }
  1226. // ensure property is added for empty arrays
  1227. if (objects.size() == 0) {
  1228. addValue(subject, property, new ArrayList<Object>(), true);
  1229. continue;
  1230. }
  1231. for (Object o : objects) {
  1232. if ("@type".equals(property)) {
  1233. // rename @type blank nodes
  1234. o = (((String) o).indexOf("_:") == 0) ? namer.getName((String) o) : o;
  1235. if (!((Map<String, Object>) graphs.get(graph)).containsKey(o)) {
  1236. final Map<String, Object> tmp = new LinkedHashMap<String, Object>();
  1237. tmp.put("@id", o);
  1238. ((Map<String, Object>) graphs.get(graph)).put((String) o, tmp);
  1239. }
  1240. }
  1241. // handle embedded subject or subject reference
  1242. if (isSubject(o) || isSubjectReference(o)) {
  1243. // rename blank node @id
  1244. final String id = isBlankNode(o) ? namer
  1245. .getName((String) ((Map<String, Object>) o).get("@id"))
  1246. : (String) ((Map<String, Object>) o).get("@id");
  1247. // add reference and recurse
  1248. final Map<String, Object> tmp = new LinkedHashMap<String, Object>();
  1249. tmp.put("@id", id);
  1250. addValue(subject, property, tmp, true, false);
  1251. createNodeMap(o, graphs, graph, namer, id);
  1252. }
  1253. // handle @list
  1254. else if (isList(o)) {
  1255. final List<Object> _list = new ArrayList<Object>();
  1256. createNodeMap(((Map<String, Object>) o).get("@list"), graphs, graph, namer,
  1257. name, _list);
  1258. o = new LinkedHashMap<String, Object>();
  1259. ((Map<String, Object>) o).put("@list", _list);
  1260. addValue(subject, property, o, true, false);
  1261. }
  1262. // handle @value
  1263. else {
  1264. createNodeMap(o, graphs, graph, namer, name);
  1265. addValue(subject, property, o, true, false);
  1266. }
  1267. }
  1268. }
  1269. }
  1270. static void createNodeMap(Object input, Map<String, Object> graphs, String graph,
  1271. UniqueNamer namer, String name) throws JSONLDProcessingError {
  1272. createNodeMap(input, graphs, graph, namer, name, null);
  1273. }
  1274. static void createNodeMap(Object input, Map<String, Object> graphs, String graph,
  1275. UniqueNamer namer) throws JSONLDProcessingError {
  1276. createNodeMap(input, graphs, graph, namer, null, null);
  1277. }
  1278. /**
  1279. * Determines if the given value is a property of the given subject.
  1280. *
  1281. * @param subject
  1282. * the subject to check.
  1283. * @param property
  1284. * the property to check.
  1285. * @param value
  1286. * the value to check.
  1287. *
  1288. * @return true if the value exists, false if not.
  1289. */
  1290. static boolean hasValue(Map<String, Object> subject, String property, Object value) {
  1291. boolean rval = false;
  1292. if (hasProperty(subject, property)) {
  1293. Object val = subject.get(property);
  1294. final boolean isList = isList(val);
  1295. if (isList || val instanceof List) {
  1296. if (isList) {
  1297. val = ((Map<String, Object>) val).get("@list");
  1298. }
  1299. for (final Object i : (List) val) {
  1300. if (compareValues(value, i)) {
  1301. rval = true;
  1302. break;
  1303. }
  1304. }
  1305. } else if (!(value instanceof List)) {
  1306. rval = compareValues(value, val);
  1307. }
  1308. }
  1309. return rval;
  1310. }
  1311. private static boolean hasProperty(Map<String, Object> subject, String property) {
  1312. boolean rval = false;
  1313. if (subject.containsKey(property)) {
  1314. final Object value = subject.get(property);
  1315. rval = (!(value instanceof List) || ((List) value).size() > 0);
  1316. }
  1317. return rval;
  1318. }
  1319. /**
  1320. * Compares two JSON-LD values for equality. Two JSON-LD values will be
  1321. * considered equal if:
  1322. *
  1323. * 1. They are both primitives of the same type and value. 2. They are both @values
  1324. * with the same @value, @type, and @language, OR 3. They both have @ids
  1325. * they are the same.
  1326. *
  1327. * @param v1
  1328. * the first value.
  1329. * @param v2
  1330. * the second value.
  1331. *
  1332. * @return true if v1 and v2 are considered equal, false if not.
  1333. */
  1334. static boolean compareValues(Object v1, Object v2) {
  1335. if (v1.equals(v2)) {
  1336. return true;
  1337. }
  1338. if (isValue(v1)
  1339. && isValue(v2)
  1340. && JSONUtils.equals(((Map<String, Object>) v1).get("@value"),
  1341. ((Map<String, Object>) v2).get("@value"))
  1342. && JSONUtils.equals(((Map<String, Object>) v1).get("@type"),
  1343. ((Map<String, Object>) v2).get("@type"))
  1344. && JSONUtils.equals(((Map<String, Object>) v1).get("@language"),
  1345. ((Map<String, Object>) v2).get("@language"))
  1346. && JSONUtils.equals(((Map<String, Object>) v1).get("@index"),
  1347. ((Map<String, Object>) v2).get("@index"))) {
  1348. return true;
  1349. }
  1350. if ((v1 instanceof Map && ((Map<String, Object>) v1).containsKey("@id"))
  1351. && (v2 instanceof Map && ((Map<String, Object>) v2).containsKey("@id"))
  1352. && ((Map<String, Object>) v1).get("@id").equals(
  1353. ((Map<String, Object>) v2).get("@id"))) {
  1354. return true;
  1355. }
  1356. return false;
  1357. }
  1358. /**
  1359. * Removes a value from a subject.
  1360. *
  1361. * @param subject
  1362. * the subject.
  1363. * @param property
  1364. * the property that relates the value to the subject.
  1365. * @param value
  1366. * the value to remove.
  1367. * @param [options] the options to use: [propertyIsArray] true if the
  1368. * property is always an array, false if not (default: false).
  1369. */
  1370. static void removeValue(Map<String, Object> subject, String property, Map<String, Object> value) {
  1371. removeValue(subject, property, value, false);
  1372. }
  1373. static void removeValue(Map<String, Object> subject, String property,
  1374. Map<String, Object> value, boolean propertyIsArray) {
  1375. // filter out value
  1376. final List<Object> values = new ArrayList<Object>();
  1377. if (subject.get(property) instanceof List) {
  1378. for (final Object e : ((List) subject.get(property))) {
  1379. if (!(value.equals(e))) {
  1380. values.add(value);
  1381. }
  1382. }
  1383. } else {
  1384. if (!value.equals(subject.get(property))) {
  1385. values.add(subject.get(property));
  1386. }
  1387. }
  1388. if (values.size() == 0) {
  1389. subject.remove(property);
  1390. } else if (values.size() == 1 && !propertyIsArray) {
  1391. subject.put(property, values.get(0));
  1392. } else {
  1393. subject.put(property, values);
  1394. }
  1395. }
  1396. /**
  1397. * Returns true if the given value is a blank node.
  1398. *
  1399. * @param v
  1400. * the value to check.
  1401. *
  1402. * @return true if the value is a blank node, false if not.
  1403. */
  1404. static boolean isBlankNode(Object v) {
  1405. // Note: A value is a blank node if all of these hold true:
  1406. // 1. It is an Object.
  1407. // 2. If it has an @id key its value begins with '_:'.
  1408. // 3. It has no keys OR is not a @value, @set, or @list.
  1409. if (v instanceof Map) {
  1410. if (((Map) v).containsKey("@id")) {
  1411. return ((String) ((Map) v).get("@id")).startsWith("_:");
  1412. } else {
  1413. return ((Map) v).size() == 0
  1414. || !(((Map) v).containsKey("@value") || ((Map) v).containsKey("@set") || ((Map) v)
  1415. .containsKey("@list"));
  1416. }
  1417. }
  1418. return false;
  1419. }
  1420. /**
  1421. * Returns true if the given value is a subject with properties.
  1422. *
  1423. * @param v
  1424. * the value to check.
  1425. *
  1426. * @return true if the value is a subject with properties, false if not.
  1427. */
  1428. static boolean isSubject(Object v) {
  1429. // Note: A value is a subject if all of these hold true:
  1430. // 1. It is an Object.
  1431. // 2. It is not a @value, @set, or @list.
  1432. // 3. It has more than 1 key OR any existing key is not @id.
  1433. if (v instanceof Map
  1434. && !(((Map) v).containsKey("@value") || ((Map) v).containsKey("@set") || ((Map) v)
  1435. .containsKey("@list"))) {
  1436. return ((Map<String, Object>) v).size() > 1 || !((Map) v).containsKey("@id");
  1437. }
  1438. return false;
  1439. }
  1440. /**
  1441. * Returns true if the given value is a subject reference.
  1442. *
  1443. * @param v
  1444. * the value to check.
  1445. *
  1446. * @return true if the value is a subject reference, false if not.
  1447. */
  1448. static boolean isSubjectReference(Object v) {
  1449. // Note: A value is a subject reference if all of these hold true:
  1450. // 1. It is an Object.
  1451. // 2. It has a single key: @id.
  1452. return (v instanceof Map && ((Map<String, Object>) v).size() == 1 && ((Map<String, Object>) v)
  1453. .containsKey("@id"));
  1454. }
  1455. /**
  1456. * Resolves external @context URLs using the given URL resolver. Each
  1457. * instance of @context in the input that refers to a URL will be replaced
  1458. * with the JSON @context found at that URL.
  1459. *
  1460. * @param input
  1461. * the JSON-LD input with possible contexts.
  1462. * @param resolver
  1463. * (url, callback(err, jsonCtx)) the URL resolver to use.
  1464. * @param callback
  1465. * (err, input) called once the operation completes.
  1466. * @throws JSONLDProcessingError
  1467. */
  1468. static void resolveContextUrls(Object input) throws JSONLDProcessingError {
  1469. resolve(input, new LinkedHashMap<String, Object>());
  1470. }
  1471. private static void resolve(Object input, Map<String, Object> cycles)
  1472. throws JSONLDProcessingError {
  1473. final Pattern regex = Pattern
  1474. .compile("(http|https)://(\\w+:{0,1}\\w*@)?(\\S+)(:[0-9]+)?(/|/([\\w#!:.?+=&%@!\\-/]))?");
  1475. if (cycles.size() > MAX_CONTEXT_URLS) {
  1476. throw new JSONLDProcessingError("Maximum number of @context URLs exceeded.").setType(
  1477. JSONLDProcessingError.Error.CONTEXT_URL_ERROR).setDetail("max",
  1478. MAX_CONTEXT_URLS);
  1479. }
  1480. // for tracking the URLs to resolve
  1481. final Map<String, Object> urls = new LinkedHashMap<String, Object>();
  1482. // find all URLs in the given input
  1483. if (!findContextUrls(input, urls, false)) {
  1484. // finished
  1485. findContextUrls(input, urls, true);
  1486. }
  1487. // queue all unresolved URLs
  1488. final List<String> queue = new ArrayList<String>();
  1489. for (final String url : urls.keySet()) {
  1490. if (Boolean.FALSE.equals(urls.get(url))) {
  1491. // validate URL
  1492. if (!regex.matcher(url).matches()) {
  1493. throw new JSONLDProcessingError("Malformed URL.").setType(
  1494. JSONLDProcessingError.Error.INVALID_URL).setDetail("url", url);
  1495. }
  1496. queue.add(url);
  1497. }
  1498. }
  1499. // resolve URLs in queue
  1500. int count = queue.size();
  1501. for (final String url : queue) {
  1502. // check for context URL cycle
  1503. if (cycles.containsKey(url)) {
  1504. throw new JSONLDProcessingError("Cyclical @context URLs detected.").setType(
  1505. JSONLDProcessingError.Error.CONTEXT_URL_ERROR).setDetail("url", url);
  1506. }
  1507. final Map<String, Object> _cycles = (Map<String, Object>) clone(cycles);
  1508. _cycles.put(url, Boolean.TRUE);
  1509. try {
  1510. Map<String, Object> ctx = (Map<String, Object>) JSONUtils.fromURL(new java.net.URL(
  1511. url));
  1512. if (!ctx.containsKey("@context")) {
  1513. ctx = new LinkedHashMap<String, Object>();
  1514. ctx.put("@context", new LinkedHashMap<String, Object>());
  1515. }
  1516. resolve(ctx, _cycles);
  1517. urls.put(url, ctx.get("@context"));
  1518. count -= 1;
  1519. if (count == 0) {
  1520. findContextUrls(input, urls, true);
  1521. }
  1522. } catch (final JsonParseException e) {
  1523. throw new JSONLDProcessingError("URL does not resolve to a valid JSON-LD object.")
  1524. .setType(JSONLDProcessingError.Error.INVALID_URL).setDetail("url", url);
  1525. } catch (final MalformedURLException e) {
  1526. throw new JSONLDProcessingError("Malformed URL.").setType(
  1527. JSONLDProcessingError.Error.INVALID_URL).setDetail("url", url);
  1528. } catch (final IOException e) {
  1529. throw new JSONLDProcessingError("Unable to open URL.").setType(
  1530. JSONLDProcessingError.Error.INVALID_URL).setDetail("url", url);
  1531. }
  1532. }
  1533. }
  1534. /**
  1535. * Finds all @context URLs in the given JSON-LD input.
  1536. *
  1537. * @param input
  1538. * the JSON-LD input.
  1539. * @param urls
  1540. * a map of URLs (url => false/@contexts).
  1541. * @param replace
  1542. * true to replace the URLs in the given input with the
  1543. * @contexts from the urls map, false not to.
  1544. *
  1545. * @return true if new URLs to resolve were found, false if not.
  1546. */
  1547. private static boolean findContextUrls(Object input, Map<String, Object> urls, Boolean replace) {
  1548. final int count = urls.size();
  1549. if (input instanceof List) {
  1550. for (final Object i : (List) input) {
  1551. findContextUrls(i, urls, replace);
  1552. }
  1553. return count < urls.size();
  1554. } else if (input instanceof Map) {
  1555. for (final String key : ((Map<String, Object>) input).keySet()) {
  1556. if (!"@context".equals(key)) {
  1557. findContextUrls(((Map) input).get(key), urls, replace);
  1558. continue;
  1559. }
  1560. // get @context
  1561. final Object ctx = ((Map) input).get(key);
  1562. // array @context
  1563. if (ctx instanceof List) {
  1564. int length = ((List) ctx).size();
  1565. for (int i = 0; i < length; i++) {
  1566. Object _ctx = ((List) ctx).get(i);
  1567. if (_ctx instanceof String) {
  1568. // replace w/@context if requested
  1569. if (replace) {
  1570. _ctx = urls.get(_ctx);
  1571. if (_ctx instanceof List) {
  1572. // add flattened context
  1573. ((List) ctx).remove(i);
  1574. ((List) ctx).addAll((Collection) _ctx);
  1575. i += ((List) _ctx).size();
  1576. length += ((List) _ctx).size();
  1577. } else {
  1578. ((List) ctx).set(i, _ctx);
  1579. }
  1580. }
  1581. // @context URL found
  1582. else if (!urls.containsKey(_ctx)) {
  1583. urls.put((String) _ctx, Boolean.FALSE);
  1584. }
  1585. }
  1586. }
  1587. }
  1588. // string @context
  1589. else if (ctx instanceof String) {
  1590. // replace w/@context if requested
  1591. if (replace) {
  1592. ((Map) input).put(key, urls.get(ctx));
  1593. }
  1594. // @context URL found
  1595. else if (!urls.containsKey(ctx)) {
  1596. urls.put((String) ctx, Boolean.FALSE);
  1597. }
  1598. }
  1599. }
  1600. return (count < urls.size());
  1601. }
  1602. return false;
  1603. }
  1604. static Object clone(Object value) {// throws
  1605. // CloneNotSupportedException {
  1606. Object rval = null;
  1607. if (value instanceof Cloneable) {
  1608. try {
  1609. rval = value.getClass().getMethod("clone").invoke(value);
  1610. } catch (final Exception e) {
  1611. rval = e;
  1612. }
  1613. }
  1614. if (rval == null || rval instanceof Exception) {
  1615. // the object wasn't cloneable, or an error occured
  1616. if (value == null || value instanceof String || value instanceof Number
  1617. || value instanceof Boolean) {
  1618. // strings numbers and booleans are immutable
  1619. rval = value;
  1620. } else {
  1621. // TODO: making this throw runtime exception so it doesn't have
  1622. // to be caught
  1623. // because simply it should never fail in the case of JSON-LD
  1624. // and means that
  1625. // the input JSON-LD is invalid
  1626. throw new RuntimeException(new CloneNotSupportedException(
  1627. (rval instanceof Exception ? ((Exception) rval).getMessage() : "")));
  1628. }
  1629. }
  1630. return rval;
  1631. }
  1632. /**
  1633. * Returns true if the given value is a JSON-LD Array
  1634. *
  1635. * @param v
  1636. * the value to check.
  1637. * @return
  1638. */
  1639. static Boolean isArray(Object v) {
  1640. return (v instanceof List);
  1641. }
  1642. /**
  1643. * Returns true if the given value is a JSON-LD List
  1644. *
  1645. * @param v
  1646. * the value to check.
  1647. * @return
  1648. */
  1649. static Boolean isList(Object v) {
  1650. return (v instanceof Map && ((Map<String, Object>) v).containsKey("@list"));
  1651. }
  1652. /**
  1653. * Returns true if the given value is a JSON-LD Object
  1654. *
  1655. * @param v
  1656. * the value to check.
  1657. * @return
  1658. */
  1659. static Boolean isObject(Object v) {
  1660. return (v instanceof Map);
  1661. }
  1662. /**
  1663. * Returns true if the given value is a JSON-LD value
  1664. *
  1665. * @param v
  1666. * the value to check.
  1667. * @return
  1668. */
  1669. static Boolean isValue(Object v) {
  1670. return (v instanceof Map && ((Map<String, Object>) v).containsKey("@value"));
  1671. }
  1672. /**
  1673. * Returns true if the given value is a JSON-LD string
  1674. *
  1675. * @param v
  1676. * the value to check.
  1677. * @return
  1678. */
  1679. static Boolean isString(Object v) {
  1680. // TODO: should this return true for arrays of strings as well?
  1681. return (v instanceof String);
  1682. }
  1683. }