PageRenderTime 32ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 1ms

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

http://github.com/tristan/jsonld-java
Java | 1862 lines | 1185 code | 174 blank | 503 comment | 395 complexity | 8af154bce8aac54ccf141b99f7c7597b MD5 | raw file
Possible License(s): BSD-3-Clause
  1. package com.github.jsonldjava.core;
  2. import static com.github.jsonldjava.core.JSONLDConsts.RDF_FIRST;
  3. import static com.github.jsonldjava.core.JSONLDConsts.RDF_NIL;
  4. import static com.github.jsonldjava.core.JSONLDConsts.RDF_REST;
  5. import static com.github.jsonldjava.core.JSONLDConsts.RDF_TYPE;
  6. import static com.github.jsonldjava.core.JSONLDUtils.addValue;
  7. import static com.github.jsonldjava.core.JSONLDUtils.compactIri;
  8. import static com.github.jsonldjava.core.JSONLDUtils.compactValue;
  9. import static com.github.jsonldjava.core.JSONLDUtils.compareValues;
  10. import static com.github.jsonldjava.core.JSONLDUtils.createNodeMap;
  11. import static com.github.jsonldjava.core.JSONLDUtils.createTermDefinition;
  12. import static com.github.jsonldjava.core.JSONLDUtils.expandIri;
  13. import static com.github.jsonldjava.core.JSONLDUtils.expandLanguageMap;
  14. import static com.github.jsonldjava.core.JSONLDUtils.expandValue;
  15. import static com.github.jsonldjava.core.JSONLDUtils.hasValue;
  16. import static com.github.jsonldjava.core.JSONLDUtils.isAbsoluteIri;
  17. import static com.github.jsonldjava.core.JSONLDUtils.isArray;
  18. import static com.github.jsonldjava.core.JSONLDUtils.isKeyword;
  19. import static com.github.jsonldjava.core.JSONLDUtils.isList;
  20. import static com.github.jsonldjava.core.JSONLDUtils.isObject;
  21. import static com.github.jsonldjava.core.JSONLDUtils.isString;
  22. import static com.github.jsonldjava.core.JSONLDUtils.isSubjectReference;
  23. import static com.github.jsonldjava.core.JSONLDUtils.isValue;
  24. import static com.github.jsonldjava.core.JSONLDUtils.removeValue;
  25. import static com.github.jsonldjava.core.JSONLDUtils.validateTypeValue;
  26. import java.util.ArrayList;
  27. import java.util.Collection;
  28. import java.util.Collections;
  29. import java.util.LinkedHashMap;
  30. import java.util.List;
  31. import java.util.Map;
  32. import java.util.Set;
  33. import org.slf4j.Logger;
  34. import org.slf4j.LoggerFactory;
  35. import com.github.jsonldjava.utils.JSONUtils;
  36. import com.github.jsonldjava.utils.Obj;
  37. import com.github.jsonldjava.utils.URL;
  38. public class JSONLDProcessor {
  39. private static final Logger LOG = LoggerFactory.getLogger(JSONLDProcessor.class);
  40. Options opts;
  41. public JSONLDProcessor() {
  42. opts = new Options("");
  43. }
  44. public JSONLDProcessor(Options opts) {
  45. if (opts == null) {
  46. opts = new Options("");
  47. } else {
  48. this.opts = opts;
  49. }
  50. }
  51. /**
  52. * Processes a local context and returns a new active context.
  53. *
  54. * @param activeCtx
  55. * the current active context.
  56. * @param localCtx
  57. * the local context to process.
  58. * @param options
  59. * the context processing options.
  60. *
  61. * @return the new active context.
  62. */
  63. ActiveContext processContext(ActiveContext activeCtx, Object localCtx)
  64. throws JSONLDProcessingError {
  65. // TODO: get context from cache if available
  66. // initialize the resulting context
  67. ActiveContext rval = activeCtx.clone();
  68. // normalize local context to an array of @context objects
  69. if (localCtx instanceof Map && ((Map) localCtx).containsKey("@context")
  70. && ((Map) localCtx).get("@context") instanceof List) {
  71. localCtx = ((Map) localCtx).get("@context");
  72. }
  73. List<Map<String, Object>> ctxs;
  74. if (localCtx instanceof List) {
  75. ctxs = (List<Map<String, Object>>) localCtx;
  76. } else {
  77. ctxs = new ArrayList<Map<String, Object>>();
  78. ctxs.add((Map<String, Object>) localCtx);
  79. }
  80. // process each context in order
  81. for (Object ctx : ctxs) {
  82. if (ctx == null) {
  83. // reset to initial context
  84. rval = new ActiveContext(opts);
  85. continue;
  86. }
  87. // context must be an object by now, all URLs resolved before this
  88. // call
  89. if (ctx instanceof Map) {
  90. // dereference @context key if present
  91. if (((Map<String, Object>) ctx).containsKey("@context")) {
  92. ctx = ((Map<String, Object>) ctx).get("@context");
  93. }
  94. } else {
  95. // context must be an object by now, all URLs resolved before
  96. // this call
  97. throw new JSONLDProcessingError("@context must be an object").setType(
  98. JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail("context", ctx);
  99. }
  100. // define context mappings for keys in local context
  101. final Map<String, Boolean> defined = new LinkedHashMap<String, Boolean>();
  102. // helper for access to ctx as a map
  103. final Map<String, Object> ctxm = (Map<String, Object>) ctx;
  104. // handle @base
  105. if (ctxm.containsKey("@base")) {
  106. Object base = ctxm.get("@base");
  107. // reset base
  108. if (base == null) {
  109. base = opts.base;
  110. } else if (!isString(base)) {
  111. throw new JSONLDProcessingError(
  112. "Invalid JSON-LD syntax; the value of \"@base\" in a "
  113. + "@context must be a string or null.").setType(
  114. JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail("context", ctx);
  115. } else if (!"".equals(base) && !isAbsoluteIri((String) base)) {
  116. throw new JSONLDProcessingError(
  117. "Invalid JSON-LD syntax; the value of \"@base\" in a "
  118. + "@context must be an absolute IRI or the empty string.")
  119. .setType(JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail("context",
  120. ctx);
  121. }
  122. base = URL.parse((String) base);
  123. rval.put("@base", base);
  124. defined.put("@base", true);
  125. }
  126. // handle @vocab
  127. if (ctxm.containsKey("@vocab")) {
  128. final Object value = ctxm.get("@vocab");
  129. if (value == null) {
  130. rval.remove("@vocab");
  131. } else if (!isString(value)) {
  132. throw new JSONLDProcessingError(
  133. "Invalid JSON-LD syntax; the value of \"@vocab\" in a "
  134. + "@context must be a string or null.").setType(
  135. JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail("context", ctx);
  136. } else if (!isAbsoluteIri((String) value)) {
  137. throw new JSONLDProcessingError(
  138. "Invalid JSON-LD syntax; the value of \"@vocab\" in a "
  139. + "@context must be an absolute IRI.").setType(
  140. JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail("context", ctx);
  141. } else {
  142. rval.put("@vocab", value);
  143. }
  144. defined.put("@vocab", true);
  145. }
  146. // handle @language
  147. if (ctxm.containsKey("@language")) {
  148. final Object value = ctxm.get("@language");
  149. if (value == null) {
  150. rval.remove("@language");
  151. } else if (!isString(value)) {
  152. throw new JSONLDProcessingError(
  153. "Invalid JSON-LD syntax; the value of \"@language\" in a "
  154. + "@context must be a string or null.").setType(
  155. JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail("context", ctx);
  156. } else {
  157. rval.put("@language", ((String) value).toLowerCase());
  158. }
  159. defined.put("@language", true);
  160. }
  161. // process all other keys
  162. for (final String key : ctxm.keySet()) {
  163. createTermDefinition(rval, ctxm, key, defined);
  164. }
  165. }
  166. // TODO: cache results
  167. return rval;
  168. }
  169. /**
  170. * Recursively expands an element using the given context. Any context in
  171. * the element will be removed. All context URLs must have been retrieved
  172. * before calling this method.
  173. *
  174. * @param activeCtx
  175. * the context to use.
  176. * @param activeProperty
  177. * the property for the element, null for none.
  178. * @param element
  179. * the element to expand.
  180. * @param options
  181. * the expansion options.
  182. * @param insideList
  183. * true if the element is a list, false if not.
  184. *
  185. * @return the expanded value.
  186. *
  187. * TODO: - does this function always return a map, or can it also
  188. * return a list, the expandedValue variable below seems to assume a
  189. * map, but in javascript, `in` will just return false if the result
  190. * is a list
  191. */
  192. public Object expand(ActiveContext activeCtx, String activeProperty, Object element,
  193. Boolean insideList) throws JSONLDProcessingError {
  194. // nothing to expand
  195. if (element == null) {
  196. return null;
  197. }
  198. // recursively expand array
  199. if (element instanceof List) {
  200. final List<Object> rval = new ArrayList<Object>();
  201. for (final Object i : (List<Object>) element) {
  202. // expand element
  203. final Object e = expand(activeCtx, activeProperty, i, insideList);
  204. if (insideList && (isArray(e) || isList(e))) {
  205. // lists of lists are illegal
  206. throw new JSONLDProcessingError(
  207. "Invalid JSON-LD syntax; lists of lists are not permitted.")
  208. .setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
  209. // drop null values
  210. } else if (e != null) {
  211. if (isArray(e)) {
  212. rval.addAll((Collection<? extends Object>) e);
  213. } else {
  214. rval.add(e);
  215. }
  216. }
  217. }
  218. return rval;
  219. }
  220. // recursively expand object
  221. if (isObject(element)) {
  222. // access helper
  223. final Map<String, Object> elem = (Map<String, Object>) element;
  224. // if element has a context, process it
  225. if (elem.containsKey("@context")) {
  226. activeCtx = processContext(activeCtx, elem.get("@context"));
  227. // elem.remove("@context");
  228. }
  229. // expand the active property
  230. final String expandedActiveProperty = expandIri(activeCtx, activeProperty, false, true,
  231. null, null); // {vocab: true}
  232. Object rval = new LinkedHashMap<String, Object>();
  233. Map<String, Object> mval = (Map<String, Object>) rval; // to make
  234. // things
  235. // easier
  236. // while we
  237. // know rval
  238. // is a map
  239. final List<String> keys = new ArrayList<String>(elem.keySet());
  240. Collections.sort(keys);
  241. for (final String key : keys) {
  242. final Object value = elem.get(key);
  243. Object expandedValue;
  244. // skip @context
  245. if (key.equals("@context")) {
  246. continue;
  247. }
  248. // expand key to IRI
  249. final String expandedProperty = expandIri(activeCtx, key, false, true, null, null); // {vocab:
  250. // true}
  251. // drop non-absolute IRI keys that aren't keywords
  252. if (expandedProperty == null
  253. || !(isAbsoluteIri(expandedProperty) || isKeyword(expandedProperty))) {
  254. continue;
  255. }
  256. if (isKeyword(expandedProperty) && "@reverse".equals(expandedActiveProperty)) {
  257. throw new JSONLDProcessingError(
  258. "Invalid JSON-LD syntax; a keyword cannot be used as a @reverse propery.")
  259. .setType(JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail("value",
  260. value);
  261. }
  262. if ("@id".equals(expandedProperty) && !isString(value)) {
  263. throw new JSONLDProcessingError(
  264. "Invalid JSON-LD syntax; \"@id\" value must a string.").setType(
  265. JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail("value", value);
  266. }
  267. // validate @type value
  268. if ("@type".equals(expandedProperty)) {
  269. validateTypeValue(value);
  270. }
  271. // @graph must be an array or an object
  272. if ("@graph".equals(expandedProperty) && !(isObject(value) || isArray(value))) {
  273. throw new JSONLDProcessingError(
  274. "Invalid JSON-LD syntax; \"@graph\" value must be an object or an array.")
  275. .setType(JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail("value",
  276. value);
  277. }
  278. // @value must not be an object or an array
  279. if ("@value".equals(expandedProperty)
  280. && (value instanceof Map || value instanceof List)) {
  281. throw new JSONLDProcessingError(
  282. "Invalid JSON-LD syntax; \"@value\" value must not be an object or an array.")
  283. .setType(JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail("value",
  284. value);
  285. }
  286. // @language must be a string
  287. if ("@language".equals(expandedProperty) && !(value instanceof String)) {
  288. throw new JSONLDProcessingError(
  289. "Invalid JSON-LD syntax; \"@language\" value must be a string.")
  290. .setType(JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail("value",
  291. value);
  292. }
  293. // @index must be a string
  294. if ("@index".equals(expandedProperty) && !(value instanceof String)) {
  295. throw new JSONLDProcessingError(
  296. "Invalid JSON-LD syntax; \"@index\" value must be a string.").setType(
  297. JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail("value", value);
  298. }
  299. // @reverse must be an object
  300. if ("@reverse".equals(expandedProperty)) {
  301. if (!isObject(value)) {
  302. throw new JSONLDProcessingError(
  303. "Invalid JSON-LD syntax; \"@reverse\" value must be an object.")
  304. .setType(JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail(
  305. "value", value);
  306. }
  307. expandedValue = expand(activeCtx, "@reverse", value, insideList);
  308. // properties double-reversed
  309. if (expandedValue instanceof Map
  310. && ((Map<String, Object>) expandedValue).containsKey("@reverse")) {
  311. // TODO: javascript seems to assume that the value of
  312. // reverse will always be an object, may need to add a
  313. // check here if this turns out to be the case
  314. final Map<String, Object> rev = (Map<String, Object>) ((Map<String, Object>) expandedValue)
  315. .get("@reverse");
  316. for (final String property : rev.keySet()) {
  317. addValue(mval, property, rev.get(property), true);
  318. }
  319. }
  320. // FIXME: can this be merged with the code below to
  321. // simplify?
  322. // merge in all reversed properties
  323. if (expandedValue instanceof Map) { // TODO: javascript
  324. // doesn't make this
  325. // check, can we assume
  326. // expandedValue is
  327. // always going to be an
  328. // object?
  329. Map<String, Object> reverseMap = (Map<String, Object>) mval.get("@reverse");
  330. for (final String property : ((Map<String, Object>) expandedValue).keySet()) {
  331. if ("@reverse".equals(property)) {
  332. continue;
  333. }
  334. if (reverseMap == null) {
  335. reverseMap = new LinkedHashMap<String, Object>();
  336. mval.put("@reverse", reverseMap);
  337. }
  338. addValue(reverseMap, property, new ArrayList<Object>(), true);
  339. final List<Object> items = (List<Object>) ((Map<String, Object>) expandedValue)
  340. .get(property);
  341. for (final Object item : items) {
  342. if (isValue(item) || isList(item)) {
  343. throw new JSONLDProcessingError(
  344. "Invalid JSON-LD syntax; \"@reverse\" value must not be a @value or an @list.")
  345. .setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
  346. .setDetail("value", expandedValue);
  347. }
  348. addValue(reverseMap, property, item, true);
  349. }
  350. }
  351. }
  352. continue;
  353. }
  354. final String container = (String) activeCtx.getContextValue(key, "@container");
  355. // handle language map container (skip if value is not an
  356. // object)
  357. if ("@language".equals(container) && isObject(value)) {
  358. expandedValue = expandLanguageMap((Map<String, Object>) value);
  359. }
  360. // handle index container (skip if value is not an object)
  361. else if ("@index".equals(container) && isObject(value)) {
  362. // NOTE: implementing embeded function expandIndexMap from
  363. // javascript as rolled out code here
  364. // as it doesn't call itself and needs access to this
  365. // instance's expand method.
  366. // using eim_ prefix for variables to avoid clashes
  367. final String eim_activeProperty = key;
  368. final List<Object> eim_rval = new ArrayList<Object>();
  369. for (final String eim_key : ((Map<String, Object>) value).keySet()) {
  370. List<Object> eim_val;
  371. if (!isArray(((Map<String, Object>) value).get(eim_key))) {
  372. eim_val = new ArrayList<Object>();
  373. eim_val.add(((Map<String, Object>) value).get(eim_key));
  374. } else {
  375. eim_val = (List<Object>) ((Map<String, Object>) value).get(eim_key);
  376. }
  377. // NOTE: javascript assumes list result here, so I am as
  378. // well
  379. eim_val = (List<Object>) expand(activeCtx, eim_activeProperty, eim_val,
  380. false);
  381. for (final Object eim_item : eim_val) {
  382. if (isObject(eim_item)) {
  383. if (!((Map<String, Object>) eim_item).containsKey("@index")) {
  384. ((Map<String, Object>) eim_item).put("@index", eim_key);
  385. }
  386. eim_rval.add(eim_item);
  387. }
  388. }
  389. }
  390. expandedValue = eim_rval;
  391. } else {
  392. // recurse into @list or @set
  393. final Boolean isList = "@list".equals(expandedProperty);
  394. if (isList || "@set".equals(expandedProperty)) {
  395. String nextActiveProperty = activeProperty;
  396. if (isList && "@graph".equals(expandedActiveProperty)) {
  397. nextActiveProperty = null;
  398. }
  399. expandedValue = expand(activeCtx, nextActiveProperty, value, isList);
  400. if (isList && isList(expandedValue)) {
  401. throw new JSONLDProcessingError(
  402. "Invalid JSON-LD syntax; lists of lists are not permitted.")
  403. .setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
  404. }
  405. } else {
  406. // recursively expand value with key as new active
  407. // property
  408. expandedValue = expand(activeCtx, key, value, false);
  409. }
  410. }
  411. // drop null values if property is not @value
  412. if (expandedValue == null && !"@value".equals(expandedProperty)) {
  413. continue;
  414. }
  415. // convert expanded value to @list if container specified it
  416. if (!"@list".equals(expandedProperty) && !isList(expandedValue)
  417. && "@list".equals(container)) {
  418. // ensure expanded value is an array
  419. final Map<String, Object> tm = new LinkedHashMap<String, Object>();
  420. List<Object> tl;
  421. if (isArray(expandedValue)) {
  422. tl = (List<Object>) expandedValue;
  423. } else {
  424. tl = new ArrayList<Object>();
  425. tl.add(expandedValue);
  426. }
  427. tm.put("@list", tl);
  428. expandedValue = tm;
  429. }
  430. // FIXME: can this be merged with the code above to simplify?
  431. // merge in all reversed properties
  432. if (Boolean.TRUE.equals(Obj.get(activeCtx.mappings, key, "reverse"))) {
  433. final Map<String, Object> reverseMap = new LinkedHashMap<String, Object>();
  434. mval.put("@reverse", reverseMap);
  435. if (!isArray(expandedValue)) {
  436. final List<Object> tmp = new ArrayList<Object>();
  437. tmp.add(expandedValue);
  438. expandedValue = tmp;
  439. }
  440. for (final Object item : (List<Object>) expandedValue) {
  441. if (isValue(item) || isList(item)) {
  442. throw new JSONLDProcessingError(
  443. "Invalid JSON-LD syntax; \"@reverse\" value must not be a @value or an @list.")
  444. .setType(JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail(
  445. "value", expandedValue);
  446. }
  447. addValue(reverseMap, expandedProperty, item, true);
  448. }
  449. continue;
  450. }
  451. // add value for property
  452. // use an array except for certain keywords
  453. final Boolean useArray = !("@index".equals(expandedProperty)
  454. || "@id".equals(expandedProperty) || "@type".equals(expandedProperty)
  455. || "@value".equals(expandedProperty) || "@language"
  456. .equals(expandedProperty));
  457. addValue(mval, expandedProperty, expandedValue, useArray);
  458. }
  459. // get property count on expanded output
  460. int count = mval.size();
  461. // @value must only have @language or @type
  462. if (mval.containsKey("@value")) {
  463. // @value must only have @language or @type
  464. if (mval.containsKey("@type") && mval.containsKey("@language")) {
  465. throw new JSONLDProcessingError(
  466. "Invalid JSON-LD syntax; an element containing \"@value\" may not contain both \"@type\" and \"@language\".")
  467. .setType(JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail("element",
  468. mval);
  469. }
  470. int validCount = count - 1;
  471. if (mval.containsKey("@type") || mval.containsKey("@language")) {
  472. validCount -= 1;
  473. }
  474. if (mval.containsKey("@index")) {
  475. validCount -= 1;
  476. }
  477. if (validCount != 0) {
  478. throw new JSONLDProcessingError(
  479. "Invalid JSON-LD syntax; an element containing \"@value\" may only have an \"@index\" property "
  480. + "and at most one other property which can be \"@type\" or \"@language\".")
  481. .setType(JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail("element",
  482. mval);
  483. }
  484. // drop null @values
  485. if (mval.get("@value") == null) {
  486. rval = null;
  487. mval = null;
  488. }
  489. // drop @language if @value isn't a string
  490. else if (mval.containsKey("@language") && !isString(mval.get("@value"))) {
  491. mval.remove("@language");
  492. }
  493. }
  494. // convert @type to an array
  495. else if (mval.containsKey("@type") && !isArray(mval.get("@type"))) {
  496. final List<Object> tmp = new ArrayList<Object>();
  497. tmp.add(mval.get("@type"));
  498. mval.put("@type", tmp);
  499. }
  500. // handle @set and @list
  501. else if (mval.containsKey("@set") || mval.containsKey("@list")) {
  502. if (count > 1 && (count != 2 && mval.containsKey("@index"))) {
  503. throw new JSONLDProcessingError(
  504. "Invalid JSON-LD syntax; if an element has the property \"@set\" or \"@list\", then it can have "
  505. + "at most one other property that is \"@index\".").setType(
  506. JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail("element", mval);
  507. }
  508. // optimize away @set
  509. if (mval.containsKey("@set")) {
  510. rval = mval.get("@set");
  511. mval = null; // result is no longer a map, so don't allow
  512. // this to be used anymore
  513. count = ((Collection) rval).size(); // TODO: i'm sure the
  514. // result here should be
  515. // a List, but
  516. // Collection works, so
  517. // it'll do for now
  518. }
  519. }
  520. // drop objects with only @language
  521. else if (mval.containsKey("@language") && count == 1) {
  522. rval = null;
  523. mval = null;
  524. }
  525. // drop certain top-level object that do not occur in lists
  526. if (isObject(rval) && !opts.keepFreeFloatingNodes && !insideList
  527. && (activeProperty == null || "@graph".equals(expandedActiveProperty))) {
  528. // drop empty object or top-level @value
  529. if (count == 0 || mval.containsKey("@value")) {
  530. rval = null;
  531. mval = null;
  532. } else {
  533. // drop nodes that generate no triples
  534. boolean hasTriples = false;
  535. for (final String key : mval.keySet()) {
  536. if (hasTriples) {
  537. break;
  538. }
  539. if (!isKeyword(key) || "@graph".equals(key) || "@type".equals(key)) {
  540. hasTriples = true;
  541. }
  542. }
  543. if (!hasTriples) {
  544. rval = null;
  545. mval = null;
  546. }
  547. }
  548. }
  549. return rval;
  550. }
  551. // drop top-level scalars that are not in lists
  552. if (!insideList
  553. && (activeProperty == null || "@graph".equals(expandIri(activeCtx, activeProperty,
  554. false, true, null, null)))) {
  555. return null;
  556. }
  557. // expand element according to value expansion rules
  558. return expandValue(activeCtx, activeProperty, element);
  559. }
  560. /**
  561. * Recursively compacts an element using the given active context. All
  562. * values must be in expanded form before this method is called.
  563. *
  564. * @param activeCtx
  565. * the active context to use.
  566. * @param activeProperty
  567. * the compacted property associated with the element to compact,
  568. * null for none.
  569. * @param element
  570. * the element to compact.
  571. * @param options
  572. * the compaction options.
  573. *
  574. * @return the compacted value.
  575. */
  576. public Object compact(ActiveContext activeCtx, String activeProperty, Object element)
  577. throws JSONLDProcessingError {
  578. // recursively compact array
  579. if (isArray(element)) {
  580. final List<Object> rval = new ArrayList<Object>();
  581. for (final Object i : (List<Object>) element) {
  582. // compact, dropping any null values
  583. final Object compacted = compact(activeCtx, activeProperty, i);
  584. if (compacted != null) {
  585. rval.add(compacted);
  586. }
  587. }
  588. if (opts.compactArrays && rval.size() == 1) {
  589. // use single element if no container is specified
  590. final Object container = activeCtx.getContextValue(activeProperty, "@container");
  591. if (container == null) {
  592. return rval.get(0);
  593. }
  594. }
  595. return rval;
  596. }
  597. // recursively compact object
  598. if (isObject(element)) {
  599. // access helper
  600. final Map<String, Object> elem = (Map<String, Object>) element;
  601. // do value compaction on @value and subject references
  602. if (isValue(element) || isSubjectReference(element)) {
  603. return compactValue(activeCtx, activeProperty, element);
  604. }
  605. // FIXME: avoid misuse of active property as an expanded property?
  606. final boolean insideReverse = ("@reverse".equals(activeProperty));
  607. // process element keys in order
  608. final List<String> keys = new ArrayList<String>(elem.keySet());
  609. Collections.sort(keys);
  610. final Map<String, Object> rval = new LinkedHashMap<String, Object>();
  611. for (final String expandedProperty : keys) {
  612. final Object expandedValue = elem.get(expandedProperty);
  613. /*
  614. * TODO: // handle ignored keys if (opts.isIgnored(key)) {
  615. * //JSONLDUtils.addValue(rval, key, value, false);
  616. * rval.put(key, value); continue; }
  617. */
  618. // compact @id and @type(s)
  619. if ("@id".equals(expandedProperty) || "@type".equals(expandedProperty)) {
  620. Object compactedValue;
  621. // compact single @id
  622. if (isString(expandedValue)) {
  623. compactedValue = compactIri(activeCtx, (String) expandedValue, null,
  624. "@type".equals(expandedProperty), false);
  625. }
  626. // expanded value must be a @type array
  627. else {
  628. final List<String> types = new ArrayList<String>();
  629. for (final String i : (List<String>) expandedValue) {
  630. types.add(compactIri(activeCtx, i, null, true, false));
  631. }
  632. compactedValue = types;
  633. }
  634. // use keyword alias and add value
  635. final String alias = compactIri(activeCtx, expandedProperty);
  636. addValue(rval, alias, compactedValue, isArray(compactedValue)
  637. && ((List<Object>) expandedValue).size() == 0);
  638. continue;
  639. }
  640. // handle @reverse
  641. if ("@reverse".equals(expandedProperty)) {
  642. // recursively compact expanded value
  643. // TODO: i'm assuming this will always be a map due to the
  644. // rest of the code
  645. final Map<String, Object> compactedValue = (Map<String, Object>) compact(
  646. activeCtx, "@reverse", expandedValue);
  647. // handle double-reversed properties
  648. for (final String compactedProperty : compactedValue.keySet()) {
  649. if (Boolean.TRUE.equals(Obj.get(activeCtx.mappings, compactedProperty,
  650. "reverse"))) {
  651. if (!rval.containsKey(compactedProperty) && !opts.compactArrays) {
  652. rval.put(compactedProperty, new ArrayList<Object>());
  653. }
  654. addValue(rval, compactedProperty, compactedValue.get(compactedProperty));
  655. compactedValue.remove(compactedProperty);
  656. }
  657. }
  658. if (compactedValue.size() > 0) {
  659. // use keyword alias and add value
  660. addValue(rval, compactIri(activeCtx, expandedProperty), compactedValue);
  661. }
  662. continue;
  663. }
  664. // handle @index property
  665. if ("@index".equals(expandedProperty)) {
  666. // drop @index if inside an @index container
  667. final String container = (String) activeCtx.getContextValue(activeProperty,
  668. "@container");
  669. if ("@index".equals(container)) {
  670. continue;
  671. }
  672. // use keyword alias and add value
  673. addValue(rval, compactIri(activeCtx, expandedProperty), expandedValue);
  674. continue;
  675. }
  676. // NOTE: expanded value must be an array due to expansion
  677. // algorithm.
  678. // preserve empty arrays
  679. if (((List<Object>) expandedValue).size() == 0) {
  680. addValue(
  681. rval,
  682. compactIri(activeCtx, expandedProperty, expandedValue, true,
  683. insideReverse), expandedValue, true);
  684. }
  685. // recusively process array values
  686. for (final Object expandedItem : (List<Object>) expandedValue) {
  687. // compact property and get container type
  688. final String itemActiveProperty = compactIri(activeCtx, expandedProperty,
  689. expandedItem, true, insideReverse);
  690. final String container = (String) activeCtx.getContextValue(itemActiveProperty,
  691. "@container");
  692. // get @list value if appropriate
  693. final boolean isList = isList(expandedItem);
  694. Object list = null;
  695. if (isList) {
  696. list = ((Map<String, Object>) expandedItem).get("@list");
  697. }
  698. // recursively compact expanded item
  699. Object compactedItem = compact(activeCtx, itemActiveProperty, isList ? list
  700. : expandedItem);
  701. // handle @list
  702. if (isList) {
  703. // ensure @list value is an array
  704. if (!isArray(compactedItem)) {
  705. final List<Object> tmp = new ArrayList<Object>();
  706. tmp.add(compactedItem);
  707. compactedItem = tmp;
  708. }
  709. if (!"@list".equals(container)) {
  710. // wrap using @list alias
  711. final Map<String, Object> wrapper = new LinkedHashMap<String, Object>();
  712. wrapper.put(compactIri(activeCtx, "@list"), compactedItem);
  713. compactedItem = wrapper;
  714. // include @index from expanded @list, if any
  715. if (((Map<String, Object>) expandedItem).containsKey("@index")) {
  716. ((Map<String, Object>) compactedItem).put(
  717. compactIri(activeCtx, "@index"),
  718. ((Map<String, Object>) expandedItem).get("@index"));
  719. }
  720. }
  721. // can't use @list container for more than 1 list
  722. else if (rval.containsKey(itemActiveProperty)) {
  723. throw new JSONLDProcessingError(
  724. "Invalid JSON-LD compact error; property has a \"@list\" @container "
  725. + "rule but there is more than a single @list that matches "
  726. + "the compacted term in the document. Compaction might mix "
  727. + "unwanted items into the list.")
  728. .setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
  729. }
  730. }
  731. // handle language and index maps
  732. if ("@language".equals(container) || "@index".equals(container)) {
  733. // get or create the map object
  734. Map<String, Object> mapObject;
  735. if (rval.containsKey(itemActiveProperty)) {
  736. mapObject = (Map<String, Object>) rval.get(itemActiveProperty);
  737. } else {
  738. mapObject = new LinkedHashMap<String, Object>();
  739. rval.put(itemActiveProperty, mapObject);
  740. }
  741. // if container is a language map, simplify compacted
  742. // value to
  743. // a simple string
  744. if ("@language".equals(container) && isValue(compactedItem)) {
  745. compactedItem = ((Map<String, Object>) compactedItem).get("@value");
  746. }
  747. // add compact value to map object using key from
  748. // expanded value
  749. // based on the container type
  750. addValue(mapObject,
  751. (String) ((Map<String, Object>) expandedItem).get(container),
  752. compactedItem);
  753. } else {
  754. // use an array if: compactArrays flag is false,
  755. // @container is @set or @list, value is an empty
  756. // array, or key is @graph
  757. final Boolean isArray = (!opts.compactArrays
  758. || "@set".equals(container)
  759. || "@list".equals(container)
  760. || (isArray(compactedItem) && ((List<Object>) compactedItem).size() == 0)
  761. || "@list".equals(expandedProperty) || "@graph"
  762. .equals(expandedProperty));
  763. // add compact value
  764. addValue(rval, itemActiveProperty, compactedItem, isArray);
  765. }
  766. }
  767. }
  768. return rval;
  769. }
  770. // only primatives remain which are already compact
  771. return element;
  772. }
  773. private class FramingContext {
  774. public Map<String, Object> embeds = null;
  775. public Map<String, Object> graphs = null;
  776. public Map<String, Object> subjects = null;
  777. public Options options = opts;
  778. }
  779. /**
  780. * Performs JSON-LD framing.
  781. *
  782. * @param input
  783. * the expanded JSON-LD to frame.
  784. * @param frame
  785. * the expanded JSON-LD frame to use.
  786. * @param options
  787. * the framing options.
  788. *
  789. * @return the framed output.
  790. * @throws JSONLDProcessingError
  791. */
  792. public Object frame(Object input, Object frame) throws JSONLDProcessingError {
  793. // create framing state
  794. final FramingContext state = new FramingContext();
  795. // Map<String,Object> state = new HashMap<String, Object>();
  796. // state.put("options", this.opts);
  797. state.graphs = new LinkedHashMap<String, Object>();
  798. state.graphs.put("@default", new LinkedHashMap<String, Object>());
  799. state.graphs.put("@merged", new LinkedHashMap<String, Object>());
  800. // produce a map of all graphs and name each bnode
  801. // FIXME: currently uses subjects from @merged graph only
  802. final UniqueNamer namer = new UniqueNamer("_:b");
  803. createNodeMap(input, state.graphs, "@merged", namer);
  804. state.subjects = (Map<String, Object>) state.graphs.get("@merged");
  805. // frame the subjects
  806. final List<Object> framed = new ArrayList<Object>();
  807. final List<String> sortedKeys = new ArrayList<String>(state.subjects.keySet());
  808. Collections.sort(sortedKeys);
  809. frame(state, sortedKeys, frame, framed, null);
  810. return framed;
  811. }
  812. /**
  813. * Frames subjects according to the given frame.
  814. *
  815. * @param state
  816. * the current framing state.
  817. * @param subjects
  818. * the subjects to filter.
  819. * @param frame
  820. * the frame.
  821. * @param parent
  822. * the parent subject or top-level array.
  823. * @param property
  824. * the parent property, initialized to null.
  825. * @throws JSONLDProcessingError
  826. */
  827. private void frame(FramingContext state, Collection<String> subjects, Object frame,
  828. Object parent, String property) throws JSONLDProcessingError {
  829. // validate the frame
  830. validateFrame(state, frame);
  831. // NOTE: once validated we move to the function where the frame is
  832. // specifically a map
  833. frame(state, subjects, (Map<String, Object>) ((List<Object>) frame).get(0), parent,
  834. property);
  835. }
  836. private void frame(FramingContext state, Collection<String> subjects,
  837. Map<String, Object> frame, Object parent, String property) throws JSONLDProcessingError {
  838. // filter out subjects that match the frame
  839. final Map<String, Object> matches = filterSubjects(state, subjects, frame);
  840. // get flags for current frame
  841. final Options options = state.options;
  842. Boolean embedOn = (frame.containsKey("@embed")) ? (Boolean) ((List) frame.get("@embed"))
  843. .get(0) : options.embed;
  844. final Boolean explicicOn = (frame.containsKey("@explicit")) ? (Boolean) ((List) frame
  845. .get("@explicit")).get(0) : options.explicit;
  846. // add matches to output
  847. final List<String> ids = new ArrayList<String>(matches.keySet());
  848. Collections.sort(ids);
  849. for (final String id : ids) {
  850. // Note: In order to treat each top-level match as a
  851. // compartmentalized
  852. // result, create an independent copy of the embedded subjects map
  853. // when the
  854. // property is null, which only occurs at the top-level.
  855. if (property == null) {
  856. state.embeds = new LinkedHashMap<String, Object>();
  857. }
  858. // start output
  859. final Map<String, Object> output = new LinkedHashMap<String, Object>();
  860. output.put("@id", id);
  861. // prepare embed meta info
  862. final Map<String, Object> embed = new LinkedHashMap<String, Object>();
  863. embed.put("parent", parent);
  864. embed.put("property", property);
  865. // if embed is on and there is an existing embed
  866. if (embedOn && state.embeds.containsKey(id)) {
  867. // only overwrite an existing embed if it has already been added
  868. // to its
  869. // parent -- otherwise its parent is somewhere up the tree from
  870. // this
  871. // embed and the embed would occur twice once the tree is added
  872. embedOn = false;
  873. // existing embed's parent is an array
  874. final Map<String, Object> existing = (Map<String, Object>) state.embeds.get(id);
  875. if (isArray(existing.get("parent"))) {
  876. for (final Object o : (List<Object>) existing.get("parent")) {
  877. if (compareValues(output, o)) {
  878. embedOn = true;
  879. break;
  880. }
  881. }
  882. }
  883. // existing embed's parent is an object
  884. else if (hasValue((Map<String, Object>) existing.get("parent"),
  885. (String) existing.get("property"), output)) {
  886. embedOn = true;
  887. }
  888. // existing embed has already been added, so allow an overwrite
  889. if (embedOn) {
  890. removeEmbed(state, id);
  891. }
  892. }
  893. // not embedding, add output without any other properties
  894. if (!embedOn) {
  895. addFrameOutput(state, parent, property, output);
  896. } else {
  897. // add embed meta info
  898. state.embeds.put(id, embed);
  899. // iterate over subject properties
  900. final Map<String, Object> subject = (Map<String, Object>) matches.get(id);
  901. List<String> props = new ArrayList<String>(subject.keySet());
  902. Collections.sort(props);
  903. for (final String prop : props) {
  904. // handle ignored keys
  905. if (opts.isIgnored(prop)) {
  906. output.put(prop, JSONLDUtils.clone(subject.get(prop)));
  907. continue;
  908. }
  909. // copy keywords to output
  910. if (isKeyword(prop)) {
  911. output.put(prop, JSONLDUtils.clone(subject.get(prop)));
  912. continue;
  913. }
  914. // if property isn't in the frame
  915. if (!frame.containsKey(prop)) {
  916. // if explicit is off, embed values
  917. if (!explicicOn) {
  918. embedValues(state, subject, prop, output);
  919. }
  920. continue;
  921. }
  922. // add objects
  923. final Object objects = subject.get(prop);
  924. // TODO: i've done some crazy stuff here because i'm unsure
  925. // if objects is always a list or if it can
  926. // be a map as well. I think it's always a map, but i'll get
  927. // it working like this first
  928. for (final Object i : objects instanceof List ? (List) objects
  929. : ((Map) objects).keySet()) {
  930. final Object o = objects instanceof List ? i : ((Map) objects).get(i);
  931. // recurse into list
  932. if (isList(o)) {
  933. // add empty list
  934. final Map<String, Object> list = new LinkedHashMap<String, Object>();
  935. list.put("@list", new ArrayList<Object>());
  936. addFrameOutput(state, output, prop, list);
  937. // add list objects
  938. final List src = (List) ((Map) o).get("@list");
  939. for (final Object n : src) {
  940. // recurse into subject reference
  941. if (isSubjectReference(o)) {
  942. final List tmp = new ArrayList();
  943. tmp.add(((Map) n).get("@id"));
  944. frame(state, tmp, frame.get(prop), list, "@list");
  945. } else {
  946. // include other values automatcially
  947. addFrameOutput(state, list, "@list", JSONLDUtils.clone(n));
  948. }
  949. }
  950. continue;
  951. }
  952. // recurse into subject reference
  953. if (isSubjectReference(o)) {
  954. final List tmp = new ArrayList();
  955. tmp.add(((Map) o).get("@id"));
  956. frame(state, tmp, frame.get(prop), output, prop);
  957. } else {
  958. // include other values automatically
  959. addFrameOutput(state, output, prop, JSONLDUtils.clone(o));
  960. }
  961. }
  962. }
  963. // handle defaults
  964. props = new ArrayList<String>(frame.keySet());
  965. Collections.sort(props);
  966. for (final String prop : props) {
  967. // skip keywords
  968. if (isKeyword(prop)) {
  969. continue;
  970. }
  971. // if omit default is off, then include default values for
  972. // properties
  973. // that appear in the next frame but are not in the matching
  974. // subject
  975. final Map<String, Object> next = (Map<String, Object>) ((List<Object>) frame
  976. .get(prop)).get(0);
  977. final boolean omitDefaultOn = (next.containsKey("@omitDefault")) ? (Boolean) ((List) next
  978. .get("@omitDefault")).get(0) : options.omitDefault;
  979. if (!omitDefaultOn && !output.containsKey(prop)) {
  980. Object preserve = "@null";
  981. if (next.containsKey("@default")) {
  982. preserve = JSONLDUtils.clone(next.get("@default"));
  983. }
  984. if (!isArray(preserve)) {
  985. final List<Object> tmp = new ArrayList<Object>();
  986. tmp.add(preserve);
  987. preserve = tmp;
  988. }
  989. final Map<String, Object> tmp1 = new LinkedHashMap<String, Object>();
  990. tmp1.put("@preserve", preserve);
  991. final List<Object> tmp2 = new ArrayList<Object>();
  992. tmp2.add(tmp1);
  993. output.put(prop, tmp2);
  994. }
  995. }
  996. // add output to parent
  997. addFrameOutput(state, parent, property, output);
  998. }
  999. }
  1000. }
  1001. /**
  1002. * Embeds values for the given subject and property into the given output
  1003. * during the framing algorithm.
  1004. *
  1005. * @param state
  1006. * the current framing state.
  1007. * @param subject
  1008. * the subject.
  1009. * @param property
  1010. * the property.
  1011. * @param output
  1012. * the output.
  1013. */
  1014. private void embedValues(FramingContext state, Map<String, Object> subject, String property,
  1015. Object output) {
  1016. // embed subject properties in output
  1017. final Object objects = subject.get(property);
  1018. // TODO: more craziness due to lack of knowledge about whether objects
  1019. // should
  1020. // be an array or an object
  1021. for (final Object i : objects instanceof List ? (List) objects : ((Map) objects).keySet()) {
  1022. Object o = objects instanceof List ? i : ((Map) objects).get(i);
  1023. // recurse into @list
  1024. if (isList(o)) {
  1025. final Map<String, Object> list = new LinkedHashMap<String, Object>();
  1026. list.put("@list", new ArrayList());
  1027. addFrameOutput(state, output, property, list);
  1028. embedValues(state, (Map<String, Object>) o, "@list", list.get("@list"));
  1029. return;
  1030. }
  1031. // handle subject reference
  1032. if (isSubjectReference(o)) {
  1033. final String id = (String) ((Map<String, Object>) o).get("@id");
  1034. // embed full subject if isn't already embedded
  1035. if (!state.embeds.containsKey(id)) {
  1036. // add embed
  1037. final Map<String, Object> embed = new LinkedHashMap<String, Object>();
  1038. embed.put("parent", output);
  1039. embed.put("property", property);
  1040. state.embeds.put(id, embed);
  1041. // recurse into subject
  1042. o = new LinkedHashMap<String, Object>();
  1043. final Map<String, Object> s = (Map<String, Object>) state.subjects.get(id);
  1044. for (final String prop : s.keySet()) {
  1045. // copy keywords
  1046. if (isKeyword(prop) || opts.isIgnored(prop)) {
  1047. ((Map<String, Object>) o).put(prop, JSONLDUtils.clone(s.get(prop)));
  1048. continue;
  1049. }
  1050. embedValues(state, s, prop, o);
  1051. }
  1052. }
  1053. addFrameOutput(state, output, property, o);
  1054. }
  1055. // copy non-subject value
  1056. else {
  1057. addFrameOutput(state, output, property, JSONLDUtils.clone(o));
  1058. }
  1059. }
  1060. }
  1061. /**
  1062. * Adds framing output to the given parent.
  1063. *
  1064. * @param state
  1065. * the current framing state.
  1066. * @param parent
  1067. * the parent to add to.
  1068. * @param property
  1069. * the parent property.
  1070. * @param output
  1071. * the output to add.
  1072. */
  1073. private static void addFrameOutput(FramingContext state, Object parent, String property,
  1074. Object output) {
  1075. if (isObject(parent)) {
  1076. addValue((Map<String, Object>) parent, property, output, true);
  1077. } else {
  1078. ((List) parent).add(output);
  1079. }
  1080. }
  1081. /**
  1082. * Removes an existing embed.
  1083. *
  1084. * @param state
  1085. * the current framing state.
  1086. * @param id
  1087. * the @id of the embed to remove.
  1088. */
  1089. private static void removeEmbed(FramingContext state, String id) {
  1090. // get existing embed
  1091. final Map<String, Object> embeds = state.embeds;
  1092. final Map<String, Object> embed = (Map<String, Object>) embeds.get(id);
  1093. final Object parent = embed.get("parent");
  1094. final String property = (String) embed.get("property");
  1095. // create reference to replace embed
  1096. final Map<String, Object> subject = new LinkedHashMap<String, Object>();
  1097. subject.put("@id", id);
  1098. // remove existing embed
  1099. if (isArray(parent)) {
  1100. // replace subject with reference
  1101. for (int i = 0; i < ((List) parent).size(); i++) {
  1102. if (compareValues(((List) parent).get(i), subject)) {
  1103. ((List) parent).set(i, subject);
  1104. break;
  1105. }
  1106. }
  1107. } else {
  1108. // replace subject with reference
  1109. removeValue(((Map<String, Object>) parent), property, subject,
  1110. ((Map<String, Object>) parent).get(property) instanceof List);
  1111. addValue(((Map<String, Object>) parent), property, subject,
  1112. ((Map<String, Object>) parent).get(property) instanceof List);
  1113. }
  1114. // recursively remove dependent dangling embeds
  1115. removeDependents(embeds, id);
  1116. }
  1117. private static void removeDependents(Map<String, Object> embeds, String id) {
  1118. // get embed keys as a separate array to enable deleting keys in map
  1119. final Set<String> ids = embeds.keySet();
  1120. for (final String next : ids) {
  1121. if (embeds.containsKey(next)
  1122. && ((Map<String, Object>) embeds.get(next)).get("parent") instanceof Map
  1123. && id.equals(((Map<String, Object>) ((Map<String, Object>) embeds.get(next))
  1124. .get("parent")).get("@id"))) {
  1125. embeds.remove(next);
  1126. removeDependents(embeds, next);
  1127. }
  1128. }
  1129. }
  1130. /**
  1131. * Returns a map of all of the subjects that match a parsed frame.
  1132. *
  1133. * @param state
  1134. * the current framing state.
  1135. * @param subjects
  1136. * the set of subjects to filter.
  1137. * @param frame
  1138. * the parsed frame.
  1139. *
  1140. * @return all of the matched subjects.
  1141. */
  1142. private static Map<String, Object> filterSubjects(FramingContext state,
  1143. Collection<String> subjects, Map<String, Object> frame) {
  1144. // filter subjects in @id order
  1145. final Map<String, Object> rval = new LinkedHashMap<String, Object>();
  1146. for (final String id : subjects) {
  1147. final Map<String, Object> subject = (Map<String, Object>) state.subjects.get(id);
  1148. if (filterSubject(subject, frame)) {
  1149. rval.put(id, subject);
  1150. }
  1151. }
  1152. return rval;
  1153. }
  1154. /**
  1155. * Returns true if the given subject matches the given frame.
  1156. *
  1157. * @param subject
  1158. * the subject to check.
  1159. * @param frame
  1160. * the frame to check.
  1161. *
  1162. * @return true if the subject matches, false if not.
  1163. */
  1164. private static boolean filterSubject(Map<String, Object> subject, Map<String, Object> frame) {
  1165. // check @type (object value means 'any' type, fall through to
  1166. // ducktyping)
  1167. final Object t = frame.get("@type");
  1168. // TODO: it seems @type should always be a list
  1169. if (frame.containsKey("@type")
  1170. && !(t instanceof List && ((List) t).size() == 1 && ((List) t).get(0) instanceof Map)) {
  1171. for (final Object i : (List) t) {
  1172. if (hasValue(subject, "@type", i)) {
  1173. return true;
  1174. }
  1175. }
  1176. return false;
  1177. }
  1178. // check ducktype
  1179. for (final String key : frame.keySet()) {
  1180. if ("@id".equals(key) || !isKeyword(key) && !(subject.containsKey(key))) {
  1181. return false;
  1182. }
  1183. }
  1184. return true;
  1185. }
  1186. /**
  1187. * Validates a JSON-LD frame, throwing an exception if the frame is invalid.
  1188. *
  1189. * @param state
  1190. * the current frame state.
  1191. * @param frame
  1192. * the frame to validate.
  1193. * @throws JSONLDProcessingError
  1194. */
  1195. private static void validateFrame(FramingContext state, Object frame)
  1196. throws JSONLDProcessingError {
  1197. if (!(frame instanceof List) || ((List) frame).size() != 1
  1198. || !(((List) frame).get(0) instanceof Map)) {
  1199. throw new JSONLDProcessingError(
  1200. "Invalid JSON-LD syntax; a JSON-LD frame must be a single object.").setType(
  1201. JSONLDProcessingError.Error.SYNTAX_ERROR).setDetail("frame", frame);
  1202. }
  1203. }
  1204. /**
  1205. * Performs RDF normalization on the given JSON-LD input.
  1206. *
  1207. * @param input
  1208. * the expanded JSON-LD object to normalize.
  1209. * @param options
  1210. * the normalization options.
  1211. * @param callback
  1212. * (err, normalized) called once the operation completes.
  1213. * @throws JSONLDProcessingError
  1214. */
  1215. public Object normalize(Map<String, Object> dataset) throws JSONLDProcessingError {
  1216. // create quads and map bnodes to their associated quads
  1217. final List<Object> quads = new ArrayList<Object>();
  1218. final Map<String, Object> bnodes = new LinkedHashMap<String, Object>();
  1219. for (String graphName : dataset.keySet()) {
  1220. final List<Map<String, Object>> triples = (List<Map<String, Object>>) dataset
  1221. .get(graphName);
  1222. if ("@default".equals(graphName)) {
  1223. graphName = null;
  1224. }
  1225. for (final Map<String, Object> quad : triples) {
  1226. if (graphName != null) {
  1227. if (graphName.indexOf("_:") == 0) {
  1228. final Map<String, Object> tmp = new LinkedHashMap<String, Object>();
  1229. tmp.put("type", "blank node");
  1230. tmp.put("value", graphName);
  1231. quad.put("name", tmp);
  1232. } else {
  1233. final Map<String, Object> tmp = new LinkedHashMap<String, Object>();
  1234. tmp.put("type", "IRI");
  1235. tmp.put("value", graphName);
  1236. quad.put("name", tmp);
  1237. }
  1238. }
  1239. quads.add(quad);
  1240. final String[] attrs = new String[] { "subject", "object", "name" };
  1241. for (final String attr : attrs) {
  1242. if (quad.containsKey(attr)
  1243. && "blank node".equals(((Map<String, Object>) quad.get(attr))
  1244. .get("type"))) {
  1245. final String id = (String) ((Map<String, Object>) quad.get(attr))
  1246. .get("value");
  1247. if (!bnodes.containsKey(id)) {
  1248. bnodes.put(id, new LinkedHashMap<String, List<Object>>() {
  1249. {
  1250. put("quads", new ArrayList<Object>());
  1251. }
  1252. });
  1253. }
  1254. ((List<Object>) ((Map<String, Object>) bnodes.get(id)).get("quads"))
  1255. .add(quad);
  1256. }
  1257. }
  1258. }
  1259. }
  1260. // mapping complete, start canonical naming
  1261. final NormalizeUtils normalizeUtils = new NormalizeUtils(quads, bnodes, new UniqueNamer(
  1262. "_:c14n"), opts);
  1263. return normalizeUtils.hashBlankNodes(bnodes.keySet());
  1264. }
  1265. /**
  1266. * Adds RDF triples for each graph in the given node map to an RDF dataset.
  1267. *
  1268. * @param nodeMap
  1269. * the node map.
  1270. *
  1271. * @return the RDF dataset.
  1272. */
  1273. public RDFDataset toRDF(Map<String, Object> nodeMap) {
  1274. final UniqueNamer namer = new UniqueNamer("_:b");
  1275. final RDFDataset dataset = new RDFDataset(namer);
  1276. for (String graphName : nodeMap.keySet()) {
  1277. final Map<String, Object> graph = (Map<String, Object>) nodeMap.get(graphName);
  1278. if (graphName.indexOf("_:") == 0) {
  1279. graphName = namer.getName(graphName);
  1280. }
  1281. dataset.graphToRDF(graphName, graph);
  1282. }
  1283. return dataset;
  1284. }
  1285. /**
  1286. * Converts RDF statements into JSON-LD.
  1287. *
  1288. * @param statements
  1289. * the RDF statements.
  1290. * @param options
  1291. * the RDF conversion options.
  1292. * @param callback
  1293. * (err, output) called once the operation completes.
  1294. * @throws JSONLDProcessingError
  1295. */
  1296. public List<Object> fromRDF(final RDFDataset dataset) throws JSONLDProcessingError {
  1297. final Map<String, Object> defaultGraph = new LinkedHashMap<String, Object>();
  1298. final Map<String, Map<String, Object>> graphMap = new LinkedHashMap<String, Map<String, Object>>() {
  1299. {
  1300. put("@default", defaultGraph);
  1301. }
  1302. };
  1303. // For each graph in RDF dataset
  1304. for (final String name : dataset.graphNames()) {
  1305. final List<RDFDataset.Quad> graph = dataset.getQuads(name);
  1306. // If graph map has no name member, create one and set its value to
  1307. // an empty JSON object.
  1308. Map<String, Object> nodeMap;
  1309. if (!graphMap.containsKey(name)) {
  1310. nodeMap = new LinkedHashMap<String, Object>();
  1311. graphMap.put(name, nodeMap);
  1312. } else {
  1313. nodeMap = graphMap.get(name);
  1314. }
  1315. // If graph is not the default graph and default graph does not have
  1316. // a name member, create such
  1317. // a member and initialize its value to a new JSON object with a
  1318. // single member @id whose value is name.
  1319. if (!"@default".equals(name) && !Obj.contains(defaultGraph, name)) {
  1320. Obj.put(defaultGraph, name, new LinkedHashMap<String, Object>() {
  1321. {
  1322. put("@id", name);
  1323. }
  1324. });
  1325. }
  1326. // For each RDF triple in graph consisting of subject, predicate,
  1327. // and object
  1328. for (final RDFDataset.Quad triple : graph) {
  1329. final String subject = triple.getSubject().getValue();
  1330. final String predicate = triple.getPredicate().getValue();
  1331. final RDFDataset.Node object = triple.getObject();
  1332. // If node map does not have a subject member, create one and
  1333. // initialize its value to a new JSON object
  1334. // consisting of a single member @id whose value is set to
  1335. // subject.
  1336. Map<String, Object> node;
  1337. if (!nodeMap.containsKey(subject)) {
  1338. node = new LinkedHashMap<String, Object>() {
  1339. {
  1340. put("@id", subject);
  1341. }
  1342. };
  1343. nodeMap.put(subject, node);
  1344. } else {
  1345. node = (Map<String, Object>) nodeMap.get(subject);
  1346. }
  1347. // If object is an IRI or blank node identifier, does not equal
  1348. // rdf:nil, and node map does not have an object member,
  1349. // create one and initialize its value to a new JSON object
  1350. // consisting of a single member @id whose value is set to
  1351. // object.
  1352. if ((object.isIRI() || object.isBlankNode()) && !RDF_NIL.equals(object.getValue())
  1353. && !nodeMap.containsKey(object.getValue())) {
  1354. nodeMap.put(object.getValue(), new LinkedHashMap<String, Object>() {
  1355. {
  1356. put("@id", object.getValue());
  1357. }
  1358. });
  1359. }
  1360. // If predicate equals rdf:type, and object is an IRI or blank
  1361. // node identifier, append object to the value of the
  1362. // @type member of node. If no such member exists, create one
  1363. // and initialize it to an array whose only item is object.
  1364. // Finally, continue to the next RDF triple
  1365. if (RDF_TYPE.equals(predicate) && (object.isIRI() || object.isBlankNode())) {
  1366. addValue(node, "@type", object.getValue(), true);
  1367. continue;
  1368. }
  1369. // If object equals rdf:nil and predicate does not equal
  1370. // rdf:rest, set value to a new JSON object
  1371. // consisting of a single member @list whose value is set to an
  1372. // empty array.
  1373. Map<String, Object> value;
  1374. if (RDF_NIL.equals(object.getValue()) && !RDF_REST.equals(predicate)) {
  1375. value = new LinkedHashMap<String, Object>() {
  1376. {
  1377. put("@list", new ArrayList<Object>());
  1378. }
  1379. };
  1380. } else {
  1381. // Otherwise, set value to the result of using the RDF to
  1382. // Object Conversion algorithm, passing object and use
  1383. // native types.
  1384. value = object.toObject(opts.useNativeTypes);
  1385. }
  1386. // If node does not have an predicate member, create one and
  1387. // initialize its value to an empty array.
  1388. // Add a reference to value to the to the array associated with
  1389. // the predicate member of node.
  1390. addValue(node, predicate, value, true);
  1391. // If object is a blank node identifier and predicate equals
  1392. // neither rdf:first nor rdf:rest, it might represent the head
  1393. // of a RDF list
  1394. if (object.isBlankNode() && !RDF_FIRST.equals(predicate)
  1395. && !RDF_REST.equals(predicate)) {
  1396. // If the object member of node map has an usages member,
  1397. // add a reference to value to it;
  1398. // otherwise create such a member and set its value to an
  1399. // array whose only item is a reference to value.
  1400. addValue((Map<String, Object>) nodeMap.get(object.getValue()), "usages", value,
  1401. true);
  1402. }
  1403. }
  1404. }
  1405. // build @lists
  1406. for (final String name : graphMap.keySet()) {
  1407. final Map<String, Object> graph = graphMap.get(name);
  1408. final List<String> subjects = new ArrayList<String>(graph.keySet());
  1409. for (final String subj : subjects) {
  1410. // If graph object does not have a subj member, it has been
  1411. // removed as it was part of a list. Continue with the next
  1412. // subj.
  1413. if (!graph.containsKey(subj)) {
  1414. continue;
  1415. }
  1416. // If node has no usages member or its value is not an array
  1417. // consisting of one item, continue with the next subj.
  1418. Map<String, Object> node = (Map<String, Object>) graph.get(subj);
  1419. if (!node.containsKey("usages") || !(node.get("usages") instanceof List)
  1420. || ((List<Object>) node.get("usages")).size() != 1) {
  1421. continue;
  1422. }
  1423. final Map<String, Object> value = (Map<String, Object>) ((List<Object>) node
  1424. .get("usages")).get(0);
  1425. List<Object> list = new ArrayList<Object>();
  1426. final List<String> listNodes = new ArrayList<String>();
  1427. String subject = subj;
  1428. while (!RDF_NIL.equals(subject) && list != null) {
  1429. // If node is null; the value of its @id member does not
  1430. // begin with _: ...
  1431. boolean test = node == null || ((String) node.get("@id")).indexOf("_:") != 0;
  1432. if (!test) {
  1433. int cnt = 0;
  1434. for (final String i : new String[] { "@id", "usages", RDF_FIRST, RDF_REST }) {
  1435. if (node.containsKey(i)) {
  1436. cnt++;
  1437. }
  1438. }
  1439. // it has members other than @id, usages, rdf:first, and
  1440. // rdf:rest ...
  1441. test = (node.keySet().size() > cnt);
  1442. if (!test) {
  1443. // the value of its rdf:first member is not an array
  1444. // consisting of a single item ...
  1445. test = !(node.get(RDF_FIRST) instanceof List)
  1446. || ((List<Object>) node.get(RDF_FIRST)).size() != 1;
  1447. if (!test) {
  1448. // or the value of its rdf:rest member is not an
  1449. // array containing a single item which is a
  1450. // JSON object that has an @id member ...
  1451. test = !(node.get(RDF_REST) instanceof List)
  1452. || ((List<Object>) node.get(RDF_REST)).size() != 1;
  1453. if (!test) {
  1454. final Object o = ((List<Object>) node.get(RDF_REST)).get(0);
  1455. test = (!(o instanceof Map && ((Map<String, Object>) o)
  1456. .containsKey("@id")));
  1457. }
  1458. }
  1459. }
  1460. }
  1461. if (test) {
  1462. // it is not a valid list node. Set list to null
  1463. list = null;
  1464. } else {
  1465. list.add(((List<Object>) node.get(RDF_FIRST)).get(0));
  1466. listNodes.add((String) node.get("@id"));
  1467. subject = (String) ((Map<String, Object>) ((List<Object>) node
  1468. .get(RDF_REST)).get(0)).get("@id");
  1469. node = (Map<String, Object>) graph.get(subject);
  1470. if (listNodes.contains(subject)) {
  1471. list = null;
  1472. }
  1473. }
  1474. }
  1475. // If list is null, continue with the next subj.
  1476. if (list == null) {
  1477. continue;
  1478. }
  1479. // Remove the @id member from value.
  1480. value.remove("@id");
  1481. // Add an @list member to value and initialize it to list.
  1482. value.put("@list", list);
  1483. for (final String subject_ : listNodes) {
  1484. graph.remove(subject_);
  1485. }
  1486. }
  1487. }
  1488. final List<Object> result = new ArrayList<Object>();
  1489. final List<String> ids = new ArrayList<String>(defaultGraph.keySet());
  1490. Collections.sort(ids);
  1491. for (final String subject : ids) {
  1492. final Map<String, Object> node = (Map<String, Object>) defaultGraph.get(subject);
  1493. if (graphMap.containsKey(subject)) {
  1494. node.put("@graph", new ArrayList<Object>());
  1495. final List<String> keys = new ArrayList<String>(graphMap.get(subject).keySet());
  1496. Collections.sort(keys);
  1497. for (final String s : keys) {
  1498. final Map<String, Object> n = (Map<String, Object>) graphMap.get(subject)
  1499. .get(s);
  1500. n.remove("usages");
  1501. ((List<Object>) node.get("@graph")).add(n);
  1502. }
  1503. }
  1504. node.remove("usages");
  1505. result.add(node);
  1506. }
  1507. return result;
  1508. }
  1509. /**
  1510. * Performs JSON-LD flattening.
  1511. *
  1512. * @param input
  1513. * the expanded JSON-LD to flatten.
  1514. *
  1515. * @return the flattened output.
  1516. * @throws JSONLDProcessingError
  1517. */
  1518. public List<Object> flatten(List<Object> input) throws JSONLDProcessingError {
  1519. // produce a map of all subjects and name each bnode
  1520. final UniqueNamer namer = new UniqueNamer("_:b");
  1521. final Map<String, Object> graphs = new LinkedHashMap<String, Object>() {
  1522. {
  1523. put("@default", new LinkedHashMap<String, Object>());
  1524. }
  1525. };
  1526. createNodeMap(input, graphs, "@default", namer);
  1527. // add all non-default graphs to default graph
  1528. final Map<String, Object> defaultGraph = (Map<String, Object>) graphs.get("@default");
  1529. final List<String> graphNames = new ArrayList<String>(graphs.keySet());
  1530. Collections.sort(graphNames);
  1531. for (final String graphName : graphNames) {
  1532. if ("@default".equals(graphName)) {
  1533. continue;
  1534. }
  1535. final Map<String, Object> nodeMap = (Map<String, Object>) graphs.get(graphName);
  1536. Map<String, Object> subject = (Map<String, Object>) defaultGraph.get(graphName);
  1537. if (subject == null) {
  1538. subject = new LinkedHashMap<String, Object>();
  1539. subject.put("@id", graphName);
  1540. subject.put("@graph", new ArrayList<Object>());
  1541. defaultGraph.put(graphName, subject);
  1542. } else if (!subject.containsKey("@graph")) {
  1543. subject.put("@graph", new ArrayList<Object>());
  1544. }
  1545. final List<Object> graph = (List<Object>) subject.get("@graph");
  1546. final List<String> ids = new ArrayList<String>(nodeMap.keySet());
  1547. Collections.sort(ids);
  1548. for (final String id : ids) {
  1549. graph.add(nodeMap.get(id));
  1550. }
  1551. }
  1552. // produce flattened output
  1553. final List<Object> flattened = new ArrayList<Object>();
  1554. final List<String> keys = new ArrayList<String>(defaultGraph.keySet());
  1555. Collections.sort(keys);
  1556. for (final String key : keys) {
  1557. flattened.add(defaultGraph.get(key));
  1558. }
  1559. return flattened;
  1560. }
  1561. /**
  1562. * Generates a unique simplified key from a URI and add it to the context
  1563. *
  1564. * @param key
  1565. * to full URI to generate the simplified key from
  1566. * @param ctx
  1567. * the context to add the simplified key too
  1568. * @param isid
  1569. * whether to set the type to @id
  1570. */
  1571. private static void processKeyVal(Map<String, Object> ctx, String key, Object val) {
  1572. int idx = key.lastIndexOf('#');
  1573. if (idx < 0) {
  1574. idx = key.lastIndexOf('/');
  1575. }
  1576. String skey = key.substring(idx + 1);
  1577. Object keyval = key;
  1578. final Map entry = new LinkedHashMap();
  1579. entry.put("@id", keyval);
  1580. Object v = val;
  1581. while (true) {
  1582. if (v instanceof List && ((List) v).size() > 0) {
  1583. // use the first entry as a reference
  1584. v = ((List) v).get(0);
  1585. continue;
  1586. }
  1587. if (v instanceof Map && ((Map) v).containsKey("@list")) {
  1588. v = ((Map) v).get("@list");
  1589. entry.put("@container", "@list");
  1590. continue;
  1591. }
  1592. if (v instanceof Map && ((Map) v).containsKey("@set")) {
  1593. v = ((Map) v).get("@set");
  1594. entry.put("@container", "@set");
  1595. continue;
  1596. }
  1597. break;
  1598. }
  1599. if (v instanceof Map && ((Map) v).containsKey("@id")) {
  1600. entry.put("@type", "@id");
  1601. }
  1602. if (entry.size() == 1) {
  1603. keyval = entry.get("@id");
  1604. } else {
  1605. keyval = entry;
  1606. }
  1607. while (true) {
  1608. // check if the key is already in the frame ctx
  1609. if (ctx.containsKey(skey)) {
  1610. // if so, check if the values are the same
  1611. if (JSONUtils.equals(ctx.get(skey), keyval)) {
  1612. // if they are, skip adding this
  1613. break;
  1614. }
  1615. // if not, add a _ to the simple key and try again
  1616. skey += "_";
  1617. } else {
  1618. ctx.put(skey, keyval);
  1619. break;
  1620. }
  1621. }
  1622. }
  1623. /**
  1624. * Generates the context to be used by simplify.
  1625. *
  1626. * @param input
  1627. * @param ctx
  1628. */
  1629. private static void generateSimplifyContext(Object input, Map<String, Object> ctx) {
  1630. if (input instanceof List) {
  1631. for (final Object o : (List) input) {
  1632. generateSimplifyContext(o, ctx);
  1633. }
  1634. } else if (input instanceof Map) {
  1635. final Map<String, Object> o = (Map<String, Object>) input;
  1636. final Map<String, Object> localCtx = (Map<String, Object>) o.remove("@context");
  1637. for (final String key : o.keySet()) {
  1638. Object val = o.get(key);
  1639. if (key.matches("^https?://.+$")) {
  1640. processKeyVal(ctx, key, val);
  1641. }
  1642. if ("@type".equals(key)) {
  1643. if (!(val instanceof List)) {
  1644. final List<Object> tmp = new ArrayList<Object>();
  1645. tmp.add(val);
  1646. val = tmp;
  1647. }
  1648. for (final Object t : (List<Object>) val) {
  1649. if (t instanceof String) {
  1650. processKeyVal(ctx, (String) t, new LinkedHashMap<String, Object>() {
  1651. {
  1652. put("@id", "");
  1653. }
  1654. });
  1655. } else {
  1656. throw new RuntimeException(
  1657. "TODO: don't yet know how to handle non-string types in @type");
  1658. }
  1659. }
  1660. } else if (val instanceof Map || val instanceof List) {
  1661. generateSimplifyContext(val, ctx);
  1662. }
  1663. }
  1664. }
  1665. }
  1666. /**
  1667. * Automatically builds a context which attempts to simplify the keys and
  1668. * values as much as possible and uses that context to compact the input
  1669. *
  1670. * NOTE: this is experimental and only built for specific conditions
  1671. *
  1672. * @param input
  1673. * @return the simplified version of input
  1674. * @throws JSONLDProcessingError
  1675. */
  1676. public Object simplify(Object input) throws JSONLDProcessingError {
  1677. final Object expanded = JSONLD.expand(input, opts);
  1678. final Map<String, Object> ctx = new LinkedHashMap<String, Object>();
  1679. generateSimplifyContext(expanded, ctx);
  1680. final Map<String, Object> tmp = new LinkedHashMap<String, Object>();
  1681. tmp.put("@context", ctx);
  1682. // add optimize flag to opts (clone the opts so we don't change the flag
  1683. // for the base processor)
  1684. final Options opts1 = opts.clone();
  1685. // opts1.optimize = true;
  1686. return JSONLD.compact(input, tmp, opts1);
  1687. }
  1688. }