33import java .io .IOException ;
44import java .net .MalformedURLException ;
55import java .net .URL ;
6- import java .util .ArrayList ;
7- import java .util .HashSet ;
8- import java .util .Iterator ;
9- import java .util .List ;
10- import java .util .Map ;
11- import java .util .Set ;
12- import java .util .Vector ;
6+ import java .util .*;
137import java .util .regex .Pattern ;
148import org .apache .commons .collections4 .map .ListOrderedMap ;
159import org .slf4j .Logger ;
@@ -58,7 +52,7 @@ public final class JsonLdFormatter extends AbstractFormatter {
5852 /**
5953 * The set of used profiles
6054 */
61- private final Set <URL > profiles = new HashSet <URL >();
55+ private final Set <URL > profiles = new HashSet <>();
6256 /**
6357 * The profile registry
6458 */
@@ -143,7 +137,6 @@ public String format(FormattableObject obj) {
143137 /**
144138 * Remove all blank node IDs from a given "pretty" JSON-LD String
145139 *
146- * @param jsonLdPretty JSON-LD serialized in pretty mode
147140 * @return The same JSON-LD without blank node IDs
148141 */
149142 private String removeBlankNodeIds (String jsonLdWithBlankNodeIds ) {
@@ -171,7 +164,7 @@ private String removeBlankNodeIds(String jsonLdWithBlankNodeIds) {
171164 String [] lines = jsonLdWithBlankNodeIds .split (Pattern .quote ("\n " ));
172165 for (String line : lines ) {
173166 if (line .trim ().startsWith ("\" id\" " )) {
174- if (line .indexOf ("\" _:b" ) != - 1 ) {
167+ if (line .contains ("\" _:b" )) {
175168 continue ; // skip this unneeded line
176169 } else {
177170 builder .append (line );
@@ -197,83 +190,126 @@ private String removeBlankNodeIds(String jsonLdWithBlankNodeIds) {
197190 * @param frameString The JSON-LD Frame as String
198191 * @return the expanded string
199192 */
200- @ SuppressWarnings ("unchecked" )
201193 private String applyProfiles (String jsonLd , String frameString ) {
202194 try {
195+
203196 final Object frameObject = frameString == null ? null : JsonUtils .fromString (frameString );
204197 Object jsonObject = JsonUtils .fromString (jsonLd );
205198 // Use precreated options with in memory profiles
206199 JsonLdOptions optionsWithContexts = profileRegistry .getJsonLdOptions ();
207- Map <String , Object > framed = null ;
208- if (frameObject != null ) {
200+
201+ List <Object > contexts = new ArrayList <>();
202+
203+ //We assume that framing and compacting only needs to be applied if anno profile is present
204+ if (frameObject != null && profiles .contains (DEFAULT_PROFILE )) {
209205 final JsonLdOptions options = profileRegistry .getJsonLdOptions ();
210206 options .format = JsonLdConsts .APPLICATION_NQUADS ;
211207 options .setCompactArrays (true );
212- framed = JsonLdProcessor .frame (jsonObject , frameObject , options );
213- List <String > contexts = new Vector <String >();
208+
209+ // Frame the RDF-converted JSON-LD
210+ jsonObject = JsonLdProcessor .frame (jsonObject , frameObject , options );
211+
212+ // Collect contexts from profiles
214213 for (URL url : profiles ) {
215- String context = url .toString ();
216- contexts .add (context );
214+ contexts .add (url .toString ());
217215 }
218- java .util .Collections .reverse (contexts );
219216
220- if (!profiles .isEmpty ()) {
221- // Compact only if client requested that
222- jsonObject = JsonLdProcessor .compact (framed , contexts , optionsWithContexts );
223- }
224- } else {
225- for (URL url : profiles ) {
226- String context = url .toString ();
227- jsonObject = JsonLdProcessor .compact (jsonObject , context , optionsWithContexts );
217+ // Extract @context from the frame and add it to the list
218+ if (frameObject instanceof Map ) {
219+ Object frameContext = ((Map <?, ?>) frameObject ).get ("@context" );
220+ if (frameContext != null ) {
221+ if (frameContext instanceof List ) {
222+ // Flatten the list
223+ contexts .addAll ((List <?>) frameContext );
224+ } else {
225+ // Add single context (string or map)
226+ contexts .add (frameContext );
227+ }
228+ }
228229 }
229230 }
230- if (!profiles .isEmpty ()) {
231- // JSON-LD java api uses just objects, we cannot prevent casting here
232- insertContexts ((Map <String , Object >) jsonObject );
233- jsonObject = reorderJsonAttributes ((Map <String , Object >) jsonObject );
231+ for (URL url : profiles ) {
232+ contexts .add (url .toString ());
234233 }
234+ contexts = deduplicateContexts (contexts );
235+ jsonObject = JsonLdProcessor .compact (jsonObject , contexts , optionsWithContexts );
236+
237+ jsonObject = reorderJsonAttributes (jsonObject );
235238 return JsonUtils .toPrettyString (jsonObject );
236239 } catch (JsonLdError | IOException e ) {
237240 throw new FormatException (e .getMessage (), e );
238241 }
239242 }
240243
241- private void insertContexts (Map <String , Object > map ) {
242- Object contexts = createContextString ();
243- if (contexts != null ) {
244- map .put ("@context" , contexts );
244+ /**
245+ * Deduplicates and flattens a list of JSON-LD context entries.
246+ * Supports strings (URIs), maps (inline contexts), and nested lists.
247+ *
248+ * @param rawContexts A list of context entries (String, Map, or List)
249+ * @return A flattened, deduplicated list of context entries
250+ */
251+ public List <Object > deduplicateContexts (List <Object > rawContexts ) {
252+
253+ List <Object > result = new ArrayList <>();
254+ Set <String > seenUris = new HashSet <>();
255+ Set <String > seenInlineContexts = new HashSet <>();
256+
257+
258+ for (Object ctx : rawContexts ) {
259+ flattenAndAdd (ctx , result , seenUris , seenInlineContexts );
245260 }
261+
262+ return result ;
246263 }
247264
248- private Object createContextString () {
249- if (profiles .isEmpty ()) {
250- return null ;
251- }
252- if (profiles .size () == 1 ) {
253- return profiles .iterator ().next ().toString ();
254- }
255- Iterator <URL > iterator = profiles .iterator ();
256- List <Object > list = new ArrayList <Object >();
257- while (iterator .hasNext ()) {
258- list .add (iterator .next ().toString ());
265+ private void flattenAndAdd (Object ctx , List <Object > result ,
266+ Set <String > seenUris , Set <String > seenInlineContexts ) {
267+ if (ctx instanceof String uri ) {
268+ if (seenUris .add (uri )) {
269+ result .add (uri );
270+ }
271+ } else if (ctx instanceof Map ) {
272+ try {
273+ String json = JsonUtils .toString (ctx );
274+ if (seenInlineContexts .add (json )) {
275+ result .add (ctx );
276+ }
277+ } catch (IOException e ) {
278+ throw new RuntimeException ("Failed to serialize inline context" , e );
279+ }
280+ } else if (ctx instanceof List ) {
281+ for (Object nested : (List <?>) ctx ) {
282+ flattenAndAdd (nested , result , seenUris , seenInlineContexts );
283+ }
284+ } else {
285+ throw new IllegalArgumentException ("Unsupported context type: " + ctx .getClass ());
259286 }
260- return list ;
261287 }
262288
263- private Map <String , Object > reorderJsonAttributes (Map <String , Object > jsonMap ) {
264- if (profiles .isEmpty ()) {
265- // Framing added the context, now remove if client did not want it
266- jsonMap .remove ("@context" );
267- return jsonMap ;
268- }
269- ListOrderedMap <String , Object > orderedJsonMap = new ListOrderedMap <String , Object >();
270- orderedJsonMap .putAll (jsonMap );
271- Object context = orderedJsonMap .get ("@context" );
272- if (context != null ) {
273- orderedJsonMap .remove ("@context" );
274- orderedJsonMap .put (0 , "@context" , context );
289+ @ SuppressWarnings ("unchecked" )
290+ private Object reorderJsonAttributes (Object jsonObject ) {
291+ if (jsonObject instanceof Map ) {
292+ Map <String , Object > jsonMap = (Map <String , Object >) jsonObject ;
293+ ListOrderedMap <String , Object > orderedJsonMap = new ListOrderedMap <>();
294+ orderedJsonMap .putAll (jsonMap );
295+
296+ Object context = orderedJsonMap .get ("@context" );
297+ if (context != null ) {
298+ orderedJsonMap .remove ("@context" );
299+ orderedJsonMap .put (0 , "@context" , context );
300+ }
301+ return orderedJsonMap ;
302+ } else if (jsonObject instanceof List ) {
303+ // Recursively reorder each item in the list
304+ List <Object > reorderedList = new ArrayList <>();
305+ for (Object item : (List <?>) jsonObject ) {
306+ reorderedList .add (reorderJsonAttributes (item ));
307+ }
308+ return reorderedList ;
309+ } else {
310+ // Return as-is if it's neither a Map nor a List
311+ return jsonObject ;
275312 }
276- return orderedJsonMap ;
277313 }
278314
279315 @ Override
@@ -354,20 +390,20 @@ private String getProfilesWithingQuotes(String profiles) {
354390
355391 @ Override
356392 public String getContentType () {
357- if (profiles == null || profiles .isEmpty ()) {
393+ if (profiles .isEmpty ()) {
358394 return getFormatString ();
359395 }
360396 StringBuilder profileString = new StringBuilder ();
361397 for (URL url : profiles ) {
362398 String context = url .toString ();
363- if (profileString .length () != 0 ) {
399+ if (! profileString .isEmpty () ) {
364400 profileString .append (" " );
365401 }
366402 profileString .append (context );
367403 }
368404 profileString .insert (0 , "\" " );
369405 profileString .append ("\" " );
370- return getFormatString () + ";profile=" + profileString . toString () + ";charset=utf-8" ;
406+ return getFormatString () + ";profile=" + profileString + ";charset=utf-8" ;
371407 }
372408
373409 @ Override
0 commit comments