001package com.nimbusds.openid.connect.sdk.claims;
002
003
004import java.net.MalformedURLException;
005import java.net.URL;
006import java.text.ParseException;
007import java.util.*;
008
009import javax.mail.internet.AddressException;
010import javax.mail.internet.InternetAddress;
011
012import net.minidev.json.JSONArray;
013import net.minidev.json.JSONObject;
014
015import com.nimbusds.langtag.LangTag;
016import com.nimbusds.langtag.LangTagUtils;
017
018import com.nimbusds.jwt.JWTClaimsSet;
019
020import com.nimbusds.oauth2.sdk.util.DateUtils;
021
022
023/**
024 * Claims set serialisable to a JSON object.
025 *
026 * @author Vladimir Dzhuvinov
027 */
028public abstract class ClaimsSet {
029
030
031        /**
032         * The JSON object representing the claims set.
033         */
034        private final JSONObject claims;
035
036
037        /**
038         * Creates a new empty claims set.
039         */
040        protected ClaimsSet() {
041
042                claims = new JSONObject();
043        }
044
045
046        /**
047         * Creates a new claims set from the specified JSON object.
048         *
049         * @param jsonObject The JSON object. Must not be {@code null}.
050         */
051        protected ClaimsSet(final JSONObject jsonObject) {
052
053                if (jsonObject == null)
054                        throw new IllegalArgumentException("The JSON object must not be null");
055
056                claims = jsonObject;
057        }
058
059
060        /**
061         * Puts all claims from the specified other claims set.
062         *
063         * @param other The other claims set. Must not be {@code null}.
064         */
065        public void putAll(final ClaimsSet other) {
066
067                claims.putAll(other.claims);
068        }
069
070
071        /**
072         * Gets a claim.
073         *
074         * @param name The claim name. Must not be {@code null}.
075         *
076         * @return The claim value, {@code null} if not specified.
077         */
078        public Object getClaim(final String name) {
079
080                return claims.get(name);
081        }
082
083
084        /**
085         * Gets a claim that casts to the specified class.
086         *
087         * @param name  The claim name. Must not be {@code null}.
088         * @param clazz The Java class that the claim value should cast to.
089         *              Must not be {@code null}.
090         *
091         * @return The claim value, {@code null} if not specified or casting
092         *         failed.
093         */
094        @SuppressWarnings("unchecked")
095        public <T> T getClaim(final String name, final Class<T> clazz) {
096
097                try {
098                        return (T)claims.get(name);
099
100                } catch (ClassCastException e) {
101
102                        return null;
103                }
104        }
105
106
107        /**
108         * Returns a map of all instances, including language-tagged, of a 
109         * claim with the specified base name.
110         *
111         * <p>Example JSON serialised claims set:
112         *
113         * <pre>
114         * {
115         *   "month"    : "January",
116         *   "month#de" : "Januar"
117         *   "month#es" : "enero",
118         *   "month#it" : "gennaio"
119         * }
120         * </pre>
121         *
122         * <p>The "month" claim instances as java.util.Map:
123         *
124         * <pre>
125         * null => "January" (no language tag)
126         * "de" => "Januar"
127         * "es" => "enero"
128         * "it" => "gennaio"
129         * </pre>
130         *
131         * @param name  The claim name. Must not be {@code null}.
132         * @param clazz The Java class that the claim values should cast to.
133         *              Must not be {@code null}.
134         *
135         * @return The matching language-tagged claim values, empty map if
136         *         none. A {@code null} key indicates the value has no language
137         *         tag (corresponds to the base name).
138         */
139        @SuppressWarnings("unchecked")
140        public <T> Map<LangTag,T> getLangTaggedClaim(final String name, final Class<T> clazz) {
141
142                Map<LangTag,Object> matches = LangTagUtils.find(name, claims);
143
144                Map<LangTag,T> out = new HashMap<LangTag,T>();
145
146                for (Map.Entry<LangTag,Object> entry: matches.entrySet()) {
147
148                        try {
149                                out.put(entry.getKey(), (T)entry.getValue());
150
151                        } catch (ClassCastException e) {
152                                // skip
153                        }
154                }
155
156                return out;
157        }
158
159
160        /**
161         * Sets a claim.
162         *
163         * @param name  The claim name, with an optional language tag. Must not
164         *              be {@code null}.
165         * @param value The claim value. Should serialise to a JSON entity. If
166         *              {@code null} any existing claim with the same name will
167         *              be removed.
168         */
169        public void setClaim(final String name, final Object value) {
170
171                if (value != null)
172                        claims.put(name, value);
173                else
174                        claims.remove(name);
175        }
176
177
178        /**
179         * Sets a claim with an optional language tag.
180         *
181         * @param name    The claim name. Must not be {@code null}.
182         * @param value   The claim value. Should serialise to a JSON entity.
183         *                If {@code null} any existing claim with the same name 
184         *                and language tag (if any) will be removed.
185         * @param langTag The language tag of the claim value, {@code null} if
186         *                not tagged.
187         */
188        public <T> void setClaim(final String name, final Object value, final LangTag langTag) {
189
190                String keyName = langTag != null ? name + "#" + langTag : name;
191                
192                setClaim(keyName, value);
193        }
194
195
196        /**
197         * Gets a string-based claim.
198         *
199         * @param name The claim name. Must not be {@code null}.
200         *
201         * @return The claim value, {@code null} if not specified or casting
202         *         failed.
203         */
204        public String getStringClaim(final String name) {
205
206                return getClaim(name, String.class);
207        }
208
209
210        /**
211         * Gets a string-based claim with an optional language tag.
212         *
213         * @param name    The claim name. Must not be {@code null}.
214         * @param langTag The language tag of the claim value, {@code null} to 
215         *                get the non-tagged value.
216         *
217         * @return The claim value, {@code null} if not specified or casting
218         *         failed.
219         */
220        public String getStringClaim(final String name, final LangTag langTag) {
221        
222                if (langTag == null)
223                        return getStringClaim(name);
224                else
225                        return getStringClaim(name + '#' + langTag);
226        }
227
228
229        /**
230         * Gets a boolean-based claim.
231         *
232         * @param name The claim name. Must not be {@code null}.
233         *
234         * @return The claim value, {@code null} if not specified or casting
235         *         failed.
236         */
237        public Boolean getBooleanClaim(final String name) {
238
239                return getClaim(name, Boolean.class);
240        }
241
242
243        /**
244         * Gets a number-based claim.
245         *
246         * @param name The claim name. Must not be {@code null}.
247         *
248         * @return The claim value, {@code null} if not specified or casting
249         *         failed.
250         */
251        public Number getNumberClaim(final String name) {
252
253                return getClaim(name, Number.class);
254        }
255
256
257        /**
258         * Gets an URL string based claim.
259         *
260         * @param name The claim name. Must not be {@code null}.
261         *
262         * @return The claim value, {@code null} if not specified or parsing
263         *         failed.
264         */
265        public URL getURLClaim(final String name) {
266
267                String value = getStringClaim(name);
268
269                if (value == null)
270                        return null;
271
272                try {
273                        return new URL(value);
274
275                } catch (MalformedURLException e) {
276
277                        return null;
278                }
279        }
280
281
282        /**
283         * Sets an URL string based claim.
284         *
285         * @param name  The claim name. Must not be {@code null}.
286         * @param value The claim value. If {@code null} any existing claim 
287         *              with the same name will be removed.
288         */
289        public void setURLClaim(final String name, final URL value) {
290
291                if (value != null)
292                        setClaim(name, value.toString());
293                else
294                        claims.remove(name);
295        }
296
297
298        /**
299         * Gets an email string based claim.
300         *
301         * @param name The claim name. Must not be {@code null}.
302         *
303         * @return The claim value, {@code null} if not specified or parsing
304         *         failed.
305         */
306        public InternetAddress getEmailClaim(final String name) {
307
308                String value = getStringClaim(name);
309
310                if (value == null)
311                        return null;
312
313                try {
314                        return new InternetAddress(value);
315
316                } catch (AddressException e) {
317
318                        return null;
319                }
320        }
321
322
323        /**
324         * Sets an email string based claim.
325         *
326         * @param name  The claim name. Must not be {@code null}.
327         * @param value The claim value. If {@code null} any existing claim 
328         *              with the same name will be removed.
329         */
330        public void setEmailClaim(final String name, final InternetAddress value) {
331
332                if (value != null)
333                        setClaim(name, value.getAddress());
334                else
335                        claims.remove(name);
336        }
337        
338        
339        /**
340         * Gets a date / time based claim, represented as the number of seconds 
341         * from 1970-01-01T0:0:0Z as measured in UTC until the date / time. 
342         * 
343         * @param name The claim name. Must not be {@code null}.
344         * 
345         * @return The claim value, {@code null} if not specified or parsing
346         *         failed.
347         */
348        public Date getDateClaim(final String name) {
349                
350                Number value = getNumberClaim(name);
351                
352                if (value == null)
353                        return null;
354                
355                try {
356                        return DateUtils.fromSecondsSinceEpoch(value.longValue());
357                        
358                } catch (Exception e) {
359                        
360                        return null;
361                }
362        }
363        
364        
365        /**
366         * Sets a date / time based claim, represented as the number of seconds 
367         * from 1970-01-01T0:0:0Z as measured in UTC until the date / time. 
368         * 
369         * @param name  The claim name. Must not be {@code null}.
370         * @param value The claim value. If {@code null} any existing claim
371         *              with the same name will be removed.
372         */
373        public void setDateClaim(final String name, final Date value) {
374                
375                if (value != null)
376                        setClaim(name, DateUtils.toSecondsSinceEpoch(value));
377                else
378                        claims.remove(name);
379        }
380
381
382        /**
383         * Gets a string list based claim.
384         *
385         * @param name The claim name. Must not be {@code null}.
386         *
387         * @return The claim value, {@code null} if not specified or parsing
388         *         failed.
389         */
390        public List<String> getStringListClaim(final String name) {
391
392                @SuppressWarnings("unchecked") List<Object> rawList = getClaim(name, List.class);
393
394                if (rawList == null)
395                        rawList = getClaim(name, JSONArray.class);
396
397                if (rawList == null)
398                        return null;
399
400                List<String> outputList = new ArrayList<String>(rawList.size());
401
402                for (Object item: rawList) {
403
404                        if (item != null)
405                                outputList.add(item.toString());
406                }
407
408                return outputList;
409        }
410        
411        
412        /**
413         * Gets the JSON object representation of this claims set.
414         *
415         * <p>Example:
416         * 
417         * <pre>
418         * {
419         *   "country"       : "USA",
420         *   "country#en"    : "USA",
421         *   "country#de_DE" : "Vereinigte Staaten",
422         *   "country#fr_FR" : "Etats Unis"
423         * }
424         * </pre>
425         *
426         * @return The JSON object representation.
427         */
428        public JSONObject toJSONObject() {
429        
430                return claims;
431        }
432
433
434        /**
435         * Gets the JSON Web Token (JWT) claims set for this claim set.
436         *
437         * @return The JWT claims set.
438         *
439         * @throws ParseException If the conversion to a JWT claims set fails.
440         */
441        public JWTClaimsSet toJWTClaimsSet()
442                throws ParseException {
443
444                return JWTClaimsSet.parse(claims);
445        }
446}