package pluginbase.config.datasource; import ninja.leaping.configurate.ConfigurationNode; import ninja.leaping.configurate.ConfigurationOptions; import ninja.leaping.configurate.commented.CommentedConfigurationNode; import ninja.leaping.configurate.commented.SimpleCommentedConfigurationNode; import ninja.leaping.configurate.loader.AbstractConfigurationLoader; import ninja.leaping.configurate.loader.AtomicFiles; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import pluginbase.config.SerializableConfig; import pluginbase.config.annotation.Comment; import pluginbase.config.field.Field; import pluginbase.config.field.FieldMap; import pluginbase.config.field.FieldMapper; import pluginbase.config.serializers.Serializer; import pluginbase.config.serializers.SerializerSet; import pluginbase.messages.Message; import pluginbase.messages.Messages; import pluginbase.messages.messaging.SendablePluginBaseException; import java.io.*; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Callable; import java.util.function.Predicate; import java.util.function.Supplier; import static java.nio.charset.StandardCharsets.UTF_8; /** * A nearly complete implementation of DataSource that implements the main features of Serializable-Config and acts as * a wrapper for the zml's Configurate lib. */ public abstract class AbstractDataSource implements DataSource { protected static final String LINE_SEPARATOR = System.lineSeparator(); @NotNull private final AbstractConfigurationLoader loader; @NotNull private final SerializerSet serializerSet; private final boolean commentsEnabled; /** * This builder class is used to properly configure and create a DataSource object. * <p/> * You may specify both a source and a sink for the storage medium or simply specify a File which counts as both. * <br/> * Once all the options are set for the storage medium, simply call {@link #build()} to create a DataSource tied * to the configured storage medium. */ public static abstract class Builder<T extends Builder> { protected Callable<BufferedReader> source; protected Callable<BufferedWriter> sink; private SerializerSet.Builder serializerSet; private SerializerSet alternateSet; protected boolean commentsEnabled = false; protected Builder() { } @NotNull @SuppressWarnings("unchecked") private T self() { return (T) this; } /** * A File can be set to be used as a data source and sink. * * @param file the file to be used as the data source and sink. * @return this builder. */ @NotNull public T setFile(@NotNull File file) { return setPath(file.toPath()); } /** * A Path can be set to be used as a data source and sink. * * @param path the path to be used as the data source and sink. * @return this builder. */ @NotNull public T setPath(@NotNull Path path) { this.source = () -> Files.newBufferedReader(path, UTF_8); this.sink = AtomicFiles.createAtomicWriterFactory(path, UTF_8); return self(); } /** * A URL can be set to be used as a data source. * * @param url A url to be used as the data source. * @return this builder. */ @NotNull public T setURL(@NotNull URL url) { this.source = () -> new BufferedReader(new InputStreamReader(url.openConnection().getInputStream())); return self(); } @NotNull public T setSource(@NotNull Callable<BufferedReader> source) { this.source = source; return self(); } @NotNull public T setSink(@NotNull Callable<BufferedWriter> sink) { this.sink = sink; return self(); } /** * Refer to the docs for {@link SerializerSet.Builder#addSerializer(Class, Supplier)}. * <br/> * <strong>Note:</strong> using this method will cause any SerializerSet provided through {@link #setAlternateSerializerSet(SerializerSet)} * to be replaced! */ @NotNull public <S> T addSerializer(@NotNull Class<S> clazz, @NotNull Supplier<Serializer<S>> serializer) { alternateSet = null; getSSBuilder().addSerializer(clazz, serializer); return self(); } /** * Refer to the docs for {@link SerializerSet.Builder#setFallbackSerializer(Supplier)}. * <br/> * <strong>Note:</strong> using this method will cause any SerializerSet provided through {@link #setAlternateSerializerSet(SerializerSet)} * to be replaced! */ public T setFallbackSerializer(@NotNull Supplier<Serializer<Object>> fallbackSerializer) { alternateSet = null; getSSBuilder().setFallbackSerializer(fallbackSerializer); return self(); } /** * Refer to the docs for {@link SerializerSet.Builder#addOverrideSerializer(Class, Supplier)}. * <br/> * <strong>Note:</strong> using this method will cause any SerializerSet provided through {@link #setAlternateSerializerSet(SerializerSet)} * to be replaced! */ @NotNull public <S> T addOverrideSerializer(@NotNull Class<S> clazz, @NotNull Supplier<Serializer<S>> serializer) { alternateSet = null; getSSBuilder().addOverrideSerializer(clazz, serializer); return self(); } /** * Refer to the docs for {@link SerializerSet.Builder#registerSerializeWithInstance(Class, Supplier)}. * <br/> * <strong>Note:</strong> using this method will cause any SerializerSet provided through {@link #setAlternateSerializerSet(SerializerSet)} * to be replaced! */ public <S extends Serializer> T registerSerializeWithInstance(@NotNull Class<S> serializerClass, @NotNull Supplier<S> serializer) { getSSBuilder().registerSerializeWithInstance(serializerClass, serializer); return self(); } /** * Refer to the docs for {@link SerializerSet.Builder#registerClassReplacement(Predicate, Class)}. * <br/> * <strong>Note:</strong> using this method will cause any SerializerSet provided through {@link #setAlternateSerializerSet(SerializerSet)} * to be replaced! */ public T registerClassReplacement(@NotNull Predicate<Class<?>> checker, @NotNull Class replacementClass) { getSSBuilder().registerClassReplacement(checker, replacementClass); return self(); } /** * Refer to the docs for {@link SerializerSet.Builder#unregisterClassReplacement(Class)}. * <br/> * <strong>Note:</strong> using this method will cause any SerializerSet provided through {@link #setAlternateSerializerSet(SerializerSet)} * to be replaced! */ public T unregisterClassReplacement(@NotNull Class replacementClass) { getSSBuilder().unregisterClassReplacement(replacementClass); return self(); } private SerializerSet.Builder getSSBuilder() { if (serializerSet == null) { serializerSet = SerializerSet.builder(getDataSourceDefaultSerializerSet()); } return serializerSet; } /** * Normally a DataSource will be built with a copy of the {@link SerializerSet#defaultSet()}. This method * allows for specifying an alternate already built set. * <br/> * <strong>Note:</strong> some DataSource implementations may introduce custom serializers by default to handle * any special cases in their format. In this case, the implementation should provide a way to obtain a set * containing the special serializers so that a copy can be made if required. Additionally, the implementation * will typically add any special serializers to the set being built by this builder. * * @param serializerSet a replacement SerializerSet for the DataSource being built. * @return this builder object. */ @NotNull public T setAlternateSerializerSet(@NotNull SerializerSet serializerSet) { this.alternateSet = serializerSet; return self(); } @NotNull protected final SerializerSet getBuiltSerializerSet() { return alternateSet != null ? alternateSet : serializerSet != null ? serializerSet.build() : getDataSourceDefaultSerializerSet(); } /** * Returns the default SerializerSet to use for this DataSource type. * * @return the default SerializerSet to use for this DataSource type. */ protected SerializerSet getDataSourceDefaultSerializerSet() { return SerializerSet.defaultSet(); } /** * Sets whether or not the DataSource should write comments when saving data to a file. * <p/> * Comments can be added to a data object via {@link Comment}. * <p/> * <strong>Note:</strong> Comments are not available on all DataSource implementations. Enabling comments on an * implementation that does not support them will do nothing. * * @param commentsEnabled whether or not the DataSource should write comments when saving data to a file. * @return */ public T setCommentsEnabled(boolean commentsEnabled) { this.commentsEnabled = commentsEnabled; return self(); } /** * Creates the data source using the options of this builder. * * @return a new DataSource object usings the options specified in this builder. */ @NotNull public abstract AbstractDataSource build(); } protected AbstractDataSource(@NotNull AbstractConfigurationLoader loader, @NotNull SerializerSet serializerSet, boolean commentsEnabled) { this.loader = loader; this.serializerSet = serializerSet; this.commentsEnabled = commentsEnabled; } /** {@inheritDoc} */ @Nullable @Override public Object load() throws SendablePluginBaseException { try { ConfigurationNode node = getLoader().load(); Object value = node.getValue(); if (value == null) { return null; } return SerializableConfig.deserialize(value, serializerSet); } catch (IOException e) { throw new SendablePluginBaseException(Messages.EXCEPTION.bundle(e), e); } } /** {@inheritDoc} */ @Nullable @Override public <ObjectType> ObjectType load(Class<ObjectType> wantedType) throws SendablePluginBaseException { try { ConfigurationNode node = getLoader().load(); Object value = node.getValue(); if (value == null) { return null; } return SerializableConfig.deserializeAs(value, wantedType, serializerSet); } catch (IOException e) { throw new SendablePluginBaseException(Messages.EXCEPTION.bundle(e), e); } } /** {@inheritDoc} */ @Nullable @SuppressWarnings("unchecked") public <ObjectType> ObjectType loadToObject(@NotNull ObjectType destination) throws SendablePluginBaseException { try { Object value = getLoader().load().getValue(); if (value == null) { return null; } ObjectType source = SerializableConfig.deserializeAs(value, (Class<ObjectType>) destination.getClass(), serializerSet); if (destination.equals(source)) { return destination; } if (source != null) { destination = FieldMapper.mapFields(source, destination); return destination; } else { return null; } } catch (IOException e) { throw new SendablePluginBaseException(Messages.EXCEPTION.bundle(e), e); } } /** {@inheritDoc} */ @Override public void save(@NotNull Object object) throws SendablePluginBaseException { try { String header = getComments(object.getClass(), true); CommentedConfigurationNode node = SimpleCommentedConfigurationNode.root(ConfigurationOptions.defaults().setHeader(header)); Object serialized = SerializableConfig.serialize(object, serializerSet); node = node.setValue(serialized); if (commentsEnabled) { node = addComments(FieldMapper.getFieldMap(object.getClass()), node); } getLoader().save(node); } catch (IOException e) { throw new SendablePluginBaseException(Messages.EXCEPTION.bundle(e), e); } } @NotNull protected AbstractConfigurationLoader getLoader() { return loader; } @Nullable protected String getComments(@NotNull Class clazz, boolean header) { Comment comment = (Comment) clazz.getAnnotation(Comment.class); if (comment != null) { return joinComments(comment.value(), header); } return null; } protected String joinComments(String[] comments, boolean header) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < comments.length; i++) { if (i > 0) { if (header) { builder.append("\n"); } else { builder.append(LINE_SEPARATOR); } } builder.append(comments[i]); } return builder.toString(); } protected CommentedConfigurationNode addComments(@NotNull FieldMap fieldMap, @NotNull CommentedConfigurationNode node) { for (Map.Entry<Object, ? extends CommentedConfigurationNode> entry : node.getChildrenMap().entrySet()) { Field field = fieldMap.getField(entry.getKey().toString()); if (field != null) { String[] comments = field.getComments(); if (comments != null) { entry.getValue().setComment(joinComments(comments, false)); } digDeeper(field, entry.getValue()); } } return node; } protected void digDeeper(@NotNull Field field, @NotNull CommentedConfigurationNode node) { if (node.hasMapChildren()) { addComments(field, node); } else if (node.hasListChildren()) { for (CommentedConfigurationNode item : node.getChildrenList()) { digDeeper(field, item); } } } }