DataServicesDocumentFactory change docRef nullpointer

Hey :slight_smile:
we are currently updating to 2024.06 and there something changed in DataServicesDocument. Before this we used the Builder of it like this:

DataServicesDocument.builder()
				.kernelDocument(document)
				.createdBy(DUMMY_USER)
				.modifiedBy(DUMMY_USER)
				.createdAt(now)
				.modifiedAt(now)
				.modelName(document.getDocumentModelId())
				.docRef(new DocumentReference(ACCOUNTING_MODEL_NAME + "/" + DUMMY_MODEL_ID))
				.build();

and now we do:

return documentFactory.newDataServicesDocument(document);

But now we have the problem that I get a Nullpointer when creating a new DocumentSpec because the docRef is null.

So first we create a DocumentSpec mit a String which was a json. To do this we create a IDocument with documentSupport.convertJSONToDocument(ACCOUNTING_MODEL_NAME, new StringReader(data)). Then we use the new function documentFactory.newDataServicesDocument(document); to create a DataServicesDocument and then we use this DataServiceDocument in documentSupport.convertToDocumentSpec(dsDocument) to create a DocumentSpec and that is the point where we get a nullpointer. Because convertToDocumentSpec is creating a new DocumentSpec(dataServicesDocument.getMetadata().getDocRef(), document) And the dataServicesDocument.getMetadata().getDocRef() is not there (null)

What do we need to do to get the docRef in the metaData?

Here the full code of that:

package com.mgmtp.cosmo.accounting.a12.document;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.mgmtp.a12.dataservices.document.DataServicesDocument;
import com.mgmtp.a12.dataservices.document.DataServicesDocumentFactory;
import com.mgmtp.a12.dataservices.document.DocumentSpec;
import com.mgmtp.a12.dataservices.document.graph.DocumentGraph;
import com.mgmtp.a12.dataservices.document.support.DocumentSupport;
import com.mgmtp.a12.kernel.md.document.api.IDocument;
import com.mgmtp.cosmo.accounting.a12.document.mapper.AccountingCalculationMapper;
import com.mgmtp.cosmo.accounting.a12.document.to.AccountingCalculationRoot;
import com.mgmtp.cosmo.accounting.core.api.to.MonetaryAmountTO;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.zalando.jackson.datatype.money.MoneyModule;

import java.io.StringReader;
import java.util.Collections;
import java.util.List;

/**
 * Factory to create a {@link DocumentGraph} out of a list of {@link MonetaryAmountTO}s.
 * Can be used to transform the output of the accoutning service into an A12 data format.
 */
@Service
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class DocumentGraphFactory {

	private static final String ACCOUNTING_MODEL_NAME = "AccountingModel";

	private final ObjectMapper jsonMapper;
	private final DocumentSupport documentSupport;
	private final AccountingCalculationMapper calculationMapper;
	private final DataServicesDocumentFactory documentFactory;

	@Autowired
	public DocumentGraphFactory(DocumentSupport documentSupport, AccountingCalculationMapper calculationMapper, DataServicesDocumentFactory documentFactory) {
		this(new ObjectMapper()
						.registerModule(new MoneyModule())
						.registerModule(new JavaTimeModule()),
				documentSupport,
				calculationMapper,
				documentFactory);
	}

	/**
	 * Transform a list of {@link MonetaryAmountTO}s to an A12 {@link DocumentGraph}
	 *
	 * @param amounts to be converted
	 * @return {@link DocumentGraph} containing alle the data of the input {@param amounts}
	 */
	public DocumentGraph create(List<MonetaryAmountTO> amounts) {
		try {
			String data = serializeAmounts(amounts);
			DocumentSpec documentSpec = toDocumentSpec(data);
			return toDocumentGraph(documentSpec);
		} catch (JsonProcessingException e) {
			throw new IllegalArgumentException("Could not serialize given amounts.", e);
		}
	}

	private String serializeAmounts(List<MonetaryAmountTO> amounts) throws JsonProcessingException {
		AccountingCalculationRoot wrapper = calculationMapper.map(amounts);
		return jsonMapper.writeValueAsString(wrapper);
	}

	private DocumentSpec toDocumentSpec(String data) {
		DataServicesDocument dsDocument = toDataServiceDocument(data);
		return documentSupport.convertToDocumentSpec(dsDocument);
	}

	private DataServicesDocument toDataServiceDocument(String data) {
		IDocument document = documentSupport.convertJSONToDocument(ACCOUNTING_MODEL_NAME, new StringReader(data));
		return createDataServicesDocument(document);
	}

	private DataServicesDocument createDataServicesDocument(IDocument document) {
        return documentFactory.newDataServicesDocument(document);
	}

	private DocumentGraph toDocumentGraph(DocumentSpec documentSpec) {
		DocumentGraph graph = new DocumentGraph();
		graph.setDocuments(Collections.singletonList(documentSpec));
		return graph;
	}
}

