Custom data: splitting API and implementation


#1

Hello,

I'm willing to use custom data in the plugin I'm currently developping. I would like to split the API of my custom data from the implementation.

Therefore I first defined the interface for my custom data:

TransactionLogData.java

/*
 * Thinkpol is a Sponge plugin.
 * Copyright © 2017 Maël A
 *
 * This file is part of Thinpol.
 * 
 * Thinkpol is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Thinkpol is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Thinkpol.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

/*
 * Thinkpol use code and API from the SpongeAPI which is subject to the
 * following license:
 *
 * The MIT License (MIT)
 *
 * Copyright (c) SpongePowered <https://www.spongepowered.org>
 * Copyright (c) contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */

package tk.vendevant.thinkpol.api.data;

import org.spongepowered.api.data.manipulator.mutable.ListData;
import org.spongepowered.api.data.value.mutable.ListValue;

public interface TransactionLogData extends
	ListData<TransactionRecord,TransactionLogData,
						ImmutableTransactionLogData> {

	default ListValue<TransactionRecord> records() {
               return getListValue();
        }
}

Then I implemented it:

ThinkpolTransactionLogData.java

/*
 * Thinkpol is a Sponge plugin.
 * Copyright © 2017 Maël A
 *
 * This file is part of Thinpol.
 * 
 * Thinkpol is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Thinkpol is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Thinkpol.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

/*
 * Thinkpol use code and API from the SpongeAPI which is subject to the
 * following license:
 *
 * The MIT License (MIT)
 *
 * Copyright (c) SpongePowered <https://www.spongepowered.org>
 * Copyright (c) contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */

package tk.vendevant.thinkpol.data;

import org.spongepowered.api.data.manipulator.mutable.common.AbstractListData;
import org.spongepowered.api.data.key.Key;
import org.spongepowered.api.data.value.mutable.ListValue;
import org.spongepowered.api.data.DataHolder;
import org.spongepowered.api.data.merge.MergeFunction;
import org.spongepowered.api.data.DataContainer;
import org.spongepowered.api.data.DataView;
import org.spongepowered.api.data.persistence.AbstractDataBuilder;
import org.spongepowered.api.data.manipulator.DataManipulatorBuilder;
import org.spongepowered.api.data.persistence.InvalidDataException;

import tk.vendevant.thinkpol.api.data.TransactionRecord;
import tk.vendevant.thinkpol.api.data.TransactionLogData;
import tk.vendevant.thinkpol.api.data.ImmutableTransactionLogData;
import tk.vendevant.thinkpol.api.data.Keys;

import java.util.List;
import java.util.Optional;        
import java.util.Collections;

