package org.apereo.cas.util.serialization; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.PrettyPrinter; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.core.util.MinimalPrettyPrinter; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.google.common.base.Throwables; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.hjson.JsonValue; import org.hjson.Stringify; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.StringWriter; import java.io.Writer; import java.nio.charset.StandardCharsets; import java.util.stream.Collectors; /** * Generic class to serialize objects to/from JSON based on jackson. * * @author Misagh Moayyed * @param <T> the type parameter * @since 4.1 */ public abstract class AbstractJacksonBackedStringSerializer<T> implements StringSerializer<T> { private static final long serialVersionUID = -8415599777321259365L; private static final Logger LOGGER = LoggerFactory.getLogger(AbstractJacksonBackedStringSerializer.class); private static final int ABBREVIATE_MAX_WIDTH = 100; private final PrettyPrinter prettyPrinter; private final ObjectMapper objectMapper; /** * Instantiates a new Registered service json serializer. * Uses the {@link com.fasterxml.jackson.core.util.DefaultPrettyPrinter} for formatting. */ public AbstractJacksonBackedStringSerializer() { this(new DefaultPrettyPrinter()); } /** * Instantiates a new Registered service json serializer. * * @param prettyPrinter the pretty printer */ public AbstractJacksonBackedStringSerializer(final PrettyPrinter prettyPrinter) { this.objectMapper = initializeObjectMapper(); this.prettyPrinter = prettyPrinter; } /** * Instantiates a new Registered service json serializer. * * @param objectMapper the object mapper * @param prettyPrinter the pretty printer */ public AbstractJacksonBackedStringSerializer(final ObjectMapper objectMapper, final PrettyPrinter prettyPrinter) { this.objectMapper = objectMapper; this.prettyPrinter = prettyPrinter; } private boolean isJsonFormat() { return !(this.objectMapper.getFactory() instanceof YAMLFactory); } @Override public T from(final String json) { try { final String jsonString = isJsonFormat() ? JsonValue.readHjson(json).toString() : json; return readObjectFromJson(jsonString); } catch (final Exception e) { throw new IllegalArgumentException(e); } } @Override public T from(final File json) { try { final String jsonString = isJsonFormat() ? JsonValue.readHjson(FileUtils.readFileToString(json, StandardCharsets.UTF_8)).toString() : FileUtils.readFileToString(json, StandardCharsets.UTF_8); return readObjectFromJson(jsonString); } catch (final Exception e) { throw new IllegalArgumentException(e); } } @Override public T from(final Reader json) { try { final String jsonString = isJsonFormat() ? JsonValue.readHjson(json).toString() : IOUtils.readLines(json).stream().collect(Collectors.joining()); return readObjectFromJson(jsonString); } catch (final Exception e) { throw new IllegalArgumentException(e); } } @Override public T from(final InputStream json) { try { final String jsonString = isJsonFormat() ? JsonValue.readHjson(IOUtils.toString(json, StandardCharsets.UTF_8)).toString() : IOUtils.readLines(json, StandardCharsets.UTF_8).stream().collect(Collectors.joining("\n")); return readObjectFromJson(jsonString); } catch (final Exception e) { throw new IllegalArgumentException(e); } } @Override public void to(final OutputStream out, final T object) { try (StringWriter writer = new StringWriter()) { this.objectMapper.writer(this.prettyPrinter).writeValue(writer, object); final String hjsonString = isJsonFormat() ? JsonValue.readHjson(writer.toString()).toString(Stringify.HJSON) : writer.toString(); IOUtils.write(hjsonString, out, StandardCharsets.UTF_8); } catch (final Exception e) { throw new IllegalArgumentException(e); } } @Override public void to(final Writer out, final T object) { try (StringWriter writer = new StringWriter()) { this.objectMapper.writer(this.prettyPrinter).writeValue(writer, object); if (isJsonFormat()) { final Stringify opt = this.prettyPrinter instanceof MinimalPrettyPrinter ? Stringify.FORMATTED : Stringify.FORMATTED; JsonValue.readHjson(writer.toString()).writeTo(out, opt); } else { IOUtils.write(writer.toString(), out); } } catch (final Exception e) { throw new IllegalArgumentException(e); } } @Override public T from(final Writer writer) { return from(writer.toString()); } @Override public void to(final File out, final T object) { try (StringWriter writer = new StringWriter()) { this.objectMapper.writer(this.prettyPrinter).writeValue(writer, object); if (isJsonFormat()) { try (FileWriter fileWriter = new FileWriter(out); BufferedWriter buffer = new BufferedWriter(fileWriter)) { JsonValue.readHjson(writer.toString()).writeTo(buffer); buffer.flush(); fileWriter.flush(); } } else { FileUtils.write(out, writer.toString(), StandardCharsets.UTF_8); } } catch (final Exception e) { throw Throwables.propagate(e); } } /** * Initialize object mapper. * * @return the object mapper */ protected ObjectMapper initializeObjectMapper() { final ObjectMapper mapper = new ObjectMapper(getJsonFactory()); configureObjectMapper(mapper); return mapper; } /** * Configure mapper. * * @param mapper the mapper */ protected void configureObjectMapper(final ObjectMapper mapper) { mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); mapper.setVisibility(PropertyAccessor.SETTER, JsonAutoDetect.Visibility.PROTECTED_AND_PUBLIC); mapper.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.PROTECTED_AND_PUBLIC); mapper.setVisibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.PROTECTED_AND_PUBLIC); if (isDefaultTypingEnabled()) { mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); } mapper.findAndRegisterModules(); } protected boolean isDefaultTypingEnabled() { return true; } protected JsonFactory getJsonFactory() { return null; } /** * Gets type to serialize. * * @return the type to serialize */ protected abstract Class<T> getTypeToSerialize(); private T readObjectFromJson(final String jsonString) { try { return this.objectMapper.readValue(jsonString, getTypeToSerialize()); } catch (final Exception e) { LOGGER.error("Cannot read/parse JSON [{}] to deserialize into type [{}]. This may be caused " + "in the absence of a configuration/support module that knows how to interpret the JSON fragment, " + "specially if the fragment describes a CAS registered service definition. " + "Internal parsing error is [{}]", StringUtils.abbreviate(jsonString, ABBREVIATE_MAX_WIDTH), getTypeToSerialize(), e.getMessage()); } return null; } }