AbstractUserProfileBean.java

package org.keycloak.forms.login.freemarker.model;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import jakarta.ws.rs.core.MultivaluedMap;

import org.keycloak.models.KeycloakSession;
import org.keycloak.userprofile.AttributeMetadata;
import org.keycloak.userprofile.AttributeValidatorMetadata;
import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.UserProfileProvider;

/**
 * Abstract base for Freemarker context bean providing informations about user profile to render dynamic or crafted forms.  
 * 
 * @author Vlastimil Elias <velias@redhat.com>
 */
public abstract class AbstractUserProfileBean {

    protected final MultivaluedMap<String, String> formData;
    protected UserProfile profile;
    protected List<Attribute> attributes;
    protected Map<String, Attribute> attributesByName;

    public AbstractUserProfileBean(MultivaluedMap<String, String> formData) {
        this.formData = formData;
    }
    
    /**
     * Subclass have to call this method at the end of constructor to init user profile data. 
     * 
     * @param session
     * @param writeableOnly if true then only writeable (no read-only) attributes are put into template, if false then all readable attributes are there 
     */
    protected void init(KeycloakSession session, boolean writeableOnly) {
        UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
        this.profile = createUserProfile(provider);
        this.attributes = toAttributes(profile.getAttributes().getReadable(), writeableOnly);
        if(this.attributes != null)
            this.attributesByName = attributes.stream().collect(Collectors.toMap((a) -> a.getName(), (a) -> a));        
    }

    /**
     * Create UserProfile instance of the relevant type. Is called from {@link #init(KeycloakSession, boolean)}.
     * 
     * @param provider to create UserProfile from
     * @return user profile instance
     */
    protected abstract UserProfile createUserProfile(UserProfileProvider provider);

    /**
     * Get attribute default values to be pre-filled into the form on first show.
     * 
     * @param name of the attribute
     * @return attribute default value (can be null)
     */
    protected abstract Stream<String> getAttributeDefaultValues(String name);

    /**
     * Get context the template is used for, so view can be customized for distinct contexts. 
     * 
     * @return name of the context
     */
    public abstract String getContext();
    
    /**
     * All attributes to be shown in form sorted by the configured GUI order. Useful to render dynamic form.
     * 
     * @return list of attributes
     */
    public List<Attribute> getAttributes() {
        return attributes;
    }
    
    /**
     * Get map of all attributes where attribute name is key. Useful to render crafted form.
     * 
     * @return map of attributes by name
     */
    public Map<String, Attribute> getAttributesByName() {
        return attributesByName;
    }

    private List<Attribute> toAttributes(Map<String, List<String>> attributes, boolean writeableOnly) {
        if(attributes == null)
            return null;
        return attributes.keySet().stream().map(name -> profile.getAttributes().getMetadata(name)).filter((am) -> writeableOnly ? !profile.getAttributes().isReadOnly(am.getName()) : true).map(Attribute::new).sorted().collect(Collectors.toList());
    }

    /**
     * Info about user profile attribute available in Freemarker template. 
     */
    public class Attribute implements Comparable<Attribute> {

        private final AttributeMetadata metadata;

        public Attribute(AttributeMetadata metadata) {
            this.metadata = metadata;
        }

        public String getName() {
            return metadata.getName();
        }

        public String getDisplayName() {
            return metadata.getAttributeDisplayName();
        }

        public String getValue() {
            List<String> v = getValues();
            if (v == null || v.isEmpty()) {
                return null;
            } else {
                return v.get(0);
            }
        }
        
        public List<String> getValues() {
            List<String> v = formData != null ? formData.get(getName()) : null;
            if (v == null || v.isEmpty()) {
                Stream<String> vs = getAttributeDefaultValues(getName());
                if(vs == null)
                    return Collections.emptyList();
                else
                    return vs.collect(Collectors.toList());
            } else {
                return v;
            }
        }

        public boolean isRequired() {
            return profile.getAttributes().isRequired(getName());
        }

        public boolean isReadOnly() {
            return profile.getAttributes().isReadOnly(getName());
        }
        
        /** define value of the autocomplete attribute for html input tag. if null then no html input tag attribute is added */
        public String getAutocomplete() {
            if(getName().equals("email") || getName().equals("username"))
                return getName();
            else
                return null;    
            
        }

        public Map<String, Object> getAnnotations() {
            Map<String, Object> annotations = metadata.getAnnotations();

            if (annotations == null) {
                return Collections.emptyMap();
            }

            return annotations;
        }
      
        /**
         * Get info about validators applied to attribute.  
         * 
         * @return never null, map where key is validatorId and value is map with configuration for given validator (loaded from UserProfile configuration, never null)
         */
        public Map<String, Map<String, Object>> getValidators(){
            
            if(metadata.getValidators() == null) {
                return Collections.emptyMap();
            }
            return metadata.getValidators().stream().collect(Collectors.toMap(AttributeValidatorMetadata::getValidatorId, AttributeValidatorMetadata::getValidatorConfig));
        }

        public String getGroup() {
            if (metadata.getAttributeGroupMetadata() != null) {
                return metadata.getAttributeGroupMetadata().getName();
            }
            return null;
        }

        public String getGroupDisplayHeader() {
            if (metadata.getAttributeGroupMetadata() != null) {
                return metadata.getAttributeGroupMetadata().getDisplayHeader();
            }
            return null;
        }

        public String getGroupDisplayDescription() {
            if (metadata.getAttributeGroupMetadata() != null) {
                return metadata.getAttributeGroupMetadata().getDisplayDescription();
            }
            return null;
        }

        public Map<String, Object> getGroupAnnotations() {

            if ((metadata.getAttributeGroupMetadata() == null) || (metadata.getAttributeGroupMetadata().getAnnotations() == null)) {
                return Collections.emptyMap();
            }
            
            return metadata.getAttributeGroupMetadata().getAnnotations();
        }

        @Override
        public int compareTo(Attribute o) {
            return Integer.compare(metadata.getGuiOrder(), o.metadata.getGuiOrder());
        }
    }
}