public class ThinkpolTransactionLogData
        extends AbstractListData<TransactionRecord,TransactionLogData,
                                                ImmutableTransactionLogData>
        implements TransactionLogData {

        public static final int CONTENT_VERSION = 0;

        public static final class Builder
                extends AbstractDataBuilder<TransactionLogData>
                implements DataManipulatorBuilder<TransactionLogData,
                                                ImmutableTransactionLogData> {

                /* records is actually useless but we could add a function
                 * build() (without any parameter) to build TransactionLogData
                 * with the value of records set in reset() and from(...) */
                private List<TransactionRecord> records;
                private static Builder instance;

                private Builder() {
                        super(TransactionLogData.class, CONTENT_VERSION);
                        reset();
                }

                public static Builder getInstance() {
                        if(instance == null)
                                instance = new Builder();
                        return instance;
                }

                @Override
                public TransactionLogData create() {
                        return new ThinkpolTransactionLogData(
                                                Collections.emptyList());
                }

                @Override
                public Optional<TransactionLogData> createFrom(
                        DataHolder dataHolder
                ) {
                        if(!dataHolder.supports(TransactionLogData.class))
                                return Optional.empty();

                        return create().fill(dataHolder);
                }

                @Override
                public Builder from(TransactionLogData value) {
                        /* The list is safely duplicated by the method asList()
                         * in AbstractListData */
                        records = value.asList();
                        return this;
                }

                @Override
                public Builder reset() {
                        records = Collections.emptyList();
                        return this;
                }

                @Override
                protected Optional<TransactionLogData> buildContent(
                        DataView container
                ) throws InvalidDataException {
                        /* We need to copy the DataView to transform it into a
                         * DataContainer since from(...) only accept
                         * DataContainer */
                        return create().from(container.copy());
                }
        }

        protected ThinkpolTransactionLogData(
                List<TransactionRecord> value
        ) {
                super(value, Keys.RECORDS);
        }

        @Override
        public ImmutableTransactionLogData asImmutable() {
                /* The List<TransactionRecord> is already safely duplicated
                 in the constructor of AbstractListData and in asList() */
                return new ThinkpolImmutableTransactionLogData(asList());
        }

        @Override
        public Optional<TransactionLogData> fill(
                DataHolder dataHolder,
                MergeFunction overlap
        ) {
                TransactionLogData merged =
                        overlap.merge(
                                this,
                                dataHolder.get(TransactionLogData.class)
                                                .orElse(null)
                        );
                    setElements(merged.records().get());
                return Optional.of(this);
        }

        @Override
        public Optional<TransactionLogData> from(
                DataContainer container
        ) {
                Optional<List<TransactionRecord>> records =
                        container.getObjectList(this.usedKey.getQuery(),
                                        TransactionRecord.class);

                if (records.isPresent()) {
                        setElements(records.get());
                        return Optional.of(this);
                }

                return Optional.empty();
        }

        @Override
        public TransactionLogData copy() {
                /* The List<TransactionRecord> is already safely duplicated
                 in the constructor of AbstractListData and in asList() */
                return new ThinkpolTransactionLogData(asList());
        }

        @Override
        public int getContentVersion() {
                return CONTENT_VERSION;
        }

        @Override
        public DataContainer toContainer() {
                return super.toContainer()
                        .set(this.usedKey, asList());
        }

}

For brevity, I have not included the source for the Immutable version of the manipulator.

Finally I registered it during the INITIALIZATION phase:

DataRegistration.builder()
			.dataClass(TransactionLogData.class)
			.immutableClass(ImmutableTransactionLogData.class)
			.manipulatorId("transaction_log")
			.dataName("Transaction Log")
			.builder(ThinkpolTransactionLogData.Builder.getInstance())
			.buildAndRegister(container);

It almost works but when the server shutdown I get the following error:

[13:59:01] [Server thread/ERROR] [FML]: A TileEntity type net.minecraft.tileentity.TileEntityBrewingStand has throw an exception trying to write state. It will not persist. Report this to the mod author
org.spongepowered.api.data.DataRegistrationNotFoundException: Could not locate a DataRegistration for class class tk.vendevant.thinkpol.data.ThinkpolTransactionLogData

Regarding the documentation which states:

You must reference the implementation classes if you have split the API from the implementaton.

this seems a normal error.

But now I can't do:

DataRegistration.builder()
			.dataClass(ThinkpolTransactionLogData.class)
			.immutableClass(ThinpolImmutableTransactionLogData.class)
			.manipulatorId("transaction_log")
			.dataName("Transaction Log")
			.builder(ThinkpolTransactionLogData.Builder.getInstance())
			.buildAndRegister(container);

because dataClass() takes as parameter a Class<D> with D extends DataManipulator<D,M> but ThinpolTransactionLogData extends DataManipulator<TransactionLogData,ImmutableTransactionLogData> by extending AbstractListData<TransactionRecord,TransactionLogData,ImmutableTransactionLogData> and implementing TransactionLogData.

Then I thought I could instead extend AbstractListData<TransactionRecord,ThinkpolTransactionLogData,ThinkpolImmutableTransactionLogData> while still implementing TransactionLogData but it's impossible since TransactionLogData extends ListData<TransactionRecord,TransactionLogData,ImmutableTransactionLogData> and extending AbstractListData<ThinkpolTransactionLogData,ThinkpolImmutableTransactionLogData> would make my class implement two times the same interface (ListData) with different types parameters which is forbidden by Java.

I looked at the source of SpongeCommons to know how Sponge itself is doing this but found that SpongeCommons use functions in the DataUtil class to register DataManipulators wich is not accessible from the SpongeAPI.

My question is: How one is supposed to split the API from the implementation of a custom data in a plugin ?


#2

@gabizou Any suggestion on this ?