浏览代码

add json moshi support

Jason Rivard 3 年之前
父节点
当前提交
094c12c075

+ 1 - 0
build/checkstyle-import.xml

@@ -67,6 +67,7 @@
 
     <!-- gson -->
     <allow pkg="com.google.gson"/>
+    <allow pkg="com.squareup.moshi"/>
 
     <allow pkg="javax.management"/>
 

+ 5 - 0
server/pom.xml

@@ -311,6 +311,11 @@
             <artifactId>gson</artifactId>
             <version>2.8.7</version>
         </dependency>
+        <dependency>
+            <groupId>com.squareup.moshi</groupId>
+            <artifactId>moshi</artifactId>
+            <version>1.12.0</version>
+        </dependency>
         <dependency>
             <groupId>com.blueconic</groupId>
             <artifactId>browscap-java</artifactId>

+ 2 - 1
server/src/main/java/password/pwm/config/option/IdentityVerificationMethod.java

@@ -34,7 +34,8 @@ public enum IdentityVerificationMethod implements Serializable, ConfigurationOpt
     TOKEN( true, Display.Field_VerificationMethodToken, Display.Description_VerificationMethodToken ),
     OTP( true, Display.Field_VerificationMethodOTP, Display.Description_VerificationMethodOTP ),
     REMOTE_RESPONSES( false, Display.Field_VerificationMethodRemoteResponses, Display.Description_VerificationMethodRemoteResponses ),
-    OAUTH( true, Display.Field_VerificationMethodOAuth, Display.Description_VerificationMethodOAuth ),;
+    OAUTH( true, Display.Field_VerificationMethodOAuth, Display.Description_VerificationMethodOAuth ),
+    NAAF( false, Display.Value_Deprecated, Display.Value_Deprecated ),;
 
     private final transient boolean userSelectable;
     private final transient Display labelKey;

+ 6 - 1
server/src/main/java/password/pwm/config/value/RemoteWebServiceValue.java

@@ -212,6 +212,11 @@ public class RemoteWebServiceValue extends AbstractValue implements StoredValue
 
     @Override
     public Serializable toDebugJsonObject( final Locale locale )
+    {
+        return ( Serializable ) makeDebugJsonObject( locale );
+    }
+
+    private List<RemoteWebServiceConfiguration> makeDebugJsonObject( final Locale locale )
     {
         final ArrayList<RemoteWebServiceConfiguration> output = new ArrayList<>();
         for ( final RemoteWebServiceConfiguration remoteWebServiceConfiguration : values )
@@ -231,7 +236,7 @@ public class RemoteWebServiceValue extends AbstractValue implements StoredValue
     @Override
     public String toDebugString( final Locale locale )
     {
-        return JsonUtil.serialize( this.toDebugJsonObject( locale ) );
+        return JsonUtil.serializeCollection( this.makeDebugJsonObject( locale ) );
     }
 
 }

+ 5 - 5
server/src/main/java/password/pwm/config/value/StringValue.java

@@ -124,13 +124,13 @@ public class StringValue extends AbstractValue implements StoredValue
         {
             final String lCaseValue = value.toLowerCase( PwmConstants.DEFAULT_LOCALE );
             final List<String> reservedWords = DomainID.DOMAIN_RESERVED_WORDS;
-            final boolean contains = reservedWords.stream()
+            final Optional<String> reservedWordMatch = reservedWords.stream()
                     .map( String::toLowerCase )
-                    .anyMatch( lCaseValue::contains );
-            if ( contains )
+                    .filter( lCaseValue::contains )
+                    .findFirst();
+            if ( reservedWordMatch.isPresent() )
             {
-                return Collections.singletonList( "Domain ID is reserved word: '" + value + "'" );
-
+                return Collections.singletonList( "contains reserved word '" + reservedWordMatch.get() + "'" );
             }
         }
 

+ 3 - 1
server/src/main/java/password/pwm/i18n/Display.java

@@ -319,7 +319,9 @@ public enum Display implements PwmDisplayBundle
     Value_True,
     Value_NotApplicable,
     Value_Default,