Hello @lara-early-vale,

I think you can implement this DataServicesDocumentFactory interface, with the ability to feed dummy data into the metadata.

Hey :slight_smile:
Thanks I implemented a class for DataServicesDocumentMetadata with my dummy data and then a class for DataServicesDocumentFactory to create a DefaultDataServicesDocument with this new MetaData class. now it seams to work. Thanks. I hope this is what you meant

@Component
@RequiredArgsConstructor
public class AccountingDataServiceDocumentFactory implements DataServicesDocumentFactory {
    private final DocumentServiceFactory documentServiceFactory;
    private final DocumentUtils documentUtils;

    @Override
    public DefaultDataServicesDocument newDataServicesDocument(IDocument document) {
        IDocumentSearchService documentSearchService = documentServiceFactory.createDocumentSearchService(document);
        return new DefaultDataServicesDocument(documentSearchService, document, new AccountingDocumentMetaData(documentUtils, documentSearchService, document));
    }
@Immutable
@RequiredArgsConstructor
public class AccountingDocumentMetaData implements DataServicesDocumentMetadata {
    private final DocumentUtils documentUtils;
    private final IDocumentSearchService documentSearchService;
    private final IDocument doc;

    private static final String ACCOUNTING_MODEL_NAME = "AccountingModel";

    // Placeholder values for the DataServicesDocument
    private static final String DUMMY_MODEL_ID = "dummy";
    private static final String DUMMY_USER = "system";

    @Override public DocumentReference getDocRef() {
        return new DocumentReference(ACCOUNTING_MODEL_NAME + "/" + DUMMY_MODEL_ID);
    }

    public String getDocumentModelReference() {
        return doc.getDocumentModelId();
    }

    public String getDocumentModelVersion() {
        return getTypedValue(DocumentMetadataConstants.MODEL_VERSION_PATH, String.class)
                .orElse(null);
    }

    public String getCreator() {
        return DUMMY_USER;
    }

    public String getModifier() {
        return DUMMY_USER;
    }

    public Instant getCreatedAt() {
        return new Date().toInstant();
    }

    public Instant getModifiedAt() {
        return new Date().toInstant();
    }

    private <T> Optional<T> getTypedValue(String path, Class<T> type) {
        return documentUtils.findSingleValue(documentSearchService, path)
                .filter(type::isInstance)
                .map(type::cast);
    }
}

@lara-early-vale, yes, this is what I meant. I’m wondering if the issue is still a null pointer with this new fix or something else?

Sorry typo :sweat_smile: I should be “now it seams to work” and no “not it seams to work”. I corrected it.

Hi @lara-early-vale thanks for sharing the workarround @loi-risen-dale mentioned.

This information is not longer valid!

In my current project I stumbled also over this problem and asked the a12 data services team about that solution.
It seems that you discovered a bug in the newDataServicesDocument method. As suggested by A12 Data Services team I have created an A12 internal Bug Ticket for solving the issue (internal ticket number is A12-17457).

For the moment I will stick to your provided solution thanks again :slight_smile:

Hi @lara-early-vale and @loi-risen-dale,

I invalidated my previous reply since this is not longer valid, here is the way to go for you:

Instead of creating your own DataServicesDocumentFactory bean you simply need to add the required meta field directly to the IDocument instance you hand over to the documentFactory.newDataServicesDocument.
The entity instance that you need to add to your IDocument is __meta/docRef.

Afterwards DataServicesDocument.getMetadata().getDocRef() will return the value reading that entity instance.