001package com.nimbusds.oauth2.sdk.client;
002
003
004import java.net.MalformedURLException;
005import java.net.URL;
006import java.util.HashMap;
007import java.util.HashSet;
008import java.util.LinkedHashSet;
009import java.util.LinkedList;
010import java.util.List;
011import java.util.Map;
012import java.util.Set;
013
014import javax.mail.internet.AddressException;
015import javax.mail.internet.InternetAddress;
016
017import net.minidev.json.JSONArray;
018import net.minidev.json.JSONObject;
019
020import com.nimbusds.langtag.LangTag;
021import com.nimbusds.langtag.LangTagUtils;
022
023import com.nimbusds.oauth2.sdk.GrantType;
024import com.nimbusds.oauth2.sdk.ParseException;
025import com.nimbusds.oauth2.sdk.ResponseType;
026import com.nimbusds.oauth2.sdk.Scope;
027import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
028import com.nimbusds.oauth2.sdk.id.SoftwareID;
029import com.nimbusds.oauth2.sdk.id.SoftwareVersion;
030import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
031
032
033/**
034 * Client metadata.
035 * 
036 * <p>Example client metadata, serialised to a JSON object:
037 * 
038 * <pre>
039 * {
040 *  "redirect_uris"             : ["https://client.example.org/callback",
041 *                                 "https://client.example.org/callback2"],
042 *  "client_name"                : "My Example Client",
043 *  "client_name#ja-Jpan-JP"     : "\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u540D",
044 *  "token_endpoint_auth_method" : "client_secret_basic",
045 *  "scope"                      : "read write dolphin",
046 *  "logo_uri"                   : "https://client.example.org/logo.png",
047 *  "jwks_uri"                   : "https://client.example.org/my_public_keys.jwks"
048 * }
049 * </pre>
050 * 
051 * <p>Related specifications:
052 *
053 * <ul>
054 *     <li>OAuth 2.0 Dynamic Client Registration Protocol 
055 *         (draft-ietf-oauth-dyn-reg-14), section 2.
056 * </ul>
057 * 
058 * @author Vladimir Dzhuvinov
059 */
060public class ClientMetadata {
061        
062        
063        /**
064         * Redirect URIs.
065         */
066        private Set<URL> redirectURIs;
067        
068        
069        /**
070         * The client OAuth 2.0 scope.
071         */
072        private Scope scope;
073        
074        
075        /**
076         * The expected OAuth 2.0 response types.
077         */
078        private Set<ResponseType> responseTypes;
079        
080        
081        /**
082         * The expected OAuth 2.0 grant types.
083         */
084        private Set<GrantType> grantTypes;
085        
086        
087        /**
088         * Administrator contacts for the client.
089         */
090        private List<InternetAddress> contacts;
091
092
093        /**
094         * The client name.
095         */
096        private Map<LangTag,String> nameEntries;
097
098
099        /**
100         * The client application logo.
101         */
102        private Map<LangTag,URL> logoURIEntries;
103        
104        
105        /**
106         * The client URI entries.
107         */
108        private Map<LangTag,URL> uriEntries;
109
110
111        /**
112         * The client policy for use of end-user data.
113         */
114        private Map<LangTag,URL> policyURIEntries;
115
116
117        /**
118         * The client terms of service.
119         */
120        private Map<LangTag,URL> tosURIEntries;
121        
122        
123        /**
124         * Token endpoint authentication method.
125         */
126        private ClientAuthenticationMethod authMethod;
127        
128        
129        /**
130         * URI for this client's JSON Web Key (JWK) set containing key(s) that
131         * are used in signing requests to the server and key(s) for encrypting
132         * responses.
133         */
134        private URL jwkSetURI;
135
136
137        /**
138         * Identifier for the OAuth 2.0 client software.
139         */
140        private SoftwareID softwareID;
141
142
143        /**
144         * Version identifier for the OAuth 2.0 client software.
145         */
146        private SoftwareVersion softwareVersion;
147
148
149        /**
150         * The custom metadata fields.
151         */
152        private JSONObject customFields;
153        
154        
155        /** 
156         * Creates a new OAuth 2.0 client metadata instance.
157         */
158        public ClientMetadata() {
159
160                nameEntries = new HashMap<LangTag,String>();
161                logoURIEntries = new HashMap<LangTag,URL>();
162                uriEntries = new HashMap<LangTag,URL>();
163                policyURIEntries = new HashMap<LangTag,URL>();
164                policyURIEntries = new HashMap<LangTag,URL>();
165                tosURIEntries = new HashMap<LangTag,URL>();
166                customFields = new JSONObject();
167        }
168        
169        
170        /**
171         * Creates a shallow copy of the specified OAuth 2.0 client metadata
172         * instance.
173         * 
174         * @param metadata The client metadata to copy. Must not be 
175         *                 {@code null}.
176         */
177        public ClientMetadata(final ClientMetadata metadata) {
178                
179                redirectURIs = metadata.redirectURIs;
180                scope = metadata.scope;
181                responseTypes = metadata.responseTypes;
182                grantTypes = metadata.grantTypes;
183                contacts = metadata.contacts;
184                nameEntries = metadata.nameEntries;
185                logoURIEntries = metadata.logoURIEntries;
186                uriEntries = metadata.uriEntries;
187                policyURIEntries = metadata.policyURIEntries;
188                tosURIEntries = metadata.tosURIEntries;
189                authMethod = metadata.authMethod;
190                jwkSetURI = metadata.jwkSetURI;
191                customFields = metadata.customFields;
192        }
193        
194        
195        /**
196         * Gets the redirection URIs for this client. Corresponds to the
197         * {@code redirect_uris} client metadata field.
198         *
199         * @return The redirection URIs, {@code null} if not specified.
200         */
201        public Set<URL> getRedirectionURIs() {
202        
203                return redirectURIs;
204        }
205        
206        
207        /**
208         * Sets the redirection URIs for this client. Corresponds to the
209         * {@code redirect_uris} client metadata field.
210         *
211         * @param redirectURIs The redirection URIs, {@code null} if not
212         *                     specified.
213         */
214        public void setRedirectionURIs(final Set<URL> redirectURIs) {
215        
216                this.redirectURIs = redirectURIs;
217        }
218        
219        
220        /**
221         * Gets the scope values that the client can use when requesting access 
222         * tokens. Corresponds to the {@code scope} client metadata field.
223         * 
224         * @return The scope, {@code null} if not specified.
225         */
226        public Scope getScope() {
227                
228                return scope;
229        }
230        
231        
232        /**
233         * Sets the scope values that the client can use when requesting access 
234         * tokens. Corresponds to the {@code scope} client metadata field.
235         * 
236         * @param scope The scope, {@code null} if not specified.
237         */
238        public void setScope(final Scope scope) {
239                
240                this.scope = scope;
241        }
242        
243        
244        /**
245         * Gets the expected OAuth 2.0 response types. Corresponds to the
246         * {@code response_types} client metadata field.
247         * 
248         * @return The response types, {@code null} if not specified.
249         */
250        public Set<ResponseType> getResponseTypes() {
251                
252                return responseTypes;
253        }
254        
255        
256        /**
257         * Sets the expected OAuth 2.0 response types. Corresponds to the
258         * {@code response_types} client metadata field.
259         * 
260         * @param responseTypes The response types, {@code null} if not 
261         *                      specified.
262         */
263        public void setResponseTypes(final Set<ResponseType> responseTypes) {
264                
265                this.responseTypes = responseTypes;
266        }
267        
268        
269        /**
270         * Gets the expected OAuth 2.0 grant types. Corresponds to the
271         * {@code grant_types} client metadata field.
272         * 
273         * @return The grant types, {@code null} if not specified.
274         */
275        public Set<GrantType> getGrantTypes() {
276                
277                return grantTypes;
278        }
279        
280        
281        /**
282         * Sets the expected OAuth 2.0 grant types. Corresponds to the
283         * {@code grant_types} client metadata field.
284         * 
285         * @param grantTypes The grant types, {@code null} if not specified.
286         */
287        public void setGrantTypes(final Set<GrantType> grantTypes) {
288                
289                this.grantTypes = grantTypes;
290        }
291        
292        
293        /**
294         * Gets the administrator contacts for the client. Corresponds to the
295         * {@code contacts} client metadata field.
296         *
297         * @return The administrator contacts, {@code null} if not specified.
298         */
299        public List<InternetAddress> getContacts() {
300
301                return contacts;
302        }
303
304
305        /**
306         * Sets the administrator contacts for the client. Corresponds to the
307         * {@code contacts} client metadata field.
308         *
309         * @param contacts The administrator contacts, {@code null} if not
310         *                 specified.
311         */
312        public void setContacts(final List<InternetAddress> contacts) {
313
314                this.contacts = contacts;
315        }
316        
317        
318        /**
319         * Gets the client name. Corresponds to the {@code client_name} client 
320         * metadata field, with no language tag.
321         *
322         * @return The client name, {@code null} if not specified.
323         */
324        public String getName() {
325
326                return getName(null);
327        }
328
329
330        /**
331         * Gets the client name. Corresponds to the {@code client_name} client
332         * metadata field, with an optional language tag.
333         *
334         * @param langTag The language tag of the entry, {@code null} to get 
335         *                the non-tagged entry.
336         *
337         * @return The client name, {@code null} if not specified.
338         */
339        public String getName(final LangTag langTag) {
340
341                return nameEntries.get(langTag);
342        }
343
344
345        /**
346         * Gets the client name entries. Corresponds to the {@code client_name}
347         * client metadata field.
348         *
349         * @return The client name entries, empty map if none.
350         */
351        public Map<LangTag,String> getNameEntries() {
352
353                return nameEntries;
354        }
355
356
357        /**
358         * Sets the client name. Corresponds to the {@code client_name} client
359         * metadata field, with no language tag.
360         *
361         * @param name The client name, {@code null} if not specified.
362         */
363        public void setName(final String name) {
364
365                nameEntries.put(null, name);
366        }
367
368
369        /**
370         * Sets the client name. Corresponds to the {@code client_name} client
371         * metadata field, with an optional language tag.
372         *
373         * @param name    The client name. Must not be {@code null}.
374         * @param langTag The language tag, {@code null} if not specified.
375         */
376        public void setName(final String name, final LangTag langTag) {
377
378                nameEntries.put(langTag, name);
379        }
380
381
382        /**
383         * Gets the client application logo. Corresponds to the 
384         * {@code logo_uri} client metadata field, with no language 
385         * tag.
386         *
387         * @return The logo URI, {@code null} if not specified.
388         */
389        public URL getLogoURI() {
390
391                return getLogoURI(null);
392        }
393
394
395        /**
396         * Gets the client application logo. Corresponds to the 
397         * {@code logo_uri} client metadata field, with an optional
398         * language tag.
399         *
400         * @return The logo URI, {@code null} if not specified.
401         */
402        public URL getLogoURI(final LangTag langTag) {
403
404                return logoURIEntries.get(langTag);
405        }
406
407
408        /**
409         * Gets the client application logo entries. Corresponds to the 
410         * {@code logo_uri} client metadata field.
411         *
412         * @return The logo URI entries, empty map if none.
413         */
414        public Map<LangTag,URL> getLogoURIEntries() {
415
416                return logoURIEntries;
417        }
418
419
420        /**
421         * Sets the client application logo. Corresponds to the 
422         * {@code logo_uri} client metadata field, with no language 
423         * tag.
424         *
425         * @param logoURI The logo URI, {@code null} if not specified.
426         */
427        public void setLogoURI(final URL logoURI) {
428
429                logoURIEntries.put(null, logoURI);
430        }
431
432
433        /**
434         * Sets the client application logo. Corresponds to the 
435         * {@code logo_uri} client metadata field, with an optional
436         * language tag.
437         *
438         * @param logoURI The logo URI. Must not be {@code null}.
439         * @param langTag The language tag, {@code null} if not specified.
440         */
441        public void setLogoURI(final URL logoURI, final LangTag langTag) {
442
443                logoURIEntries.put(langTag, logoURI);
444        }
445        
446        
447        /**
448         * Gets the client home page. Corresponds to the {@code client_uri} 
449         * client metadata field, with no language tag.
450         *
451         * @return The client URI, {@code null} if not specified.
452         */
453        public URL getURI() {
454
455                return getURI(null);
456        }
457
458
459        /**
460         * Gets the client home page. Corresponds to the {@code client_uri} 
461         * client metadata field, with an optional language tag.
462         *
463         * @return The client URI, {@code null} if not specified.
464         */
465        public URL getURI(final LangTag langTag) {
466
467                return uriEntries.get(langTag);
468        }
469
470
471        /**
472         * Gets the client home page entries. Corresponds to the 
473         * {@code client_uri} client metadata field.
474         *
475         * @return The client URI entries, empty map if none.
476         */
477        public Map<LangTag,URL> getURIEntries() {
478
479                return uriEntries;
480        }
481
482
483        /**
484         * Sets the client home page. Corresponds to the {@code client_uri} 
485         * client metadata field, with no language tag.
486         *
487         * @param uri The client URI, {@code null} if not specified.
488         */
489        public void setURI(final URL uri) {
490
491                uriEntries.put(null, uri);
492        }
493
494
495        /**
496         * Sets the client home page. Corresponds to the {@code client_uri} 
497         * client metadata field, with an optional language tag.
498         *
499         * @param uri     The URI. Must not be {@code null}.
500         * @param langTag The language tag, {@code null} if not specified.
501         */
502        public void setURI(final URL uri, final LangTag langTag) {
503
504                uriEntries.put(langTag, uri);
505        }
506        
507
508        /**
509         * Gets the client policy for use of end-user data. Corresponds to the 
510         * {@code policy_uri} client metadata field, with no language 
511         * tag.
512         *
513         * @return The policy URI, {@code null} if not specified.
514         */
515        public URL getPolicyURI() {
516
517                return getPolicyURI(null);
518        }
519
520
521        /**
522         * Gets the client policy for use of end-user data. Corresponds to the 
523         * {@code policy_url} client metadata field, with an optional
524         * language tag.
525         *
526         * @return The policy URI, {@code null} if not specified.
527         */
528        public URL getPolicyURI(final LangTag langTag) {
529
530                return policyURIEntries.get(langTag);
531        }
532
533
534        /**
535         * Gets the client policy entries for use of end-user data. 
536         * Corresponds to the {@code policy_uri} client metadata field.
537         *
538         * @return The policy URI entries, empty map if none.
539         */
540        public Map<LangTag,URL> getPolicyURIEntries() {
541
542                return policyURIEntries;
543        }
544
545
546        /**
547         * Sets the client policy for use of end-user data. Corresponds to the 
548         * {@code policy_uri} client metadata field, with no language 
549         * tag.
550         *
551         * @param policyURI The policy URI, {@code null} if not specified.
552         */
553        public void setPolicyURI(final URL policyURI) {
554
555                policyURIEntries.put(null, policyURI);
556        }
557
558
559        /**
560         * Sets the client policy for use of end-user data. Corresponds to the 
561         * {@code policy_uri} client metadata field, with an optional
562         * language tag.
563         *
564         * @param policyURI The policy URI. Must not be {@code null}.
565         * @param langTag   The language tag, {@code null} if not specified.
566         */
567        public void setPolicyURI(final URL policyURI, final LangTag langTag) {
568
569                policyURIEntries.put(langTag, policyURI);
570        }
571
572
573        /**
574         * Gets the client's terms of service. Corresponds to the 
575         * {@code tos_uri} client metadata field, with no language 
576         * tag.
577         *
578         * @return The terms of service URI, {@code null} if not specified.
579         */
580        public URL getTermsOfServiceURI() {
581
582                return getTermsOfServiceURI(null);
583        }
584
585
586        /**
587         * Gets the client's terms of service. Corresponds to the 
588         * {@code tos_uri} client metadata field, with an optional
589         * language tag.
590         *
591         * @return The terms of service URI, {@code null} if not specified.
592         */
593        public URL getTermsOfServiceURI(final LangTag langTag) {
594
595                return tosURIEntries.get(langTag);
596        }
597
598
599        /**
600         * Gets the client's terms of service entries. Corresponds to the 
601         * {@code tos_uri} client metadata field.
602         *
603         * @return The terms of service URI entries, empty map if none.
604         */
605        public Map<LangTag,URL> getTermsOfServiceURIEntries() {
606
607                return tosURIEntries;
608        }
609
610
611        /**
612         * Sets the client's terms of service. Corresponds to the 
613         * {@code tos_uri} client metadata field, with no language 
614         * tag.
615         *
616         * @param tosURI The terms of service URI, {@code null} if not 
617         *               specified.
618         */
619        public void setTermsOfServiceURI(final URL tosURI) {
620
621                tosURIEntries.put(null, tosURI);
622        }
623
624
625        /**
626         * Sets the client's terms of service. Corresponds to the 
627         * {@code tos_uri} client metadata field, with an optional
628         * language tag.
629         *
630         * @param tosURI  The terms of service URI. Must not be {@code null}.
631         * @param langTag The language tag, {@code null} if not specified.
632         */
633        public void setTermsOfServiceURI(final URL tosURI, final LangTag langTag) {
634
635                tosURIEntries.put(langTag, tosURI);
636        }
637        
638        
639        /**
640         * Gets the Token endpoint authentication method. Corresponds to the 
641         * {@code token_endpoint_auth_method} client metadata field.
642         *
643         * @return The Token endpoint authentication method, {@code null} if
644         *         not specified.
645         */
646        public ClientAuthenticationMethod getTokenEndpointAuthMethod() {
647
648                return authMethod;
649        }
650
651
652        /**
653         * Sets the Token endpoint authentication method. Corresponds to the 
654         * {@code token_endpoint_auth_method} client metadata field.
655         *
656         * @param authMethod The Token endpoint authentication  method, 
657         *                   {@code null} if not specified.
658         */
659        public void setTokenEndpointAuthMethod(final ClientAuthenticationMethod authMethod) {
660
661                this.authMethod = authMethod;
662        }
663        
664        
665        /**
666         * Gets the URI for this client's JSON Web Key (JWK) set containing 
667         * key(s) that are used in signing requests to the server and key(s) 
668         * for encrypting responses. Corresponds to the {@code jwks_uri} client 
669         * metadata field.
670         *
671         * @return The JWK set URI, {@code null} if not specified.
672         */
673        public URL getJWKSetURI() {
674
675                return jwkSetURI;
676        }
677
678
679        /**
680         * Sets the URI for this client's JSON Web Key (JWK) set containing 
681         * key(s) that are used in signing requests to the server and key(s) 
682         * for encrypting responses. Corresponds to the {@code jwks_uri} client 
683         * metadata field.
684         *
685         * @param jwkSetURI The JWK set URI, {@code null} if not specified.
686         */
687        public void setJWKSetURL(final URL jwkSetURI) {
688
689                this.jwkSetURI = jwkSetURI;
690        }
691
692
693        /**
694         * Gets the identifier for the OAuth 2.0 client software. Corresponds
695         * to the {@code software_id} client metadata field.
696         *
697         * @return The software identifier, {@code null} if not specified.
698         */
699        public SoftwareID getSoftwareID() {
700
701                return softwareID;
702        }
703
704
705        /**
706         * Sets the identifier for the OAuth 2.0 client software. Corresponds
707         * to the {@code software_id} client metadata field.
708         *
709         * @param softwareID The software identifier, {@code null} if not
710         *                   specified.
711         */
712        public void setSoftwareID(final SoftwareID softwareID) {
713
714                this.softwareID = softwareID;
715        }
716
717
718        /**
719         * Gets the version identifier for the OAuth 2.0 client software.
720         * Corresponds to the {@code software_version} client metadata field.
721         *
722         * @return The version identifier, {@code null} if not specified.
723         */
724        public SoftwareVersion getSoftwareVersion() {
725
726                return softwareVersion;
727        }
728
729
730        /**
731         * Sets the version identifier for the OAuth 2.0 client software.
732         * Corresponds to the {@code software_version} client metadata field.
733         *
734         * @param softwareVersion The version identifier, {@code null} if not
735         *                        specified.
736         */
737        public void setSoftwareVersion(final SoftwareVersion softwareVersion) {
738
739                this.softwareVersion = softwareVersion;
740        }
741
742
743        /**
744         * Gets the specified custom metadata field.
745         *
746         * @param name The field name. Must not be {@code null}.
747         *
748         * @return The field value, typically serialisable to a JSON entity,
749         *         {@code null} if none.
750         */
751        public Object getCustomField(final String name) {
752
753                return customFields.get(name);
754        }
755
756
757        /**
758         * Gets the custom metadata fields.
759         *
760         * @return The custom metadata fields, as a JSON object, empty object
761         *         if none.
762         */
763        public JSONObject getCustomFields() {
764
765                return customFields;
766        }
767
768
769        /**
770         * Sets the specified custom metadata field.
771         *
772         * @param name  The field name. Must not be {@code null}.
773         * @param value The field value. Should serialise to a JSON entity.
774         */
775        public void setCustomField(final String name, final Object value) {
776
777                customFields.put(name, value);
778        }
779
780
781        /**
782         * Sets the custom metadata fields.
783         *
784         * @param customFields The custom metadata fields, as a JSON object,
785         *                     empty object if none. Must not be {@code null}.
786         */
787        public void setCustomFields(final JSONObject customFields) {
788
789                if (customFields == null)
790                        throw new IllegalArgumentException("The custom fields JSON object must not be null");
791
792                this.customFields = customFields;
793        }
794        
795        
796        /**
797         * Applies the client metadata defaults where no values have been
798         * specified.
799         * 
800         * <ul>
801         *     <li>The response types default to {@code ["code"]}.
802         *     <li>The grant types default to {@code "authorization_code".}
803         *     <li>The client authentication method defaults to 
804         *         "client_secret_basic".
805         * </ul>
806         */
807        public void applyDefaults() {
808                
809                if (responseTypes == null) {
810                        responseTypes = new HashSet<ResponseType>();
811                        responseTypes.add(ResponseType.getDefault());
812                }
813                
814                if (grantTypes == null) {
815                        grantTypes = new HashSet<GrantType>();
816                        grantTypes.add(GrantType.AUTHORIZATION_CODE);
817                }
818                
819                if (authMethod == null) {
820                        authMethod = ClientAuthenticationMethod.getDefault();
821                }
822        }
823        
824        
825        /**
826         * Returns the JSON object representation of this client metadata,
827         * including any custom fields.
828         *
829         * @return The JSON object.
830         */
831        public JSONObject toJSONObject() {
832
833                return toJSONObject(true);
834        }
835
836
837        /**
838         * Returns the JSON object representation of this client metadata.
839         *
840         * @param includeCustomFields {@code true} to include any custom
841         *                            metadata fields, {@code false} to omit
842         *                            them.
843         *
844         * @return The JSON object.
845         */
846        public JSONObject toJSONObject(final boolean includeCustomFields) {
847
848                JSONObject o;
849
850                if (includeCustomFields)
851                        o = new JSONObject(customFields);
852                else
853                        o = new JSONObject();
854
855
856                if (redirectURIs != null) {
857
858                        JSONArray uriList = new JSONArray();
859
860                        for (URL uri: redirectURIs)
861                                uriList.add(uri.toString());
862
863                        o.put("redirect_uris", uriList);
864                }
865                
866                
867                if (scope != null)
868                        o.put("scope", scope.toString());
869                
870                
871                if (responseTypes != null) {
872                        
873                        JSONArray rtList = new JSONArray();
874                        
875                        for (ResponseType rt: responseTypes)
876                                rtList.add(rt.toString());
877                        
878                        o.put("response_types", rtList);
879                }
880                
881                
882                if (grantTypes != null) {
883                        
884                        JSONArray grantList = new JSONArray();
885                        
886                        for (GrantType grant: grantTypes)
887                                grantList.add(grant.toString());
888                        
889                        o.put("grant_types", grantList);
890                }
891
892
893                if (contacts != null) {
894
895                        JSONArray contactList = new JSONArray();
896
897                        for (InternetAddress email: contacts)
898                                contactList.add(email.toString());
899
900                        o.put("contacts", contactList);
901                }
902
903
904                if (! nameEntries.isEmpty()) {
905
906                        for (Map.Entry<LangTag,String> entry: nameEntries.entrySet()) {
907
908                                LangTag langTag = entry.getKey();
909                                String name = entry.getValue();
910                                
911                                if (name == null)
912                                        continue;
913
914                                if (langTag == null)
915                                        o.put("client_name", entry.getValue());
916                                else
917                                        o.put("client_name#" + langTag, entry.getValue());
918                        } 
919                }
920                
921                
922                if (! logoURIEntries.isEmpty()) {
923
924                        for (Map.Entry<LangTag,URL> entry: logoURIEntries.entrySet()) {
925
926                                LangTag langTag = entry.getKey();
927                                URL uri = entry.getValue();
928                                
929                                if (uri == null)
930                                        continue;
931
932                                if (langTag == null)
933                                        o.put("logo_uri", entry.getValue().toString());
934                                else
935                                        o.put("logo_uri#" + langTag, entry.getValue().toString());
936                        } 
937                }
938                
939                
940                if (! uriEntries.isEmpty()) {
941
942                        for (Map.Entry<LangTag,URL> entry: uriEntries.entrySet()) {
943
944                                LangTag langTag = entry.getKey();
945                                URL uri = entry.getValue();
946                                
947                                if (uri == null)
948                                        continue;
949
950                                if (langTag == null)
951                                        o.put("client_uri", entry.getValue().toString());
952                                else
953                                        o.put("client_uri#" + langTag, entry.getValue().toString());
954                        } 
955                }
956                
957                
958                if (! policyURIEntries.isEmpty()) {
959
960                        for (Map.Entry<LangTag,URL> entry: policyURIEntries.entrySet()) {
961
962                                LangTag langTag = entry.getKey();
963                                URL uri = entry.getValue();
964                                
965                                if (uri == null)
966                                        continue;
967
968                                if (langTag == null)
969                                        o.put("policy_uri", entry.getValue().toString());
970                                else
971                                        o.put("policy_uri#" + langTag, entry.getValue().toString());
972                        } 
973                }
974                
975                
976                if (! tosURIEntries.isEmpty()) {
977
978                        for (Map.Entry<LangTag,URL> entry: tosURIEntries.entrySet()) {
979
980                                LangTag langTag = entry.getKey();
981                                URL uri = entry.getValue();
982                                
983                                if (uri == null)
984                                        continue;
985
986                                if (langTag == null)
987                                        o.put("tos_uri", entry.getValue().toString());
988                                else
989                                        o.put("tos_uri#" + langTag, entry.getValue().toString());
990                        } 
991                }
992
993
994                if (authMethod != null)
995                        o.put("token_endpoint_auth_method", authMethod.toString());
996
997
998                if (jwkSetURI != null)
999                        o.put("jwks_uri", jwkSetURI.toString());
1000
1001
1002                if (softwareID != null)
1003                        o.put("software_id", softwareID.getValue());
1004
1005                if (softwareVersion != null)
1006                        o.put("software_version", softwareVersion.getValue());
1007
1008                return o;
1009        }
1010        
1011        
1012        /**
1013         * Parses an client metadata instance from the specified JSON object.
1014         *
1015         * @param jsonObject The JSON object to parse. Must not be  
1016         *                   {@code null}.
1017         *
1018         * @return The client metadata.
1019         *
1020         * @throws ParseException If the JSON object couldn't be parsed to a
1021         *                        client metadata instance.
1022         */
1023        public static ClientMetadata parse(final JSONObject jsonObject)
1024                throws ParseException {
1025
1026                // Copy JSON object, then parse
1027                return parseFromModifiableJSONObject(new JSONObject(jsonObject));
1028        }
1029
1030
1031        /**
1032         * Parses an client metadata instance from the specified JSON object.
1033         *
1034         * @param jsonObject The JSON object to parse, will be modified by
1035         *                   the parse routine. Must not be {@code null}.
1036         *
1037         * @return The client metadata.
1038         *
1039         * @throws ParseException If the JSON object couldn't be parsed to a
1040         *                        client metadata instance.
1041         */
1042        private static ClientMetadata parseFromModifiableJSONObject(final JSONObject jsonObject)
1043                throws ParseException {
1044
1045                ClientMetadata metadata = new ClientMetadata();
1046
1047                if (jsonObject.containsKey("redirect_uris")) {
1048
1049                        Set<URL> redirectURIs = new LinkedHashSet<URL>();
1050
1051                        for (String uriString: JSONObjectUtils.getStringArray(jsonObject, "redirect_uris")) {
1052
1053                                try {
1054                                        redirectURIs.add(new URL(uriString));
1055
1056                                } catch (MalformedURLException e) {
1057
1058                                        throw new ParseException("Invalid \"redirect_uris\" parameter: " +
1059                                                                  e.getMessage());
1060                                }
1061                        }
1062
1063                        metadata.setRedirectionURIs(redirectURIs);
1064                        jsonObject.remove("redirect_uris");
1065                }
1066                
1067                
1068                if (jsonObject.containsKey("scope")) {
1069                        metadata.setScope(Scope.parse(JSONObjectUtils.getString(jsonObject, "scope")));
1070                        jsonObject.remove("scope");
1071                }
1072                
1073                
1074                if (jsonObject.containsKey("response_types")) {
1075                        
1076                        Set<ResponseType> responseTypes = new LinkedHashSet<ResponseType>();
1077                        
1078                        for (String rt: JSONObjectUtils.getStringArray(jsonObject, "response_types")) {
1079                                
1080                                responseTypes.add(ResponseType.parse(rt));
1081                        }
1082                        
1083                        metadata.setResponseTypes(responseTypes);
1084                        jsonObject.remove("response_types");
1085                }
1086                
1087                
1088                if (jsonObject.containsKey("grant_types")) {
1089                        
1090                        Set<GrantType> grantTypes = new LinkedHashSet<GrantType>();
1091                        
1092                        for (String grant: JSONObjectUtils.getStringArray(jsonObject, "grant_types")) {
1093                                
1094                                grantTypes.add(new GrantType(grant));
1095                        }
1096                        
1097                        metadata.setGrantTypes(grantTypes);
1098                        jsonObject.remove("grant_types");
1099                }       
1100                
1101
1102                if (jsonObject.containsKey("contacts")) {
1103
1104                        List<InternetAddress> emailList = new LinkedList<InternetAddress>();
1105
1106                        for (String emailString: JSONObjectUtils.getStringArray(jsonObject, "contacts")) {
1107
1108                                try {
1109                                        emailList.add(new InternetAddress(emailString));
1110
1111                                } catch (AddressException e) {
1112
1113                                        throw new ParseException("Invalid \"contacts\" parameter: " +
1114                                                                 e.getMessage());
1115                                }
1116                        }
1117
1118                        metadata.setContacts(emailList);
1119                        jsonObject.remove("contacts");
1120                }
1121
1122                // Find lang-tagged client_name params
1123                Map<LangTag,Object> matches = LangTagUtils.find("client_name", jsonObject);
1124
1125                for (Map.Entry<LangTag,Object> entry: matches.entrySet()) {
1126
1127                        try {
1128                                metadata.setName((String)entry.getValue(), entry.getKey());
1129
1130                        } catch (ClassCastException e) {
1131
1132                                throw new ParseException("Invalid \"client_name\" (language tag) parameter");
1133                        }
1134
1135                        removeMember(jsonObject, "client_name", entry.getKey());
1136                }
1137
1138
1139                matches = LangTagUtils.find("logo_uri", jsonObject);
1140
1141                for (Map.Entry<LangTag,Object> entry: matches.entrySet()) {
1142
1143                        try {
1144                                metadata.setLogoURI(new URL((String)entry.getValue()), entry.getKey());
1145
1146                        } catch (Exception e) {
1147
1148                                throw new ParseException("Invalid \"logo_uri\" (language tag) parameter");
1149                        }
1150
1151                        removeMember(jsonObject, "logo_uri", entry.getKey());
1152                }
1153                
1154                
1155                matches = LangTagUtils.find("client_uri", jsonObject);
1156
1157                for (Map.Entry<LangTag,Object> entry: matches.entrySet()) {
1158
1159                        try {
1160                                metadata.setURI(new URL((String)entry.getValue()), entry.getKey());
1161
1162
1163                        } catch (Exception e) {
1164
1165                                throw new ParseException("Invalid \"client_uri\" (language tag) parameter");
1166                        }
1167
1168                        removeMember(jsonObject, "client_uri", entry.getKey());
1169                }
1170                
1171                
1172                matches = LangTagUtils.find("policy_uri", jsonObject);
1173
1174                for (Map.Entry<LangTag,Object> entry: matches.entrySet()) {
1175
1176                        try {
1177                                metadata.setPolicyURI(new URL((String)entry.getValue()), entry.getKey());
1178
1179                        } catch (Exception e) {
1180
1181                                throw new ParseException("Invalid \"policy_uri\" (language tag) parameter");
1182                        }
1183
1184                        removeMember(jsonObject, "policy_uri", entry.getKey());
1185                }
1186                
1187                
1188                matches = LangTagUtils.find("tos_uri", jsonObject);
1189
1190                for (Map.Entry<LangTag,Object> entry: matches.entrySet()) {
1191
1192                        try {
1193                                metadata.setTermsOfServiceURI(new URL((String)entry.getValue()), entry.getKey());
1194
1195                        } catch (Exception e) {
1196
1197                                throw new ParseException("Invalid \"tos_uri\" (language tag) parameter");
1198                        }
1199
1200                        removeMember(jsonObject, "tos_uri", entry.getKey());
1201                }
1202                
1203
1204                if (jsonObject.containsKey("token_endpoint_auth_method")) {
1205                        metadata.setTokenEndpointAuthMethod(new ClientAuthenticationMethod(
1206                                JSONObjectUtils.getString(jsonObject, "token_endpoint_auth_method")));
1207
1208                        jsonObject.remove("token_endpoint_auth_method");
1209                }
1210
1211                        
1212                if (jsonObject.containsKey("jwks_uri")) {
1213                        metadata.setJWKSetURL(JSONObjectUtils.getURL(jsonObject, "jwks_uri"));
1214                        jsonObject.remove("jwks_uri");
1215                }
1216
1217                if (jsonObject.containsKey("software_id")) {
1218                        metadata.setSoftwareID(new SoftwareID(JSONObjectUtils.getString(jsonObject, "software_id")));
1219                        jsonObject.remove("software_id");
1220                }
1221
1222                if (jsonObject.containsKey("software_version")) {
1223                        metadata.setSoftwareVersion(new SoftwareVersion(JSONObjectUtils.getString(jsonObject, "software_version")));
1224                        jsonObject.remove("software_version");
1225                }
1226
1227                // The remaining fields are custom
1228                metadata.customFields = jsonObject;
1229
1230                return metadata;
1231        }
1232
1233
1234        /**
1235         * Removes a JSON object member with the specified base name and
1236         * optional language tag.
1237         *
1238         * @param jsonObject The JSON object. Must not be {@code null}.
1239         * @param name       The base member name. Must not be {@code null}.
1240         * @param langTag    The language tag, {@code null} if none.
1241         */
1242        private static void removeMember(final JSONObject jsonObject, final String name, final LangTag langTag) {
1243
1244                if (langTag == null)
1245                        jsonObject.remove(name);
1246                else
1247                        jsonObject.remove(name + "#" + langTag);
1248        }
1249}