-    Placeholder_Search,;
+    Placeholder_Search,
+
+    Value_Deprecated,;
 
     public static String getLocalizedMessage( final Locale locale, final Display key, final SettingReader config )
     {

+ 2 - 1
server/src/main/java/password/pwm/svc/event/CEFAuditFormatter.java

@@ -96,7 +96,8 @@ public class CEFAuditFormatter implements AuditFormatter
     {
         final AppConfig domainConfig = pwmApplication.getConfig();
         final Settings settings = Settings.fromConfiguration( domainConfig );
-        final Map<String, Object> auditRecordMap = JsonUtil.deserializeMap( JsonUtil.serialize( auditRecord ) );
+        final String auditRecordAsJson = JsonUtil.serialize( auditRecord );
+        final Map<String, Object> auditRecordMap = JsonUtil.deserializeMap( auditRecordAsJson );
 
         final Optional<String> srcHost = PwmApplication.deriveLocalServerHostname( pwmApplication.getConfig() );
 

+ 662 - 187
server/src/main/java/password/pwm/util/java/JsonUtil.java

@@ -30,8 +30,15 @@ import com.google.gson.JsonPrimitive;
 import com.google.gson.JsonSerializationContext;
 import com.google.gson.JsonSerializer;
 import com.google.gson.reflect.TypeToken;
+import com.squareup.moshi.JsonAdapter;
+import com.squareup.moshi.JsonReader;
+import com.squareup.moshi.JsonWriter;
+import com.squareup.moshi.Moshi;
+import com.squareup.moshi.Types;
+import org.jetbrains.annotations.Nullable;
 import password.pwm.PwmConstants;
 import password.pwm.bean.DomainID;
+import password.pwm.error.PwmInternalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.ldap.PwmLdapVendor;
 import password.pwm.util.PasswordData;
@@ -51,6 +58,7 @@ import java.util.Collection;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.TimeZone;
 import java.util.concurrent.atomic.LongAdder;
 
@@ -61,352 +69,819 @@ public class JsonUtil
     public enum Flag
     {
         PrettyPrint,
-        HtmlEscape,
     }
 
-    private static final Gson GENERIC_GSON = registerTypeAdapters( new GsonBuilder() )
-            .disableHtmlEscaping()
-            .create();
-
-    private static Gson getGson( final Flag... flags )
+    interface PwmJsonServiceProvider
     {
-        if ( flags == null || flags.length == 0 )
-        {
-            return GENERIC_GSON;
-        }
+        <T> T deserialize( String json, Class<T> classOfT );
 
-        final GsonBuilder gsonBuilder = registerTypeAdapters( new GsonBuilder() );
+        <T> T deserialize( String jsonString, TypeToken typeToken );
 
-        if ( !JavaHelper.enumArrayContainsValue( flags, Flag.HtmlEscape ) )
-        {
-            gsonBuilder.disableHtmlEscaping();
-        }
+        <T> T deserialize( String jsonString, Type type );
 
-        if ( JavaHelper.enumArrayContainsValue( flags, Flag.PrettyPrint ) )
-        {
-            gsonBuilder.setPrettyPrinting();
-        }
+        List<String> deserializeStringList( String jsonString );
+
+        Map<String, String> deserializeStringMap( String jsonString );
 
-        return gsonBuilder.create();
+        Map<String, Object> deserializeStringObjectMap( String jsonString );
+
+        Map<String, Object> deserializeMap( String jsonString );
+
+        <T> T deserialize( String jsonString, Class<T> classOfT, Type... parameterizedTypes );
+
+        String serialize( Serializable object, Flag... flags );
+
+        String serializeMap( Map object, Flag... flags );
+
+        String serializeCollection( Collection object, Flag... flags );
+
+        <T> T cloneUsingJson( Serializable srcObject, Class<T> classOfT );
     }
 
+    private static final PwmJsonServiceProvider GSON_PROVIDER = new GsonPwmJsonServiceProvider();
+    private static final PwmJsonServiceProvider MOSHI_PROVIDER = new MoshiPwmJsonServiceProvider();
+    private static final PwmJsonServiceProvider PROVIDER = GSON_PROVIDER;
+
     public static <T> T deserialize( final String jsonString, final TypeToken typeToken )
     {
-        return JsonUtil.getGson().fromJson( jsonString, typeToken.getType() );
+        return PROVIDER.deserialize( jsonString, typeToken );
     }
 
     public static <T> T deserialize( final String jsonString, final Type type )
     {
-        return JsonUtil.getGson().fromJson( jsonString, type );
+        return PROVIDER.deserialize( jsonString, type );
+    }
+
+    public static <T> T deserialize( final String json, final Class<T> classOfT )
+    {
+        return PROVIDER.deserialize( json, classOfT );
+    }
+
+    public static <T> T deserialize( final String json, final Class<T> classOfT, final Type... parameterizedTypes )
+    {
+        return PROVIDER.deserialize( json, classOfT, parameterizedTypes );
     }
 
     public static List<String> deserializeStringList( final String jsonString )
     {
-        return JsonUtil.getGson().fromJson( jsonString, new TypeToken<List<Object>>()
-        {
-        }.getType() );
+        return PROVIDER.deserializeStringList( jsonString );
     }
 
     public static Map<String, String> deserializeStringMap( final String jsonString )
     {
-        return JsonUtil.getGson().fromJson( jsonString, new TypeToken<Map<String, String>>()
-        {
-        }.getType() );
+        return PROVIDER.deserializeStringMap( jsonString );
     }
 
     public static Map<String, Object> deserializeStringObjectMap( final String jsonString )
     {
-        return JsonUtil.getGson().fromJson( jsonString, new TypeToken<Map<String, Object>>()
-        {
-        }.getType() );
+        return PROVIDER.deserializeStringObjectMap( jsonString );
     }
 
     public static Map<String, Object> deserializeMap( final String jsonString )
     {
-        return JsonUtil.getGson().fromJson( jsonString, new TypeToken<Map<String, Object>>()
-        {
-        }.getType() );
-    }
-
-    public static <T> T deserialize( final String json, final Class<T> classOfT )
-    {
-        return JsonUtil.getGson().fromJson( json, classOfT );
+        return PROVIDER.deserializeMap( jsonString );
     }
 
     public static String serialize( final Serializable object, final Flag... flags )
     {
-        return JsonUtil.getGson( flags ).toJson( object );
+        return PROVIDER.serialize( object, flags );
     }
 
     public static String serializeMap( final Map object, final Flag... flags )
     {
-        return JsonUtil.getGson( flags ).toJson( object );
+        return PROVIDER.serializeMap( object, flags );
     }
 
     public static String serializeCollection( final Collection object, final Flag... flags )
     {
-        return JsonUtil.getGson( flags ).toJson( object );
+        return PROVIDER.serializeCollection( object, flags );
     }
 
-    /**
-     * Gson Serializer for {@link java.security.cert.X509Certificate}.  Necessary because sometimes X509Certs have circular references
-     * and the default gson serializer will cause a {@code java.lang.StackOverflowError}.  Standard Base64 encoding of
-     * the cert is used as the json format.
-     */
-    private static class X509CertificateAdapter implements JsonSerializer<X509Certificate>, JsonDeserializer<X509Certificate>
+    public static <T> T cloneUsingJson( final Serializable srcObject, final Class<T> classOfT )
+    {
+        return PROVIDER.cloneUsingJson( srcObject, classOfT );
+    }
+
+    private static class MoshiPwmJsonServiceProvider implements PwmJsonServiceProvider
     {
-        private X509CertificateAdapter( )
+        private static final Moshi GENERIC_MOSHI = getMoshi();
+
+        private static Moshi getMoshi( final Flag... flags )
         {
+            if ( GENERIC_MOSHI != null && ( flags == null || flags.length <= 0 ) )
+            {
+                return GENERIC_MOSHI;
+            }
+
+            final Moshi.Builder moshiBuilder = new Moshi.Builder();
+            registerTypeAdapters( moshiBuilder );
+            return moshiBuilder.build();
+        }
+
+        private static <T> JsonAdapter<T> applyFlagsToAdapter( final JsonAdapter<T> adapter, final Flag... flags )
+        {
+            JsonAdapter<T> adapterInProgress = adapter;
+
+            if ( JavaHelper.enumArrayContainsValue( flags, Flag.PrettyPrint ) )
+            {
+                adapterInProgress = adapter.indent( "  " );
+            }
+
+            return adapterInProgress;
+        }
+
+        private static void registerTypeAdapters( final Moshi.Builder moshiBuilder, final Flag... flags )
+        {
+            moshiBuilder.add( Date.class, applyFlagsToAdapter( new DateTypeAdapter(), flags ) );
+            moshiBuilder.add( Instant.class, applyFlagsToAdapter( new InstantTypeAdapter(), flags ) );
+            moshiBuilder.add( X509Certificate.class, applyFlagsToAdapter( new X509CertificateAdapter(), flags ) );
+            moshiBuilder.add( PasswordData.class, applyFlagsToAdapter( new PasswordDataAdapter(), flags ) );
+            moshiBuilder.add( DomainID.class, applyFlagsToAdapter( new DomainIdAdaptor(), flags ) );
+            moshiBuilder.add( LongAdder.class, applyFlagsToAdapter( new LongAdderTypeAdaptor(), flags ) );
         }
 
         @Override
-        public JsonElement serialize( final X509Certificate cert, final Type type, final JsonSerializationContext jsonSerializationContext )
+        public List<String> deserializeStringList( final String jsonString )
         {
+            final Moshi moshi = getMoshi();
+            final Type type = Types.newParameterizedType( List.class, String.class );
+            final JsonAdapter<List<String>> adapter = moshi.adapter( type );
+
             try
             {
-                return new JsonPrimitive( StringUtil.base64Encode( cert.getEncoded() ) );
+                return List.copyOf( Objects.requireNonNull( adapter.fromJson( jsonString ) ) );
             }
-            catch ( final PwmUnrecoverableException | CertificateEncodingException e )
+            catch ( final IOException e )
             {
-                throw new IllegalStateException( "unable to json-encode certificate: " + e.getMessage() );
+                throw new RuntimeException( e.getMessage() );
             }
         }
 
         @Override
-        public X509Certificate deserialize( final JsonElement jsonElement, final Type type, final JsonDeserializationContext jsonDeserializationContext )
-                throws JsonParseException
+        public <T> T deserialize( final String jsonString, final Type type )
         {
+            final Moshi moshi = getMoshi();
+            final JsonAdapter<T> adapter = moshi.adapter( type );
+
             try
             {
-                final CertificateFactory certificateFactory = CertificateFactory.getInstance( "X.509" );
-                return ( X509Certificate ) certificateFactory.generateCertificate( new ByteArrayInputStream( StringUtil.base64Decode(
-                        jsonElement.getAsString() ) ) );
+                return adapter.fromJson( jsonString );
             }
-            catch ( final Exception e )
+            catch ( final IOException e )
             {
-                throw new JsonParseException( "unable to parse x509certificate: " + e.getMessage() );
+                throw new RuntimeException( e.getMessage() );
             }
         }
-    }
 
-    /**
-     * GsonSerializer that stores dates in ISO 8601 format, with a deserialier that also reads local-platform format reading.
-     */
-    private static class DateTypeAdapter implements JsonSerializer<Date>, JsonDeserializer<Date>
-    {
-        private static final PwmDateFormat ISO_DATE_FORMAT = PwmDateFormat.newPwmDateFormat(
-                "yyyy-MM-dd'T'HH:mm:ss'Z'",
-                PwmConstants.DEFAULT_LOCALE,
-                TimeZone.getTimeZone( "Zulu" ) );
+        @Override
+        public Map<String, String> deserializeStringMap( final String jsonString )
+        {
+            final Type type = Types.newParameterizedType( Map.class, String.class, String.class );
+            return Map.copyOf( deserialize( jsonString, type ) );
+        }
+
+        @Override
+        public Map<String, Object> deserializeStringObjectMap( final String jsonString )
+        {
+            final Type type = Types.newParameterizedType( Map.class, String.class, String.class );
+            return Map.copyOf( deserialize( jsonString, type ) );
+        }
 
-        private DateFormat getGsonDateFormat()
+        @Override
+        public Map<String, Object> deserializeMap( final String jsonString )
         {
-            final DateFormat gsonDateFormat = DateFormat.getDateTimeInstance( DateFormat.DEFAULT, DateFormat.DEFAULT );
-            gsonDateFormat.setTimeZone( TimeZone.getDefault() );
-            return gsonDateFormat;
+            final Type type = Types.newParameterizedType( Map.class, String.class, Object.class );
+            return Map.copyOf( deserialize( jsonString, type ) );
         }
 
-        private DateTypeAdapter( )
+        @Override
+        public <T> T deserialize( final String jsonString, final Class<T> classOfT )
         {
+            final Type type = Types.supertypeOf( classOfT );
+            return deserialize( jsonString, type );
         }
 
         @Override
-        public JsonElement serialize( final Date date, final Type type, final JsonSerializationContext jsonSerializationContext )
+        public <T> T deserialize( final String jsonString, final TypeToken typeToken )
         {
-            return new JsonPrimitive( ISO_DATE_FORMAT.format( date.toInstant() ) );
+            final Type type = Types.newParameterizedType( typeToken.getRawType() );
+            return deserialize( jsonString, type );
         }
 
         @Override
-        public Date deserialize( final JsonElement jsonElement, final Type type, final JsonDeserializationContext jsonDeserializationContext )
+        public <T> T deserialize( final String jsonString, final Class<T> classOfT, final Type... parameterizedTypes )
         {
-            try
-            {
-                return Date.from( ISO_DATE_FORMAT.parse( jsonElement.getAsString() ) );
-            }
-            catch ( final ParseException e )
+            final Type type = Types.newParameterizedType( classOfT, parameterizedTypes );
+            return deserialize( jsonString, type );
+        }
+
+        private <T> String serialize( final T object, final Type type, final Flag... flags )
+        {
+            final Moshi moshi = getMoshi();
+            final JsonAdapter<T> jsonAdapter = applyFlagsToAdapter( moshi.adapter( type ), flags );
+            return jsonAdapter.toJson( object );
+        }
+
+        @Override
+        public String serialize( final Serializable object, final Flag... flags )
+        {
+            final Type type;
+            if ( object instanceof Collection )
             {
-                /* noop */
+                type = Collection.class;
             }
-
-            // for backwards compatibility
-            try
+            else if  ( object instanceof Map )
             {
-                return getGsonDateFormat().parse( jsonElement.getAsString() );
+                type = Map.class;
             }
-            catch ( final ParseException e )
+            else
             {
-                LOGGER.debug( () -> "unable to parse stored json Date.class timestamp '" + jsonElement.getAsString() + "' error: " + e.getMessage() );
-                throw new JsonParseException( e );
+                type = object.getClass();
             }
+
+            return serialize( object, type, flags );
         }
-    }
 
-    /**
-     * GsonSerializer that stores instants in ISO 8601 format, with a deserialier that also reads local-platform format reading.
-     */
-    private static class InstantTypeAdapter implements JsonSerializer<Instant>, JsonDeserializer<Instant>
-    {
-        private InstantTypeAdapter( )
+        @Override
+        public String serializeMap( final Map object, final Flag... flags )
         {
+            final Type type = Types.newParameterizedType( Map.class );
+            return serialize( object, type, flags );
         }
 
         @Override
-        public JsonElement serialize( final Instant instant, final Type type, final JsonSerializationContext jsonSerializationContext )
+        public String serializeCollection( final Collection object, final Flag... flags )
         {
-            return new JsonPrimitive( JavaHelper.toIsoDate( instant ) );
+            final Type type = Types.subtypeOf( Collection.class );
+            return serialize( object, type, flags );
         }
 
         @Override
-        public Instant deserialize( final JsonElement jsonElement, final Type type, final JsonDeserializationContext jsonDeserializationContext )
+        public <T> T cloneUsingJson( final Serializable srcObject, final Class<T> classOfT )
         {
-            try
+            final String jsonObj = this.serialize( srcObject );
+            return this.deserialize( jsonObj, classOfT );
+        }
+
+        /**
+         * GsonSerializer that stores instants in ISO 8601 format, with a deserializer that also reads local-platform format reading.
+         */
+        private static class InstantTypeAdapter extends JsonAdapter<Instant>
+        {
+            @Nullable
+            @Override
+            public Instant fromJson( final JsonReader reader ) throws IOException
             {
-                return JavaHelper.parseIsoToInstant( jsonElement.getAsString() );
+                final String strValue = reader.nextString();
+                if ( StringUtil.isEmpty( strValue ) )
+                {
+                    return null;
+                }
+                try
+                {
+                    return JavaHelper.parseIsoToInstant( strValue );
+                }
+                catch ( final Exception e )
+                {
+                    LOGGER.debug( () -> "unable to parse stored json Instant.class timestamp '" + strValue + "' error: " + e.getMessage() );
+                    throw new IOException( e );
+                }
             }
-            catch ( final Exception e )
+
+            @Override
+            public void toJson( final JsonWriter writer, @Nullable final Instant value ) throws IOException
             {
-                LOGGER.debug( () -> "unable to parse stored json Instant.class timestamp '" + jsonElement.getAsString() + "' error: " + e.getMessage() );
-                throw new JsonParseException( e );
+                if ( value == null )
+                {
+                    writer.jsonValue( "" );
+                }
+                else
+                {
+                    writer.jsonValue( JavaHelper.toIsoDate( value ) );
+                }
             }
         }
-    }
 
-    private static class ByteArrayToBase64TypeAdapter implements JsonSerializer<byte[]>, JsonDeserializer<byte[]>
-    {
-        @Override
-        public byte[] deserialize( final JsonElement json, final Type typeOfT, final JsonDeserializationContext context ) throws JsonParseException
+        /**
+         * GsonSerializer that stores dates in ISO 8601 format, with a deserializer that also reads local-platform format reading.
+         */
+        private static class DateTypeAdapter extends JsonAdapter<Date>
         {
-            try
+            private static final PwmDateFormat ISO_DATE_FORMAT = PwmDateFormat.newPwmDateFormat(
+                    "yyyy-MM-dd'T'HH:mm:ss'Z'",
+                    PwmConstants.DEFAULT_LOCALE,
+                    TimeZone.getTimeZone( "Zulu" ) );
+
+            private static DateFormat getLegacyDateFormat()
             {
-                return StringUtil.base64Decode( json.getAsString() );
+                final DateFormat gsonDateFormat = DateFormat.getDateTimeInstance( DateFormat.DEFAULT, DateFormat.DEFAULT );
+                gsonDateFormat.setTimeZone( TimeZone.getDefault() );
+                return gsonDateFormat;
             }
-            catch ( final IOException e )
+
+            @Nullable
+            @Override
+            public Date fromJson( final JsonReader reader ) throws IOException
+            {
+                final String strValue = reader.nextString();
+                try
+                {
+                    return Date.from( ISO_DATE_FORMAT.parse( strValue ) );
+                }
+                catch ( final ParseException e )
+                {
+                    /* noop */
+                }
+
+                // for backwards compatibility
+                try
+                {
+                    return getLegacyDateFormat().parse( strValue );
+                }
+                catch ( final ParseException e )
+                {
+                    LOGGER.debug( () -> "unable to parse stored json Date.class timestamp '" + strValue + "' error: " + e.getMessage() );
+                    throw new IOException( e );
+                }
+            }
+
+            @Override
+            public void toJson( final JsonWriter writer, @Nullable final Date value ) throws IOException
             {
-                final String errorMsg = "io stream error while de-serializing byte array: " + e.getMessage();
-                LOGGER.error( () -> errorMsg );
-                throw new JsonParseException( errorMsg, e );
+                Objects.requireNonNull( value );
+                writer.value( ISO_DATE_FORMAT.format( value.toInstant() ) );
             }
         }
 
-        @Override
-        public JsonElement serialize( final byte[] src, final Type typeOfSrc, final JsonSerializationContext context )
+        private static class DomainIdAdaptor extends JsonAdapter<DomainID>
         {
-            try
+            @Nullable
+            @Override
+            public DomainID fromJson( final JsonReader reader ) throws IOException
             {
-                return new JsonPrimitive( StringUtil.base64Encode( src, StringUtil.Base64Options.GZIP ) );
+                final String stringValue = reader.nextString();
+
+                if ( StringUtil.isEmpty( stringValue ) )
+                {
+                    return null;
+                }
+
+                if ( DomainID.systemId().toString().equals( stringValue ) )
+                {
+                    return DomainID.systemId();
+                }
+
+                return DomainID.create( stringValue );
             }
-            catch ( final PwmUnrecoverableException e )
+
+            @Override
+            public void toJson( final JsonWriter writer, @Nullable final DomainID value ) throws IOException
             {
-                final String errorMsg = "error while JSON serializing byte array: " + e.getMessage();
-                LOGGER.error( () -> errorMsg );
-                throw new JsonParseException( errorMsg, e );
+                if ( value == null )
+                {
+                    writer.nullValue();
+
+                }
+                else
+                {
+                    writer.value( value.toString() );
+                }
             }
         }
-    }
 
-    private static class PasswordDataTypeAdapter implements JsonSerializer<PasswordData>, JsonDeserializer<PasswordData>
-    {
-        @Override
-        public PasswordData deserialize( final JsonElement json, final Type typeOfT, final JsonDeserializationContext context ) throws JsonParseException
+        /**
+         * Gson Serializer for {@link java.security.cert.X509Certificate}.  Necessary because sometimes X509Certs have circular references
+         * and the default gson serializer will cause a {@code java.lang.StackOverflowError}.  Standard Base64 encoding of
+         * the cert is used as the json format.
+         */
+        private static class X509CertificateAdapter extends JsonAdapter<X509Certificate>
         {
-            try
+            @Nullable
+            @Override
+            public X509Certificate fromJson( final JsonReader reader ) throws IOException
             {
-                return new PasswordData( json.getAsString() );
+                final String strValue = reader.nextString();
+                try
+                {
+                    final CertificateFactory certificateFactory = CertificateFactory.getInstance( "X.509" );
+                    try ( ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( StringUtil.base64Decode( strValue ) ) )
+                    {
+                        return ( X509Certificate ) certificateFactory.generateCertificate( byteArrayInputStream );
+                    }
+                }
+                catch ( final Exception e )
+                {
+                    throw new IOException( "unable to parse x509certificate: " + e.getMessage() );
+                }
             }
-            catch ( final PwmUnrecoverableException e )
+
+
+            @Override
+            public void toJson( final JsonWriter writer, @Nullable final X509Certificate value ) throws IOException
             {
-                final String errorMsg = "error while deserializing password data: " + e.getMessage();
-                LOGGER.error( () -> errorMsg );
-                throw new JsonParseException( errorMsg, e );
+                if ( value == null )
+                {
+                    writer.nullValue();
+                }
+                else
+                {
+                    try
+                    {
+                        final byte[] encoded = value.getEncoded();
+                        writer.value( StringUtil.stripAllWhitespace( StringUtil.base64Encode( encoded ) ) );
+                    }
+                    catch ( final PwmInternalException | CertificateEncodingException e )
+                    {
+                        throw new IOException( "unable to json-encode certificate: " + e.getMessage() );
+                    }
+                }
             }
         }
 
-        @Override
-        public JsonElement serialize( final PasswordData src, final Type typeOfSrc, final JsonSerializationContext context )
+
+        private static class PasswordDataAdapter extends JsonAdapter<PasswordData>
         {
-            try
+            @Nullable
+            @Override
+            public PasswordData fromJson( final JsonReader reader ) throws IOException
             {
-                return new JsonPrimitive( src.getStringValue() );
+                final String strValue = reader.nextString();
+                try
+                {
+                    return new PasswordData( strValue );
+                }
+                catch ( final PwmUnrecoverableException e )
+                {
+                    final String errorMsg = "error while deserializing password data: " + e.getMessage();
+                    LOGGER.error( () -> errorMsg );
+                    throw new IOException( errorMsg, e );
+                }
             }
-            catch ( final PwmUnrecoverableException e )
+
+            @Override
+            public void toJson( final JsonWriter writer, @Nullable final PasswordData value ) throws IOException
             {
-                final String errorMsg = "error while serializing password data: " + e.getMessage();
-                LOGGER.error( () -> errorMsg );
-                throw new JsonParseException( errorMsg, e );
+                Objects.requireNonNull( value );
+                try
+                {
+                    writer.value( value.getStringValue() );
+                }
+                catch ( final PwmUnrecoverableException e )
+                {
+                    final String errorMsg = "error while serializing password data: " + e.getMessage();
+                    LOGGER.error( () -> errorMsg );
+                    throw new IOException( errorMsg, e );
+                }
             }
         }
 
+        private static class LongAdderTypeAdaptor extends JsonAdapter<LongAdder>
+        {
+            @Nullable
+            @Override
+            public LongAdder fromJson( final JsonReader reader ) throws IOException
+            {
+                final String strValue = reader.nextString();
+                final long longValue = Long.parseLong( strValue );
+                final LongAdder longAddr = new LongAdder();
+                longAddr.add( longValue );
+                return longAddr;
+            }
+
+            @Override
+            public void toJson( final JsonWriter writer, @Nullable final LongAdder value ) throws IOException
+            {
+                Objects.requireNonNull( value );
+                writer.value( value.longValue() );
+            }
+        }
     }
 
-    private static class PwmLdapVendorTypeAdaptor implements JsonSerializer<PwmLdapVendor>, JsonDeserializer<PwmLdapVendor>
+    private static class GsonPwmJsonServiceProvider implements PwmJsonServiceProvider
     {
+        private static final Gson GENERIC_GSON = registerTypeAdapters( new GsonBuilder() )
+                .disableHtmlEscaping()
+                .create();
+
+        private static Gson getGson( final Flag... flags )
+        {
+            if ( flags == null || flags.length == 0 )
+            {
+                return GENERIC_GSON;
+            }
+
+            final GsonBuilder gsonBuilder = registerTypeAdapters( new GsonBuilder() );
+
+            if ( JavaHelper.enumArrayContainsValue( flags, Flag.PrettyPrint ) )
+            {
+                gsonBuilder.setPrettyPrinting();
+            }
+
+            return gsonBuilder.create();
+        }
+
         @Override
-        public PwmLdapVendor deserialize( final JsonElement json, final Type typeOfT, final JsonDeserializationContext context ) throws JsonParseException
+        public <T> T deserialize( final String jsonString, final TypeToken typeToken )
         {
-            return PwmLdapVendor.fromString( json.getAsString() );
+            return getGson().fromJson( jsonString, typeToken.getType() );
         }
 
         @Override
-        public JsonElement serialize( final PwmLdapVendor src, final Type typeOfSrc, final JsonSerializationContext context )
+        public <T> T deserialize( final String jsonString, final Type type )
         {
-            return new JsonPrimitive( src.name() );
+            return getGson().fromJson( jsonString, type );
+        }
+
+
+        @Override
+        public <T> T deserialize( final String json, final Class<T> classOfT, final Type... parameterizedTypes )
+        {
+            final TypeToken typeToken = TypeToken.getParameterized( classOfT, parameterizedTypes );
+            return getGson().fromJson( json, typeToken.getType() );
+        }
+
+        @Override
+        public <T> T deserialize( final String json, final Class<T> classOfT )
+        {
+            return getGson().fromJson( json, classOfT );
         }
-    }
 
-    private static class DomainIDTypeAdaptor implements JsonSerializer<DomainID>, JsonDeserializer<DomainID>
-    {
         @Override
-        public DomainID deserialize( final JsonElement json, final Type typeOfT, final JsonDeserializationContext context ) throws JsonParseException
+        public Map<String, String> deserializeStringMap( final String jsonString )
         {
-            final String sValue = json.getAsString();
-            if ( DomainID.systemId().toString().equals( sValue ) )
+            return Map.copyOf( getGson().fromJson( jsonString, new TypeToken<Map<String, String>>()
             {
-                return DomainID.systemId();
-            }
-            return DomainID.create( json.getAsString() );
+            }.getType() ) );
         }
 
         @Override
-        public JsonElement serialize( final DomainID src, final Type typeOfSrc, final JsonSerializationContext context )
+        public Map<String, Object> deserializeStringObjectMap( final String jsonString )
         {
-            return new JsonPrimitive( src.toString() );
+            return getGson().fromJson( jsonString, new TypeToken<Map<String, Object>>()
+            {
+            }.getType() );
         }
-    }
 
-    private static class LongAdderTypeAdaptor implements JsonSerializer<LongAdder>, JsonDeserializer<LongAdder>
-    {
         @Override
-        public LongAdder deserialize( final JsonElement json, final Type typeOfT, final JsonDeserializationContext context ) throws JsonParseException
+        public Map<String, Object> deserializeMap( final String jsonString )
         {
-            final long longValue = json.getAsLong();
-            final LongAdder longAddr = new LongAdder();
-            longAddr.add( longValue );
-            return longAddr;
+            return Map.copyOf( getGson().fromJson( jsonString, new TypeToken<Map<String, Object>>()
+            {
+            }.getType() ) );
         }
 
         @Override
-        public JsonElement serialize( final LongAdder src, final Type typeOfSrc, final JsonSerializationContext context )
+        public String serialize( final Serializable object, final Flag... flags )
         {
-            return new JsonPrimitive( src.longValue() );
+            return getGson( flags ).toJson( object );
         }
-    }
 
-    private static GsonBuilder registerTypeAdapters( final GsonBuilder gsonBuilder )
-    {
-        gsonBuilder.registerTypeAdapter( Date.class, new DateTypeAdapter() );
-        gsonBuilder.registerTypeAdapter( Instant.class, new InstantTypeAdapter() );
-        gsonBuilder.registerTypeAdapter( X509Certificate.class, new X509CertificateAdapter() );
-        gsonBuilder.registerTypeAdapter( byte[].class, new ByteArrayToBase64TypeAdapter() );
-        gsonBuilder.registerTypeAdapter( PasswordData.class, new PasswordDataTypeAdapter() );
-        gsonBuilder.registerTypeAdapter( PwmLdapVendorTypeAdaptor.class, new PwmLdapVendorTypeAdaptor() );
-        gsonBuilder.registerTypeAdapter( DomainID.class, new DomainIDTypeAdaptor() );
-        gsonBuilder.registerTypeAdapter( LongAdder.class, new LongAdderTypeAdaptor() );
-        return gsonBuilder;
-    }
+        @Override
+        public String serializeMap( final Map object, final Flag... flags )
+        {
+            return getGson( flags ).toJson( object );
+        }
 
-    public static <T> T cloneUsingJson( final Serializable srcObject, final Class<T> classOfT )
-    {
-        final String asJson = JsonUtil.serialize( srcObject );
-        return JsonUtil.deserialize( asJson, classOfT );
+        @Override
+        public String serializeCollection( final Collection object, final Flag... flags )
+        {
+            return getGson( flags ).toJson( object );
+        }
+
+        public List<String> deserializeStringList( final String jsonString )
+        {
+            return List.copyOf( getGson().fromJson( jsonString, new TypeToken<List<Object>>()
+            {
+            }.getType() ) );
+        }
+
+        public <T> T cloneUsingJson( final Serializable srcObject, final Class<T> classOfT )
+        {
+            final String asJson = JsonUtil.serialize( srcObject );
+            return JsonUtil.deserialize( asJson, classOfT );
+        }
+
+
+        /**
+         * Gson Serializer for {@link java.security.cert.X509Certificate}.  Necessary because sometimes X509Certs have circular references
+         * and the default gson serializer will cause a {@code java.lang.StackOverflowError}.  Standard Base64 encoding of
+         * the cert is used as the json format.
+         */
+        private static class X509CertificateAdapter implements JsonSerializer<X509Certificate>, JsonDeserializer<X509Certificate>
+        {
+            private X509CertificateAdapter( )
+            {
+            }
+
+            @Override
+            public JsonElement serialize( final X509Certificate cert, final Type type, final JsonSerializationContext jsonSerializationContext )
+            {
+                try
+                {
+                    return new JsonPrimitive( StringUtil.stripAllWhitespace( StringUtil.base64Encode( cert.getEncoded() ) ) );
+                }
+                catch ( final PwmInternalException | CertificateEncodingException e )
+                {
+                    throw new IllegalStateException( "unable to json-encode certificate: " + e.getMessage() );
+                }
+            }
+
+            @Override
+            public X509Certificate deserialize( final JsonElement jsonElement, final Type type, final JsonDeserializationContext jsonDeserializationContext )
+                    throws JsonParseException
+            {
+                try
+                {
+                    final CertificateFactory certificateFactory = CertificateFactory.getInstance( "X.509" );
+                    return ( X509Certificate ) certificateFactory.generateCertificate( new ByteArrayInputStream( StringUtil.base64Decode(
+                            jsonElement.getAsString() ) ) );
+                }
+                catch ( final Exception e )
+                {
+                    throw new JsonParseException( "unable to parse x509certificate: " + e.getMessage() );
+                }
+            }
+        }
+
+        /**
+         * GsonSerializer that stores dates in ISO 8601 format, with a deserializer that also reads local-platform format reading.
+         */
+        private static class DateTypeAdapter implements JsonSerializer<Date>, JsonDeserializer<Date>
+        {
+            private static final PwmDateFormat ISO_DATE_FORMAT = PwmDateFormat.newPwmDateFormat(
+                    "yyyy-MM-dd'T'HH:mm:ss'Z'",
+                    PwmConstants.DEFAULT_LOCALE,
+                    TimeZone.getTimeZone( "Zulu" ) );
+
+            private DateFormat getGsonDateFormat()
+            {
+                final DateFormat gsonDateFormat = DateFormat.getDateTimeInstance( DateFormat.DEFAULT, DateFormat.DEFAULT );
+                gsonDateFormat.setTimeZone( TimeZone.getDefault() );
+                return gsonDateFormat;
+            }
+
+            private DateTypeAdapter( )
+            {
+            }
+
+            @Override
+            public JsonElement serialize( final Date date, final Type type, final JsonSerializationContext jsonSerializationContext )
+            {
+                return new JsonPrimitive( ISO_DATE_FORMAT.format( date.toInstant() ) );
+            }
+
+            @Override
+            public Date deserialize( final JsonElement jsonElement, final Type type, final JsonDeserializationContext jsonDeserializationContext )
+            {
+                try
+                {
+                    return Date.from( ISO_DATE_FORMAT.parse( jsonElement.getAsString() ) );
+                }
+                catch ( final ParseException e )
+                {
+                    /* noop */
+                }
+
+                // for backwards compatibility
+                try
+                {
+                    return getGsonDateFormat().parse( jsonElement.getAsString() );
+                }
+                catch ( final ParseException e )
+                {
+                    LOGGER.debug( () -> "unable to parse stored json Date.class timestamp '" + jsonElement.getAsString() + "' error: " + e.getMessage() );
+                    throw new JsonParseException( e );
+                }
+            }
+        }
+
+        /**
+         * GsonSerializer that stores instants in ISO 8601 format, with a deserializer that also reads local-platform format reading.
+         */
+        private static class InstantTypeAdapter implements JsonSerializer<Instant>, JsonDeserializer<Instant>
+        {
+            private InstantTypeAdapter( )
+            {
+            }
+
+            @Override
+            public JsonElement serialize( final Instant instant, final Type type, final JsonSerializationContext jsonSerializationContext )
+            {
+                return new JsonPrimitive( JavaHelper.toIsoDate( instant ) );
+            }
+
+            @Override
+            public Instant deserialize( final JsonElement jsonElement, final Type type, final JsonDeserializationContext jsonDeserializationContext )
+            {
+                try
+                {
+                    return JavaHelper.parseIsoToInstant( jsonElement.getAsString() );
+                }
+                catch ( final Exception e )
+                {
+                    LOGGER.debug( () -> "unable to parse stored json Instant.class timestamp '" + jsonElement.getAsString() + "' error: " + e.getMessage() );
+                    throw new JsonParseException( e );
+                }
+            }
+        }
+
+        private static class PasswordDataTypeAdapter implements JsonSerializer<PasswordData>, JsonDeserializer<PasswordData>
+        {
+            @Override
+            public PasswordData deserialize( final JsonElement json, final Type typeOfT, final JsonDeserializationContext context ) throws JsonParseException
+            {
+                try
+                {
+                    return new PasswordData( json.getAsString() );
+                }
+                catch ( final PwmUnrecoverableException e )
+                {
+                    final String errorMsg = "error while deserializing password data: " + e.getMessage();
+                    LOGGER.error( () -> errorMsg );
+                    throw new JsonParseException( errorMsg, e );
+                }
+            }
+
+            @Override
+            public JsonElement serialize( final PasswordData src, final Type typeOfSrc, final JsonSerializationContext context )
+            {
+                try
+                {
+                    return new JsonPrimitive( src.getStringValue() );
+                }
+                catch ( final PwmUnrecoverableException e )
+                {
+                    final String errorMsg = "error while serializing password data: " + e.getMessage();
+                    LOGGER.error( () -> errorMsg );
+                    throw new JsonParseException( errorMsg, e );
+                }
+            }
+
+        }
+
+        private static class PwmLdapVendorTypeAdaptor implements JsonSerializer<PwmLdapVendor>, JsonDeserializer<PwmLdapVendor>
+        {
+            @Override
+            public PwmLdapVendor deserialize( final JsonElement json, final Type typeOfT, final JsonDeserializationContext context ) throws JsonParseException
+            {
+                return PwmLdapVendor.fromString( json.getAsString() );
+            }
+
+            @Override
+            public JsonElement serialize( final PwmLdapVendor src, final Type typeOfSrc, final JsonSerializationContext context )
+            {
+                return new JsonPrimitive( src.name() );
+            }
+        }
+
+        private static class DomainIDTypeAdaptor implements JsonSerializer<DomainID>, JsonDeserializer<DomainID>
+        {
+            @Override
+            public DomainID deserialize( final JsonElement json, final Type typeOfT, final JsonDeserializationContext context ) throws JsonParseException
+            {
+                final String sValue = json.getAsString();
+                if ( DomainID.systemId().toString().equals( sValue ) )
+                {
+                    return DomainID.systemId();
+                }
+                return DomainID.create( json.getAsString() );
+            }
+
+            @Override
+            public JsonElement serialize( final DomainID src, final Type typeOfSrc, final JsonSerializationContext context )
+            {
+                return new JsonPrimitive( src.toString() );
+            }
+        }
+
+        private static class LongAdderTypeAdaptor implements JsonSerializer<LongAdder>, JsonDeserializer<LongAdder>
+        {
+            @Override
+            public LongAdder deserialize( final JsonElement json, final Type typeOfT, final JsonDeserializationContext context ) throws JsonParseException
+            {
+                final long longValue = json.getAsLong();
+                final LongAdder longAddr = new LongAdder();
+                longAddr.add( longValue );
+                return longAddr;
+            }
+
+            @Override
+            public JsonElement serialize( final LongAdder src, final Type typeOfSrc, final JsonSerializationContext context )
+            {
+                return new JsonPrimitive( src.longValue() );
+            }
+        }
+
+        private static GsonBuilder registerTypeAdapters( final GsonBuilder gsonBuilder )
+        {
+            gsonBuilder.registerTypeAdapter( Date.class, new DateTypeAdapter() );
+            gsonBuilder.registerTypeAdapter( Instant.class, new InstantTypeAdapter() );
+            gsonBuilder.registerTypeAdapter( X509Certificate.class, new X509CertificateAdapter() );
+            gsonBuilder.registerTypeAdapter( PasswordData.class, new PasswordDataTypeAdapter() );
+            gsonBuilder.registerTypeAdapter( PwmLdapVendorTypeAdaptor.class, new PwmLdapVendorTypeAdaptor() );
+            gsonBuilder.registerTypeAdapter( DomainID.class, new DomainIDTypeAdaptor() );
+            gsonBuilder.registerTypeAdapter( LongAdder.class, new LongAdderTypeAdaptor() );
+            return gsonBuilder;
+        }
     }
 }

