001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2020, Connect2id Ltd and contributors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.openid.connect.sdk.claims;
019
020
021import java.util.*;
022
023import net.jcip.annotations.Immutable;
024import net.minidev.json.JSONAware;
025import net.minidev.json.JSONObject;
026
027import com.nimbusds.langtag.LangTag;
028import com.nimbusds.langtag.LangTagException;
029import com.nimbusds.oauth2.sdk.ParseException;
030import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
031
032
033/**
034 * OpenID Connect claims set request, intended to represent the
035 * {@code userinfo} and {@code id_token} elements in a
036 * {@link com.nimbusds.openid.connect.sdk.OIDCClaimsRequest claims} request
037 * parameter.
038 *
039 * <p>Example:
040 *
041 * <pre>
042 * {
043 *   "given_name": {"essential": true},
044 *   "nickname": null,
045 *   "email": {"essential": true},
046 *   "email_verified": {"essential": true},
047 *   "picture": null,
048 *   "http://example.info/claims/groups": null
049 * }
050 * </pre>
051 *
052 * <p>Related specifications:
053 *
054 * <ul>
055 *     <li>OpenID Connect Core 1.0, section 5.5.
056 *     <li>OpenID Connect for Identity Assurance 1.0.
057 * </ul>
058 */
059@Immutable
060public class ClaimsSetRequest implements JSONAware {
061        
062        
063        /**
064         * Individual OpenID claim request.
065         *
066         * <p>Related specifications:
067         *
068         * <ul>
069         *     <li>OpenID Connect Core 1.0, section 5.5.1.
070         *     <li>OpenID Connect for Identity Assurance 1.0.
071         * </ul>
072         */
073        @Immutable
074        public static class Entry {
075                
076                
077                /**
078                 * The claim name.
079                 */
080                private final String claimName;
081                
082                
083                /**
084                 * The claim requirement.
085                 */
086                private final ClaimRequirement requirement;
087                
088                
089                /**
090                 * Optional language tag.
091                 */
092                private final LangTag langTag;
093                
094                
095                /**
096                 * Optional claim value, as string, number or JSON object.
097                 */
098                private final Object value;
099                
100                
101                /**
102                 * Optional claim values, as an array of JSON entities.
103                 */
104                private final List<?> values;
105                
106                
107                /**
108                 * Optional claim purpose.
109                 */
110                private final String purpose;
111                
112                
113                /**
114                 * Optional additional claim information.
115                 *
116                 * <p>Example additional information in the "info" member:
117                 *
118                 * <pre>
119                 * {
120                 *   "userinfo" : {
121                 *       "email": null,
122                 *       "email_verified": null,
123                 *       "http://example.info/claims/groups" : { "info" : "custom information" }
124                 *   }
125                 * }
126                 * </pre>
127                 */
128                private final Map<String, Object> additionalInformation;
129                
130                
131                /**
132                 * Creates a new individual claim request. The claim
133                 * requirement is set to {@link ClaimRequirement#VOLUNTARY
134                 * voluntary} (the default) and no expected value(s) or other
135                 * parameters are specified.
136                 *
137                 * @param claimName The claim name. Must not be {@code null}.
138                 */
139                public Entry(final String claimName) {
140                        this(claimName, ClaimRequirement.VOLUNTARY, null, null, null, null, null);
141                }
142                
143                
144                /**
145                 * Creates a new individual claim request. This constructor is
146                 * to be used privately. Ensures that {@code value} and
147                 * {@code values} are not simultaneously specified.
148                 *
149                 * @param claimName             The claim name. Must not be
150                 *                              {@code null}.
151                 * @param requirement           The claim requirement. Must not
152                 *                              be {@code null}.
153                 * @param langTag               Optional language tag for the
154                 *                              claim.
155                 * @param value                 Optional expected value for the
156                 *                              claim. If set, then the {@code
157                 *                              values} parameter must not be
158                 *                              set.
159                 * @param values                Optional expected values for
160                 *                              the claim. If set, then the
161                 *                              {@code value} parameter must
162                 *                              not be set.
163                 * @param purpose               The purpose for the requested
164                 *                              claim, {@code null} if not
165                 *                              specified.
166                 * @param additionalInformation Optional additional information
167                 */
168                private Entry(final String claimName,
169                              final ClaimRequirement requirement,
170                              final LangTag langTag,
171                              final Object value,
172                              final List<?> values,
173                              final String purpose,
174                              final Map<String, Object> additionalInformation) {
175                        
176                        if (claimName == null)
177                                throw new IllegalArgumentException("The claim name must not be null");
178                        
179                        this.claimName = claimName;
180                        
181                        
182                        if (requirement == null)
183                                throw new IllegalArgumentException("The claim requirement must not be null");
184                        
185                        this.requirement = requirement;
186                        
187                        
188                        this.langTag = langTag;
189                        
190                        
191                        if (value != null && values == null) {
192                                
193                                this.value = value;
194                                this.values = null;
195                                
196                        } else if (value == null && values != null) {
197                                
198                                this.value = null;
199                                this.values = values;
200                                
201                        } else if (value == null && values == null) {
202                                
203                                this.value = null;
204                                this.values = null;
205                                
206                        } else {
207                                
208                                throw new IllegalArgumentException("Either value or values must be specified, but not both");
209                        }
210                        
211                        this.purpose = purpose;
212                        
213                        this.additionalInformation = additionalInformation;
214                }
215                
216                
217                /**
218                 * Returns the claim name.
219                 *
220                 * @return The claim name.
221                 */
222                public String getClaimName() {
223                        return getClaimName(false);
224                }
225                
226                
227                /**
228                 * Returns the claim name, optionally with the language tag
229                 * appended.
230                 *
231                 * <p>Example with language tag:
232                 *
233                 * <pre>
234                 * name#de-DE
235                 * </pre>
236                 *
237                 * @param withLangTag If {@code true} the language tag will be
238                 *                    appended to the name (if any), else not.
239                 *
240                 * @return The claim name, with optionally appended language
241                 *         tag.
242                 */
243                public String getClaimName(final boolean withLangTag) {
244                        
245                        if (withLangTag && langTag != null)
246                                return claimName + "#" + langTag;
247                        else
248                                return claimName;
249                }
250                
251                
252                /**
253                 * Sets the claim requirement.
254                 *
255                 * @param requirement The claim requirement. Must not be
256                 *                    {@code null},
257                 *
258                 * @return The updated entry.
259                 */
260                public ClaimsSetRequest.Entry withClaimRequirement(final ClaimRequirement requirement) {
261                        return new ClaimsSetRequest.Entry(claimName, requirement, langTag, value, values, purpose, additionalInformation);
262                }
263                
264                
265                /**
266                 * Returns the claim requirement.
267                 *
268                 * @return The claim requirement.
269                 */
270                public ClaimRequirement getClaimRequirement() {
271                        return requirement;
272                }
273                
274                
275                /**
276                 * Sets the language tag for the claim.
277                 *
278                 * @param langTag The language tag, {@code null} if not
279                 *                specified.
280                 *
281                 * @return The updated entry.
282                 */
283                public ClaimsSetRequest.Entry withLangTag(final LangTag langTag) {
284                        return new ClaimsSetRequest.Entry(claimName, requirement, langTag, value, values, purpose, additionalInformation);
285                }
286                
287                
288                /**
289                 * Returns the optional language tag for the claim.
290                 *
291                 * @return The language tag, {@code null} if not specified.
292                 */
293                public LangTag getLangTag() {
294                        return langTag;
295                }
296                
297                
298                /**
299                 * Sets the requested value (as string) for the claim.
300                 *
301                 * @param value The value, {@code null} if not specified.
302                 *
303                 * @return The updated entry.
304                 */
305                public ClaimsSetRequest.Entry withValue(final String value) {
306                        return new ClaimsSetRequest.Entry(claimName, requirement, langTag, value, null, purpose, additionalInformation);
307                }
308                
309                
310                /**
311                 * Sets the requested value (as number) for the claim.
312                 *
313                 * @param value The value, {@code null} if not specified.
314                 *
315                 * @return The updated entry.
316                 */
317                public ClaimsSetRequest.Entry withValue(final Number value) {
318                        return new ClaimsSetRequest.Entry(claimName, requirement, langTag, value, null, purpose, additionalInformation);
319                }
320                
321                
322                /**
323                 * Sets the requested value (as JSON object) for the claim.
324                 *
325                 * @param value The value, {@code null} if not specified.
326                 *
327                 * @return The updated entry.
328                 */
329                public ClaimsSetRequest.Entry withValue(final JSONObject value) {
330                        return new ClaimsSetRequest.Entry(claimName, requirement, langTag, value, null, purpose, additionalInformation);
331                }
332                
333                
334                /**
335                 * Sets the requested value (untyped) for the claim.
336                 *
337                 * @param value The value, {@code null} if not specified.
338                 *
339                 * @return The updated entry.
340                 */
341                public ClaimsSetRequest.Entry withValue(final Object value) {
342                        return new ClaimsSetRequest.Entry(claimName, requirement, langTag, value, null, purpose, additionalInformation);
343                }
344                
345                
346                /**
347                 * Returns the requested value (as string) for the claim.
348                 *
349                 * @return The value as string, {@code null} if not specified
350                 *         or the value isn't a string.
351                 */
352                public String getValueAsString() {
353                        if (value instanceof String) {
354                                return (String)value;
355                        } else {
356                                return null;
357                        }
358                }
359                
360                
361                /**
362                 * Returns the requested value (as string) for the claim. Use
363                 * {@link #getValueAsString()} instead.
364                 *
365                 * @return The value as string, {@code null} if not specified
366                 *         or the value isn't a string.
367                 */
368                @Deprecated
369                public String getValue() {
370                        return getValueAsString();
371                }
372                
373                
374                /**
375                 * Returns the requested value (as number) for the claim.
376                 *
377                 * @return The value as number, {@code null} if not specified
378                 *         or the value isn't a number.
379                 */
380                public Number getValueAsNumber() {
381                        if (value instanceof Number) {
382                                return (Number)value;
383                        } else {
384                                return null;
385                        }
386                }
387                
388                
389                /**
390                 * Returns the requested value (as JSON object) for the claim.
391                 *
392                 * @return The value as JSON object, {@code null} if not
393                 *         specified or the value isn't a JSON object.
394                 */
395                public JSONObject getValueAsJSONObject() {
396                        if (value instanceof JSONObject) {
397                                return (JSONObject)value;
398                        } else {
399                                return null;
400                        }
401                }
402                
403                
404                /**
405                 * Returns the requested value (untyped) for the claim.
406                 *
407                 * @return The value (untyped), {@code null} if not specified.
408                 */
409                public Object getRawValue() {
410                        return value;
411                }
412                
413                
414                /**
415                 * Sets the requested values (untyped) for the claim.
416                 *
417                 * @param values The values, {@code null} if not specified.
418                 *
419                 * @return The updated entry.
420                 */
421                public ClaimsSetRequest.Entry withValues(final List<?> values) {
422                        return new ClaimsSetRequest.Entry(claimName, requirement, langTag, null, values, purpose, additionalInformation);
423                }
424                
425                
426                /**
427                 * Returns the requested values (as strings) for the claim.
428                 *
429                 * @return The values as list of strings, {@code null} if not
430                 *         specified or the values aren't strings.
431                 */
432                public List<String> getValuesAsListOfStrings() {
433                        if (values == null) {
434                                return null;
435                        }
436                        if (values.isEmpty()) {
437                                return Collections.emptyList();
438                        }
439                        List<String> list = new ArrayList<>(values.size());
440                        for (Object v: values) {
441                                if (v instanceof String) {
442                                        list.add((String)v);
443                                } else {
444                                        return null;
445                                }
446                        }
447                        return list;
448                }
449                
450                
451                /**
452                 * Returns the requested values (as strings) for the claim. Use
453                 * {@link #getValuesAsListOfStrings()} instead.
454                 *
455                 * @return The values as list of strings, {@code null} if not
456                 *         specified or the values aren't strings.
457                 */
458                @Deprecated
459                public List<String> getValues() {
460                        return getValuesAsListOfStrings();
461                }
462                
463                
464                /**
465                 * Returns the requested values (as JSON objects) for the
466                 * claim.
467                 *
468                 * @return The values as list of JSON objects, {@code null} if
469                 *         not specified or the values aren't JSON objects.
470                 */
471                public List<JSONObject> getValuesAsListOfJSONObjects() {
472                        if (values == null) {
473                                return null;
474                        }
475                        if (values.isEmpty()) {
476                                return Collections.emptyList();
477                        }
478                        List<JSONObject> list = new ArrayList<>(values.size());
479                        for (Object v: values) {
480                                if (v instanceof JSONObject) {
481                                        list.add((JSONObject) v);
482                                } else {
483                                        return null;
484                                }
485                        }
486                        return list;
487                }
488                
489                
490                /**
491                 * Returns the requested values (untyped) for the claim.
492                 *
493                 * @return The values as list of untyped objects, {@code null}
494                 *         if not specified.
495                 */
496                public List<?> getValuesAsRawList() {
497                        return values;
498                }
499                
500                
501                /**
502                 * Sets the purpose for which the claim is requested.
503                 *
504                 * @param purpose The purpose, {@code null} if not specified.
505                 *
506                 * @return The updated entry.
507                 */
508                public ClaimsSetRequest.Entry withPurpose(final String purpose) {
509                        return new ClaimsSetRequest.Entry(claimName, requirement, langTag, value, values, purpose, additionalInformation);
510                }
511                
512                
513                /**
514                 * Returns the optional purpose for which the claim is
515                 * requested.
516                 *
517                 * @return The purpose, {@code null} if not specified.
518                 */
519                public String getPurpose() {
520                        return purpose;
521                }
522                
523                
524                /**
525                 * Sets additional information for the requested claim.
526                 *
527                 * <p>Example additional information in the "info" member:
528                 *
529                 * <pre>
530                 * {
531                 *   "userinfo" : {
532                 *       "email": null,
533                 *       "email_verified": null,
534                 *       "http://example.info/claims/groups" : { "info" : "custom information" }
535                 *   }
536                 * }
537                 * </pre>
538                 *
539                 * @param additionalInformation The additional information,
540                 *                              {@code null} if not specified.
541                 *
542                 * @return The updated entry.
543                 */
544                public ClaimsSetRequest.Entry withAdditionalInformation(final Map<String, Object> additionalInformation) {
545                        return new ClaimsSetRequest.Entry(claimName, requirement, langTag, value, values, purpose, additionalInformation);
546                }
547                
548                
549                /**
550                 * Returns the additional information for the claim.
551                 *
552                 * <p>Example additional information in the "info" member:
553                 *
554                 * <pre>
555                 * {
556                 *   "userinfo" : {
557                 *       "email": null,
558                 *       "email_verified": null,
559                 *       "http://example.info/claims/groups" : { "info" : "custom information" }
560                 *   }
561                 * }
562                 * </pre>
563                 *
564                 * @return The additional information, {@code null} if not
565                 *         specified.
566                 */
567                public Map<String, Object> getAdditionalInformation() {
568                        return additionalInformation;
569                }
570                
571                
572                /**
573                 * Returns the JSON object entry for this individual claim
574                 * request.
575                 *
576                 * @return The JSON object entry.
577                 */
578                public Map.Entry<String,JSONObject> toJSONObjectEntry() {
579                        
580                        // Compose the optional value
581                        JSONObject entrySpec = null;
582                        
583                        if (getRawValue() != null) {
584                                
585                                entrySpec = new JSONObject();
586                                entrySpec.put("value", getRawValue());
587                        }
588                        
589                        if (getValuesAsRawList() != null) {
590                                
591                                // Either "value" or "values", or none
592                                // may be defined
593                                entrySpec = new JSONObject();
594                                entrySpec.put("values", getValuesAsRawList());
595                        }
596                        
597                        if (getClaimRequirement().equals(ClaimRequirement.ESSENTIAL)) {
598                                
599                                if (entrySpec == null)
600                                        entrySpec = new JSONObject();
601                                
602                                entrySpec.put("essential", true);
603                        }
604                        
605                        if (getPurpose() != null) {
606                                if (entrySpec == null) {
607                                        entrySpec = new JSONObject();
608                                }
609                                entrySpec.put("purpose", getPurpose());
610                        }
611                        
612                        if (getAdditionalInformation() != null) {
613                                if (entrySpec == null) {
614                                        entrySpec = new JSONObject();
615                                }
616                                for (Map.Entry<String, Object> additionalInformationEntry : getAdditionalInformation().entrySet()) {
617                                        entrySpec.put(additionalInformationEntry.getKey(), additionalInformationEntry.getValue());
618                                }
619                        }
620                        
621                        return new AbstractMap.SimpleImmutableEntry<>(getClaimName(true), entrySpec);
622                }
623                
624                
625                /**
626                 * Parses an individual claim request from the specified JSON
627                 * object entry.
628                 *
629                 * @param jsonObjectEntry The JSON object entry to parse. Must
630                 *                        not be {@code null}.
631                 *
632                 * @return The individual claim request.
633                 *
634                 * @throws ParseException If parsing failed.
635                 */
636                public static ClaimsSetRequest.Entry parse(final Map.Entry<String,JSONObject> jsonObjectEntry)
637                        throws ParseException {
638                        
639                        // Process the key
640                        String claimNameWithOptLangTag = jsonObjectEntry.getKey();
641                        
642                        String claimName;
643                        LangTag langTag = null;
644                        
645                        if (claimNameWithOptLangTag.contains("#")) {
646                                
647                                String[] parts = claimNameWithOptLangTag.split("#", 2);
648                                
649                                claimName = parts[0];
650                                
651                                try {
652                                        langTag = LangTag.parse(parts[1]);
653                                } catch (LangTagException e) {
654                                        throw new ParseException(e.getMessage(), e);
655                                }
656                                
657                        } else {
658                                claimName = claimNameWithOptLangTag;
659                        }
660                        
661                        // Parse the optional spec
662                        
663                        JSONObject spec = jsonObjectEntry.getValue();
664                        
665                        if (spec == null) {
666                                // Voluntary claim with no value(s)
667                                return new ClaimsSetRequest.Entry(claimName).withLangTag(langTag);
668                        }
669                        
670                        ClaimRequirement requirement = ClaimRequirement.VOLUNTARY;
671                        
672                        if (spec.containsKey("essential")) {
673                                
674                                boolean isEssential = JSONObjectUtils.getBoolean(spec, "essential");
675                                
676                                if (isEssential)
677                                        requirement = ClaimRequirement.ESSENTIAL;
678                        }
679                        
680                        String purpose = JSONObjectUtils.getString(spec, "purpose", null);
681                        
682                        if (spec.get("value") != null) {
683                                
684                                Object expectedValue = spec.get("value");
685                                Map<String, Object> additionalInformation = getAdditionalInformationFromClaim(spec);
686                                return new ClaimsSetRequest.Entry(claimName, requirement, langTag, expectedValue, null, purpose, additionalInformation);
687                                
688                        } else if (spec.get("values") != null) {
689                                
690                                List<Object> expectedValues = JSONObjectUtils.getList(spec, "values");
691                                Map<String, Object> additionalInformation = getAdditionalInformationFromClaim(spec);
692                                return new ClaimsSetRequest.Entry(claimName, requirement, langTag, null, expectedValues, purpose, additionalInformation);
693                                
694                        } else {
695                                Map<String, Object> additionalInformation = getAdditionalInformationFromClaim(spec);
696                                return new ClaimsSetRequest.Entry(claimName, requirement, langTag, null, null, purpose, additionalInformation);
697                        }
698                }
699                
700                
701                private static Map<String, Object> getAdditionalInformationFromClaim(final JSONObject spec) {
702                        
703                        Set<String> stdKeys = new HashSet<>(Arrays.asList("essential", "value", "values", "purpose"));
704                        
705                        Map<String, Object> additionalClaimInformation = new HashMap<>();
706                        
707                        for (Map.Entry<String, Object> additionalClaimInformationEntry : spec.entrySet()) {
708                                if (stdKeys.contains(additionalClaimInformationEntry.getKey())) {
709                                        continue; // skip std key
710                                }
711                                additionalClaimInformation.put(additionalClaimInformationEntry.getKey(), additionalClaimInformationEntry.getValue());
712                        }
713                        
714                        return additionalClaimInformation.isEmpty() ? null : additionalClaimInformation;
715                }
716        }
717        
718        
719        /**
720         * The request entries.
721         */
722        private final Collection<ClaimsSetRequest.Entry> entries;
723        
724        
725        /**
726         * Creates a new empty OpenID Connect claims set request.
727         */
728        public ClaimsSetRequest() {
729                this(Collections.<Entry>emptyList());
730        }
731        
732        
733        /**
734         * Creates a new OpenID Connect claims set request.
735         *
736         * @param entries The request entries, empty collection if none. Must
737         *                not be {@code null}.
738         */
739        public ClaimsSetRequest(final Collection<ClaimsSetRequest.Entry> entries) {
740                if (entries == null) {
741                        throw new IllegalArgumentException("The entries must not be null");
742                }
743                this.entries = Collections.unmodifiableCollection(entries);
744        }
745        
746        
747        /**
748         * Adds the specified claim to the request, using default settings.
749         * Shorthand for {@link #add(Entry)}.
750         *
751         * @param claimName The claim name. Must not be {@code null}.
752         *
753         * @return The updated claims set request.
754         */
755        public ClaimsSetRequest add(final String claimName) {
756                return add(new ClaimsSetRequest.Entry(claimName));
757        }
758        
759        
760        /**
761         * Adds the specified claim to the request.
762         *
763         * @param entry The individual claim request. Must not be {@code null}.
764         *
765         * @return The updated claims set request.
766         */
767        public ClaimsSetRequest add(final ClaimsSetRequest.Entry entry) {
768                List<Entry> updatedEntries = new LinkedList<>(getEntries());
769                updatedEntries.add(entry);
770                return new ClaimsSetRequest(updatedEntries);
771        }
772        
773        
774        /**
775         * Gets the request entries.
776         *
777         * @return The request entries, empty collection if none.
778         */
779        public Collection<ClaimsSetRequest.Entry> getEntries() {
780                return entries;
781        }
782        
783        
784        /**
785         * Gets the names of the requested claims.
786         *
787         * @param withLangTag If {@code true} the language tags, if any, will
788         *                    be appended to the names, else not.
789         *
790         * @return The claim names, as an unmodifiable set, empty set if none.
791         */
792        public Set<String> getClaimNames(final boolean withLangTag) {
793                Set<String> names = new HashSet<>();
794                for (ClaimsSetRequest.Entry en : entries) {
795                        names.add(en.getClaimName(withLangTag));
796                }
797                return Collections.unmodifiableSet(names);
798        }
799        
800        
801        /**
802         * Gets the specified claim entry from this request.
803         *
804         * @param claimName The claim name. Must not be {@code null}.
805         *
806         * @return The claim entry, {@code null} if not found.
807         */
808        public Entry get(final String claimName) {
809                
810                return get(claimName, null);
811        }
812        
813        
814        /**
815         * Gets the specified claim entry from this request.
816         *
817         * @param claimName The claim name. Must not be {@code null}.
818         * @param langTag   The associated language tag, {@code null} if none.
819         *
820         * @return The claim entry, {@code null} if not found.
821         */
822        public Entry get(final String claimName, final LangTag langTag) {
823                
824                for (ClaimsSetRequest.Entry en: getEntries()) {
825                        if (claimName.equals(en.getClaimName()) && langTag == null && en.getLangTag() == null) {
826                                // No lang tag
827                                return en;
828                        } else if (claimName.equals(en.getClaimName()) && langTag != null && langTag.equals(en.getLangTag())) {
829                                // Matching lang tag
830                                return en;
831                        }
832                }
833                return null;
834        }
835        
836        
837        /**
838         * Deletes the specified claim from this request.
839         *
840         * @param claimName The claim name. Must not be {@code null}.
841         * @param langTag   The associated language tag, {@code null} if none.
842         *
843         * @return The updated claims set request.
844         */
845        public ClaimsSetRequest delete(final String claimName, final LangTag langTag) {
846                
847                Collection<ClaimsSetRequest.Entry> updatedEntries = new LinkedList<>();
848                
849                for (ClaimsSetRequest.Entry en: getEntries()) {
850                        if (claimName.equals(en.getClaimName()) && langTag == null && en.getLangTag() == null) {
851                                // don't copy
852                        } else if (claimName.equals(en.getClaimName()) && langTag != null && langTag.equals(en.getLangTag())) {
853                                // don't copy
854                        } else {
855                                updatedEntries.add(en);
856                        }
857                }
858                
859                return new ClaimsSetRequest(updatedEntries);
860        }
861        
862        
863        /**
864         * Deletes the specified claim from this request, in all existing
865         * language tag variations if any.
866         *
867         * @param claimName The claim name. Must not be {@code null}.
868         *
869         * @return The updated claims set request.
870         */
871        public ClaimsSetRequest delete(final String claimName) {
872                Collection<ClaimsSetRequest.Entry> updatedEntries = new LinkedList<>();
873                
874                for (ClaimsSetRequest.Entry en: getEntries()) {
875                        if (claimName.equals(en.getClaimName())) {
876                                // don't copy
877                        } else {
878                                updatedEntries.add(en);
879                        }
880                }
881                
882                return new ClaimsSetRequest(updatedEntries);
883        }
884        
885        
886        /**
887         * Returns the JSON object representation of this claims set request.
888         *
889         * <p>Example:
890         *
891         * <pre>
892         * {
893         *   "given_name": {"essential": true},
894         *   "nickname": null,
895         *   "email": {"essential": true},
896         *   "email_verified": {"essential": true},
897         *   "picture": null,
898         *   "http://example.info/claims/groups": null
899         * }
900         * </pre>
901         *
902         * @return The JSON object, empty if no claims are specified.
903         */
904        public JSONObject toJSONObject() {
905                JSONObject o = new JSONObject();
906                for (ClaimsSetRequest.Entry entry : entries) {
907                        Map.Entry<String, JSONObject> jsonObjectEntry = entry.toJSONObjectEntry();
908                        o.put(jsonObjectEntry.getKey(), jsonObjectEntry.getValue());
909                }
910                return o;
911        }
912        
913        
914        @Override
915        public String toJSONString() {
916                return toJSONObject().toJSONString();
917        }
918        
919        
920        @Override
921        public String toString() {
922                return toJSONString();
923        }
924        
925        
926        /**
927         * Parses an OpenID Connect claims set request from the specified JSON
928         * object representation.
929         *
930         * @param jsonObject The JSON object to parse. Must not be
931         *                   {@code null}.
932         *
933         * @return The claims set request.
934         *
935         * @throws ParseException If parsing failed.
936         */
937        public static ClaimsSetRequest parse(final JSONObject jsonObject)
938                throws ParseException {
939                
940                ClaimsSetRequest claimsRequest = new ClaimsSetRequest();
941
942                for (String key: jsonObject.keySet()) {
943                        
944                        if ("verified_claims".equals(key)) {
945                                // Implies nested VerifiedClaimsSetRequest, skip
946                                continue;
947                        }
948                        
949                        JSONObject value = JSONObjectUtils.getJSONObject(jsonObject, key, null);
950                        
951                        claimsRequest = claimsRequest.add(ClaimsSetRequest.Entry.parse(new AbstractMap.SimpleImmutableEntry<>(key, value)));
952                }
953                
954                return claimsRequest;
955        }
956        
957        
958        /**
959         * Parses an OpenID Connect claims set request from the specified JSON
960         * object string representation.
961         *
962         * @param json The JSON object string to parse. Must not be
963         *             {@code null}.
964         *
965         * @return The claims set request.
966         *
967         * @throws ParseException If parsing failed.
968         */
969        public static ClaimsSetRequest parse(final String json)
970                throws ParseException {
971                
972                return parse(JSONObjectUtils.parse(json));
973        }
974}