PageRenderTime 64ms CodeModel.GetById 3ms 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

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

  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 {

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