+ 2 - 2
server/src/main/java/password/pwm/util/java/StringUtil.java

@@ -25,6 +25,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.text.StringEscapeUtils;
 import password.pwm.PwmConstants;
 import password.pwm.error.PwmError;
+import password.pwm.error.PwmInternalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.logging.PwmLogger;
 
@@ -330,7 +331,6 @@ public abstract class StringUtil
 
 
     public static String base64Encode( final byte[] input, final StringUtil.Base64Options... options )
-            throws PwmUnrecoverableException
     {
         final byte[] compressedBytes;
         if ( JavaHelper.enumArrayContainsValue( options, Base64Options.GZIP ) )
@@ -341,7 +341,7 @@ public abstract class StringUtil
             }
             catch ( final IOException e )
             {
-                throw PwmUnrecoverableException.convert( e );
+                throw new PwmInternalException( "unexpected error during base64 decoding: " + e, e );
             }
         }
         else

+ 18 - 18
server/src/main/resources/password/pwm/config/PwmSetting.xml

@@ -278,7 +278,7 @@
     <setting hidden="false" key="accountInfo.queryMatch" level="2" required="true">
         <ldapPermission actor="proxy" access="read"/>
         <default syntaxVersion="2">
-            <value>{type:"ldapAllUsers","ldapProfileID":"all"}</value>
+            <value>{"type":"ldapAllUsers","ldapProfileID":"all"}</value>
         </default>
     </setting>
     <setting hidden="false" key="display.passwordHistory" level="1" required="true">
@@ -445,7 +445,7 @@
     <setting hidden="false" key="password.allowChange.queryMatch" level="2" required="true">
         <ldapPermission actor="proxy" access="read"/>
         <default syntaxVersion="2">
-            <value>{type:"ldapAllUsers","ldapProfileID":"all"}</value>
+            <value>{"type":"ldapAllUsers","ldapProfileID":"all"}</value>
         </default>
     </setting>
     <setting hidden="false" key="logoutAfterPasswordChange" level="1" required="true">
@@ -1151,7 +1151,7 @@
     <setting hidden="false" key="password.policy.queryMatch" level="1">
         <ldapPermission actor="proxy" access="read"/>
         <default syntaxVersion="2">
-            <value>{type:"ldapAllUsers","ldapProfileID":"all"}</value>
+            <value>{"type":"ldapAllUsers","ldapProfileID":"all"}</value>
         </default>
     </setting>
     <setting hidden="false" key="password.policy.source" level="1" required="true">
@@ -1943,7 +1943,7 @@
     <setting hidden="false" key="otp.secret.allowSetup.queryMatch" level="1" required="true">
         <ldapPermission actor="proxy" access="read"/>
         <default syntaxVersion="2">
-            <value>{type:"ldapAllUsers","ldapProfileID":"all"}</value>
+            <value>{"type":"ldapAllUsers","ldapProfileID":"all"}</value>
         </default>
     </setting>
     <setting hidden="false" key="otp.secret.identifier" level="2" required="true">
@@ -2459,13 +2459,13 @@
     <setting hidden="false" key="challenge.allowSetup.queryMatch" level="2" required="true">
         <ldapPermission actor="proxy" access="read"/>
         <default syntaxVersion="2">
-            <value>{type:"ldapAllUsers","ldapProfileID":"all"}</value>
+            <value>{"type":"ldapAllUsers","ldapProfileID":"all"}</value>
         </default>
     </setting>
     <setting hidden="false" key="command.checkResponses.queryMatch" level="2" required="true">
         <ldapPermission actor="proxy" access="read"/>
         <default syntaxVersion="2">
-            <value>{type:"ldapAllUsers","ldapProfileID":"all"}</value>
+            <value>{"type":"ldapAllUsers","ldapProfileID":"all"}</value>
         </default>
     </setting>
     <setting hidden="true" key="challenge.profile.list" level="1">
@@ -2480,7 +2480,7 @@
     <setting hidden="false" key="challenge.policy.queryMatch" level="1">
         <ldapPermission actor="proxy" access="read"/>
         <default syntaxVersion="2">
-            <value>{type:"ldapAllUsers","ldapProfileID":"all"}</value>
+            <value>{"type":"ldapAllUsers","ldapProfileID":"all"}</value>
         </default>
     </setting>
     <setting hidden="false" key="recovery.verificationMethods" level="1">
@@ -2787,9 +2787,9 @@
         <flag>Form_ShowReadOnlyOption</flag>
         <flag>Form_ShowSource</flag>
         <default>
-            <value>{"name":"mail","minimumLength":1,"maximumLength":64,"type":"email","required":true,"confirmationRequired":false,"readonly":false,"unique":true,"labels":{"":"Email Address"},"regexErrors":{"":"Email Address has invalid characters"},"description":{"":""},"placeholder":"username@example.com","selectOptions":{},regex:"^[a-zA-Z0-9 .,'@]*$"}</value>
-            <value>{"name":"givenName","minimumLength":1,"maximumLength":64,"type":"text","required":true,"confirmationRequired":false,"readonly":false,"labels":{"":"First Name"},"regexErrors":{"":""},"description":{"":""},"selectOptions":{},regex:"^[a-zA-Z0-9 .,'@]*$"}</value>
-            <value>{"name":"sn","minimumLength":1,"maximumLength":64,"type":"text","required":true,"confirmationRequired":false,"readonly":false,"labels":{"":"Last Name"},"regexErrors":{"":""},"description":{"":""},"selectOptions":{},regex:"^[a-zA-Z0-9 .,'@]*$"}</value>
+            <value>{"name":"mail","minimumLength":1,"maximumLength":64,"type":"email","required":true,"confirmationRequired":false,"readonly":false,"unique":true,"labels":{"":"Email Address"},"regexErrors":{"":"Email Address has invalid characters"},"description":{"":""},"placeholder":"username@example.com","selectOptions":{},"regex":"^[a-zA-Z0-9 .,'@]*$"}</value>
+            <value>{"name":"givenName","minimumLength":1,"maximumLength":64,"type":"text","required":true,"confirmationRequired":false,"readonly":false,"labels":{"":"First Name"},"regexErrors":{"":""},"description":{"":""},"selectOptions":{},"regex":"^[a-zA-Z0-9 .,'@]*$"}</value>
+            <value>{"name":"sn","minimumLength":1,"maximumLength":64,"type":"text","required":true,"confirmationRequired":false,"readonly":false,"labels":{"":"Last Name"},"regexErrors":{"":""},"description":{"":""},"selectOptions":{},"regex":"^[a-zA-Z0-9 .,'@]*$"}</value>
         </default>
         <options>
             <option value="text">text</option>
@@ -3098,7 +3098,7 @@
     <setting hidden="false" key="updateAttributes.queryMatch" level="1" required="true">
         <ldapPermission actor="proxy" access="read"/>
         <default syntaxVersion="2">
-            <value>{type:"ldapAllUsers","ldapProfileID":"all"}</value>
+            <value>{"type":"ldapAllUsers","ldapProfileID":"all"}</value>
         </default>
     </setting>
     <setting hidden="false" key="updateAttributes.writeAttributes" level="1">
@@ -3231,7 +3231,7 @@
     <setting hidden="false" key="peopleSearch.queryMatch" level="1" required="true">
         <ldapPermission actor="self_other" access="read"/>
         <default syntaxVersion="2">
-            <value>{type:"ldapAllUsers","ldapProfileID":"all"}</value>
+            <value>{"type":"ldapAllUsers","ldapProfileID":"all"}</value>
         </default>
     </setting>
     <setting hidden="false" key="peopleSearch.search.form" level="1" required="true">
@@ -3343,7 +3343,7 @@
     </setting>
     <setting hidden="false" key="peopleSearch.photo.queryFilter" level="2">
         <default syntaxVersion="2">
-            <value>{type:"ldapAllUsers","ldapProfileID":"all"}</value>
+            <value>{"type":"ldapAllUsers","ldapProfileID":"all"}</value>
         </default>
     </setting>
     <setting hidden="false" key="peopleSearch.idleTimeout" level="1">
@@ -3751,13 +3751,13 @@
     <setting hidden="false" key="recovery.queryMatch" level="1" required="true">
         <ldapPermission actor="proxy" access="read"/>
         <default syntaxVersion="2">
-            <value>{type:"ldapAllUsers","ldapProfileID":"all"}</value>
+            <value>{"type":"ldapAllUsers","ldapProfileID":"all"}</value>
         </default>
         <default syntaxVersion="2" template="AD">
-            <value>{type:"ldapAllUsers","ldapProfileID":"all"}</value>
+            <value>{"type":"ldapAllUsers","ldapProfileID":"all"}</value>
         </default>
         <default syntaxVersion="2" template="ORACLE_DS">
-            <value>{type:"ldapAllUsers","ldapProfileID":"all"}</value>
+            <value>{"type":"ldapAllUsers","ldapProfileID":"all"}</value>
         </default>
     </setting>
     <setting hidden="false" key="helpdesk.token.sendMethod" level="1">
@@ -3903,7 +3903,7 @@
 
     <setting hidden="false" key="reporting.ldap.userMatch" level="1">
         <default syntaxVersion="2">
-            <value>{type:"ldapAllUsers","ldapProfileID":"all"}</value>
+            <value>{"type":"ldapAllUsers","ldapProfileID":"all"}</value>
         </default>
     </setting>
     <setting hidden="false" key="reporting.ldap.maxQuerySize" level="2">
@@ -4011,7 +4011,7 @@
             <value/>
         </default>
         <default syntaxVersion="2" template="NOVL_IDM">
-            <value>{type:"ldapAllUsers","ldapProfileID":"all"}</value>
+            <value>{"type":"ldapAllUsers","ldapProfileID":"all"}</value>
         </default>
     </setting>
     <setting hidden="false" key="webservices.thirdParty.queryMatch" level="2">

+ 1 - 0
server/src/main/resources/password/pwm/i18n/Display.properties

@@ -379,6 +379,7 @@ Value_False=False
 Value_True=True
 Value_NotApplicable=n/a
 Value_Default=Default
+Value_Deprecated=*Deprecated*
 Value_ProgressComplete=Complete
 Value_ProgressInProgress=In Progress
 Placeholder_Search=Search

+ 1 - 0
server/src/test/java/password/pwm/config/PwmSettingTest.java

@@ -60,6 +60,7 @@ public class PwmSettingTest
                 .build();
         for ( final PwmSetting pwmSetting : PwmSetting.values() )
         {
+            System.out.println( pwmSetting.name() + " " + pwmSetting.getKey()  );
             for ( final PwmSettingTemplateSet templateSet : PwmSettingTemplateSet.allValues() )
             {
                 final StoredValue storedValue = pwmSetting.getDefaultValue( templateSet );

+ 292 - 0
server/src/test/java/password/pwm/util/java/JsonUtilTest.java

@@ -0,0 +1,292 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2021 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.util.java;
+
+import org.junit.Assert;
+import org.junit.Test;
+import password.pwm.bean.DomainID;
+import password.pwm.config.value.data.ActionConfiguration;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.ldap.PwmLdapVendor;
+import password.pwm.util.PasswordData;
+import password.pwm.util.secure.X509Utils;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.LongAdder;
+
+
+public class JsonUtilTest
+{
+    @Test
+    public void deserializeStringListTest()
+    {
+        final String jsonValue = "[\"value1\",\"value2\",\"value3\"]";
+        final List<String> list = JsonUtil.deserializeStringList( jsonValue );
+
+        Assert.assertNotNull( list );
+        Assert.assertEquals( 3, list.size() );
+        Assert.assertEquals( "value1", list.get( 0 ) );
+        Assert.assertEquals( "value2", list.get( 1 ) );
+        Assert.assertEquals( "value3", list.get( 2 ) );
+
+        // verify returned collection is immutable
+        Assert.assertThrows( UnsupportedOperationException.class, () -> list.add( "new value" ) );
+    }
+
+    @Test
+    public void deserializeStringMapTest()
+    {
+        final String jsonValue = "{\"key1\":\"value1\",\"key2\":\"value2\",\"key3\":\"value3\"}";
+        final Map<String, String> map = JsonUtil.deserializeStringMap( jsonValue );
+
+        Assert.assertNotNull( map );
+        Assert.assertEquals( 3, map.size() );
+        Assert.assertEquals( "value1", map.get( "key1" ) );
+        Assert.assertEquals( "value2", map.get( "key2" ) );
+        Assert.assertEquals( "value3", map.get( "key3" ) );
+
+        // verify returned collection is immutable
+        Assert.assertThrows( UnsupportedOperationException.class, () -> map.put( "new key", "new value" ) );
+    }
+
+    @Test
+    public void deserializeMapTest()
+    {
+        final String jsonValue = "{\"key1\":\"value1\",\"key2\":\"value2\",\"key3\":\"value3\"}";
+        final Map<String, Object> map = JsonUtil.deserializeMap( jsonValue );
+
+        Assert.assertNotNull( map );
+        Assert.assertEquals( 3, map.size() );
+        Assert.assertEquals( "value1", map.get( "key1" ) );
+        Assert.assertEquals( "value2", map.get( "key2" ) );
+        Assert.assertEquals( "value3", map.get( "key3" ) );
+
+        // verify returned collection is immutable
+        Assert.assertThrows( UnsupportedOperationException.class, () -> map.put( "new key", "new value" ) );
+    }
+
+    @Test
+    public void deserializeObjectTest()
+    {
+        final String jsonValue = TestObject1.JSON_VALUE;
+        final TestObject1 testObject1 = JsonUtil.deserialize( jsonValue, TestObject1.class );
+
+        Assert.assertNotNull( testObject1 );
+        Assert.assertEquals( TestObject1.VALUE_STRING1, testObject1.getString1() );
+        Assert.assertEquals( TestObject1.VALUE_INSTANT1.toString(), testObject1.getInstant1().toString() );
+        Assert.assertEquals( TestObject1.VALUE_DATE1.toInstant().toString(), testObject1.getDate1().toInstant().toString() );
+        Assert.assertEquals( TestObject1.VALUE_LONG_ADDER1.longValue(), testObject1.getLongAdder1().longValue() );
+        Assert.assertEquals( TestObject1.VALUE_DOMAINID1, testObject1.getDomainId1() );
+        Assert.assertEquals( TestObject1.VALUE_PASSWORD_DATA1, testObject1.getPasswordData1() );
+        Assert.assertEquals( TestObject1.VALUE_X509_CERT1, testObject1.getCertificate1() );
+
+    }
+
+    @Test
+    public void serializeObjectTest()
+    {
+        final TestObject1 testObject1 = TestObject1.TEST_VALUE;
+
+        final String jsonValue = JsonUtil.serialize( testObject1 );
+
+        Assert.assertEquals( TestObject1.JSON_VALUE, jsonValue );
+    }
+
+    @Test
+    public void serializeTypeTest()
+    {
+        final List<ActionConfiguration> srcList = new ArrayList<>();
+        {
+            final List<ActionConfiguration.WebAction> webActions = List.of( ActionConfiguration.WebAction.builder()
+                    .password( "password" )
+                    .method( ActionConfiguration.WebMethod.get )
+                    .url( "https://www.example.com" )
+                    .body( "body" )
+                    .build() );
+            final ActionConfiguration actionConfiguration = ActionConfiguration.builder()
+                    .name( "action1" )
+                    .description( "actionDescription" )
+                    .webActions( webActions )
+                    .build();
+            srcList.add( actionConfiguration );
+        }
+        final String json = JsonUtil.serializeCollection( srcList );
+
+        final List<ActionConfiguration> deserializedList = JsonUtil.deserialize( json, List.class, ActionConfiguration.class );
+
+        Assert.assertEquals( srcList, deserializedList );
+    }
+
+    public static class TestObject1 implements Serializable
+    {
+        static final X509Certificate VALUE_X509_CERT1;
+        static final Date VALUE_DATE1 = Date.from( Instant.parse( "2000-01-01T01:01:01Z" ) );
+        static final DomainID VALUE_DOMAINID1 = DomainID.create( "acme1" );
+        static final Instant VALUE_INSTANT1 = Instant.parse( "2000-01-01T01:01:01Z" );
+        static final PwmLdapVendor VALUE_LDAP_VENDOR1 = PwmLdapVendor.DIRECTORY_SERVER_389;
+        static final LongAdder VALUE_LONG_ADDER1;
+        static final PasswordData VALUE_PASSWORD_DATA1;
+        static final String VALUE_STRING1 = "stringValue1";
+
+        private static final String DATA_CERT1 = "MIIC1TCCAb2gAwIBAgIJAMIrQtIBUHNJMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV"
+                + "BAMTD3d3dy5leGFtcGxlLmNvbTAeFw0yMTA5MDUyMTQ2NDlaFw0zMTA5MDMyMTQ2"
+                + "NDlaMBoxGDAWBgNVBAMTD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB"
+                + "BQADggEPADCCAQoCggEBANaDkcpssTnKQ0BDLbMjIhU+b1vHBKiwHgBAdLkKEx0N"
+                + "e/5obMMy4TIecnvO/8y9eo7HEgi1Q9FB9PT+M/+YhfQ4glp8IgQBa8eL3e3MqklW"
+                + "1upWVotn4cXlpgDXIBCflR9v27r3svK5FXUc5Ge352aYbsLDJsdBiwWMHFrMjO3x"
+                + "V8OhT3vkuhgwcdCQtiVN+6GgB3Krkq/qOQtqdaRisVlqKyePhHSDyrHY1ZeQYIgR"
+                + "jYuhh+Pbrr/QMnKOIxNLOkainE68h+0R3LCYR+rb8Ex3CgsxdIBdmNBixh2k9EIm"
+                + "smA81D+at13bny8o7Jieeu2uY6dnGquD3YE4AfyiP0cCAwEAAaMeMBwwGgYDVR0R"
+                + "BBMwEYIPd3d3LmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBBQUAA4IBAQCmQw8I3N0p"
+                + "KhdaEdLn9jK+Md+PyJpca8WYdbNYzlis0Nxsp+V97+Rt5SHyxjS2mTY9tMUGAwiF"
+                + "3HNcTmPK2+yPx6TqELmJ4NfRG1bdZJ6OvFRZFVT5BeKWetUHZ8cf7J2+o5yU0V4o"
+                + "iKW2/l8jQIUVQDYlTwwfNUufWI9B4bSGf7gliFWnTaDtZ+JxP9oNOaWBqVeRcLFu"
+                + "QDcGP+kQTE0+FW4kP9/oTIjD2u2Jc4d0NcPa2hUDWyPS1OqcSPJYGngBmDo524Mv"
+                + "ye7akpMj/ywK4BEnZpl/1rO5pNMD7GIK8lST4OOycWs3vErybogF45JCp7enroTH"
+                + "UWSGBXG89MJR";
+
+        static
+        {
+            {
+                final LongAdder longAdder = new LongAdder();
+                longAdder.add( 9223372036854775807L );
+                VALUE_LONG_ADDER1 = longAdder;
+            }
+
+            try
+            {
+                VALUE_PASSWORD_DATA1 = PasswordData.forStringValue( "super-secret-password" );
+            }
+            catch ( final PwmUnrecoverableException e )
+            {
+                throw new RuntimeException( e );
+            }
+
+            try
+            {
+                VALUE_X509_CERT1 = X509Utils.certificateFromBase64( DATA_CERT1 );
+            }
+            catch ( final CertificateException | IOException e )
+            {
+                throw new RuntimeException( e );
+            }
+
+        }
+
+        static final String JSON_VALUE = "{"
+                + "\"certificate1\":\"" + DATA_CERT1 + "\"" + ","
+                + "\"date1\":\"" + VALUE_DATE1.toInstant().toString() + "\"" + ","
+                + "\"domainId1\":\"acme1\"" + ","
+                + "\"instant1\":\"" + VALUE_INSTANT1.toString() + "\"" + ","
+                + "\"ldapVendor1\":\"" + VALUE_LDAP_VENDOR1 + "\"" + ","
+                + "\"longAdder1\":9223372036854775807" + ","
+                + "\"passwordData1\":\"super-secret-password\"" + ","
+                + "\"string1\":\"" + VALUE_STRING1 + "\""
+                + "}";
+
+
+        static final TestObject1 TEST_VALUE = new TestObject1(
+                VALUE_X509_CERT1,
+                VALUE_DATE1,
+                VALUE_DOMAINID1,
+                VALUE_INSTANT1,
+                VALUE_LDAP_VENDOR1,
+                VALUE_LONG_ADDER1,
+                VALUE_PASSWORD_DATA1,
+                VALUE_STRING1
+        );
+
+        private final X509Certificate certificate1;
+        private final Date date1;
+        private final DomainID domainId1;
+        private final Instant instant1;
+        private final PwmLdapVendor ldapVendor1;
+        private final LongAdder longAdder1;
+        private final PasswordData passwordData1;
+        private final String string1;
+
+        @SuppressWarnings( "checkstyle:ParameterNumber" )
+        public TestObject1(
+                final X509Certificate certificate1,
+                final Date date1,
+                final DomainID domainId1,
+                final Instant instant1,
+                final PwmLdapVendor ldapVendor1,
+                final LongAdder longAdder1,
+                final PasswordData passwordData1,
+                final String string1
+        )
+        {
+            this.certificate1 = certificate1;
+            this.date1 = date1;
+            this.domainId1 = domainId1;
+            this.instant1 = instant1;
+            this.ldapVendor1 = ldapVendor1;
+            this.longAdder1 = longAdder1;
+            this.passwordData1 = passwordData1;
+            this.string1 = string1;
+        }
+
+        public X509Certificate getCertificate1()
+        {
+            return certificate1;
+        }
+
+        public Date getDate1()
+        {
+            return date1;
+        }
+
+        public DomainID getDomainId1()
+        {
+            return domainId1;
+        }
+
+        public Instant getInstant1()
+        {
+            return instant1;
+        }
+
+        public LongAdder getLongAdder1()
+        {
+            return longAdder1;
+        }
+
+        public PasswordData getPasswordData1()
+        {
+            return passwordData1;
+        }
+
+        public String getString1()
+        {
+            return string1;
+        }
+    }
+}
+

+ 28 - 0
server/src/test/java/password/pwm/util/java/StringUtilTest.java

@@ -24,6 +24,8 @@ import org.junit.Assert;
 import org.junit.Test;
 import password.pwm.util.secure.PwmRandom;
 
+import java.io.IOException;
+
 public class StringUtilTest
 {
     @Test
@@ -188,4 +190,30 @@ public class StringUtilTest
         final String input = "dsad(dsadaasds)dsdasdad";
         Assert.assertEquals( "dsad%28dsadaasds%29dsdasdad", StringUtil.urlPathEncode( input ) );
     }
+
+    @Test
+    public void base64EncodeTest() throws IOException
+    {
+        final byte[] input = new byte[500];
+        for ( int i = 0; i < 500; i++ )
+        {
+            input[i] = 65;
+        }
+
+        final String b64value = StringUtil.base32Encode( input );
+        final String expectedValue = "IFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKB"
+                        + "IFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKB"
+                        + "IFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKB"
+                        + "IFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKB"
+                        + "IFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKB"
+                        + "IFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKB"
+                        + "IFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKB"
+                        + "IFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKB"
+                        + "IFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKB"
+                        + "IFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKB"
+                        + "IFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKB"
+                        + "IFAUCQKB";
+
+        Assert.assertEquals( expectedValue, b64value );
+    }
 }