Providing Generic Utility Classes for Broader Use Across A12 Projects

Heyho,

i would like to raise a topic for discussion, as I’ve heard it mentioned by some developers, but it seems no concrete initiative has been taken, at least from my perspective.

During development, we often work with Documents other types of entities or event entire workflows. To simplify our work, we frequently develop utility/helper/support classes that are incredibly useful for various repetitive tasks. However, some of these functionalities already exist in some A12 components. The problem is that these classes are located within internal packages, which means they are not part of the official API and are therefore not supported for use in projects based on these A12 components

The Data Services component, for instance, is a good example. It includes an entire …utils.internal… package containing many such utility classes. Theses classes effectively address common, recurring tasks. Copying these functionalities into our own projects feels counterproductive and inefficient. Therefore, despite the risk, we use some of these internal utility classes.

Would it make sense to provide such utility classes as a non-internal package so that projects based on A12 can use them officially and receive some level of support for them ?

I’d go further and say that much of this should be easy to accomplish directly using the API, rather than relying on utility classes. If utility classes are needed (even at A12) for relatively straightforward document manipulation, those would be areas that could probably benefit from API improvements. Although as I understand, the DocumentV2 API is already a vast improvement in this regard.

In one of my projects we actually developed something similar, an extension for Kotlin to simplify the interactions with A12:
e.g. simple way to define typesafe document-model structures in code for easy and safe IDocument interaction, FilterSpecBuilder, extension functions to directly do CRUD operations without needing to search for the correct type of document service, …

Sadly there was no interest to make this available to other projects by A12

The idea is good and I (as memeber of the TPS-team) will keep an eye on this thread.
At first sight, I think that concrete examples have a better chance to be analized.

Heyho,

thanks for so many replies :slight_smile:

I agree with Marcel that functionalities for manipulations of entities should be part of the entity API or similar and not outsourced into a utility class. I think this should be always the goal. But the existing of utility classes has always been firmly established, for good or bad :slight_smile:. Despite this fact it seems to be unavoidable to create such utility classes at least at first. The com.mgmtp.a12.dataservices.utils.internal.DocumentUtils is something we use for the method setSingleValue(). In other projects i saw the usage of com.mgmtp.a12.kernel.md.model.internal.service.utils.ElementUtils for example.

@tim-steep-bit I’m interested in your code snippets. Is there a way to share it with us somehow ?

We also use this one a lot, I think many projects use this one in spite of it being officially internal :smiley: Which just shows that it’s a good example for stuff that should be directly in the API. The V2 Document API very nicely improves things in this particular case though, setting a single value is something that is really straightforward with it.

Not really, its all in private project repositories. But the basic idea is to wrap a lot of the A12 specific implementation (e.g. document field access) in a way that lets you access it like a normal kotlin data object


Using it then would look like

val myDocument = MyDocument(iDocument)
// set name
myDocument.name = "Jon Doe"
// get email
println(myDocument.email)
// interactions prohibited by the compiler, as they do not match the type
myDocument.name = null
myDocument.name = 1234
// interaction prohibited by the compiler, as the field is defined read-only
myDocument.email = "example@email.com"

In Chefsculinar we transform all A12 DocumentModel JSON files into real Java POJO Classes + Builder at compile time. The reason is, in our project is performance is very important.

POJO(generated at compile time).

@Data
@NoArgsConstructor
@AllArgsConstructor
@Model("Announcement")
public class AnnouncementModel implements UIModel {

	@JsonProperty("id")
	@Mapping(name = "id", path = "id", field = true)
	private Long id;

	@JsonProperty("root")
	@Mapping(name = "Root", path = "/Root", group = true)
	private final RootModel root = new RootModel();

	@Data
	@NoArgsConstructor
	@AllArgsConstructor
	public static class RootModel implements UIModel {
		@JsonProperty("title")
		@Mapping(name = "Title", path = "/Root/Title", field = true)
		private String title;
		@JsonProperty("content")
		@Mapping(name = "Content", path = "/Root/Content", field = true)
		private String content;
		@JsonProperty("type")
		@Mapping(name = "Type", path = "/Root/Type", field = true)
		private String type;
		@JsonProperty("start")
		@Mapping(name = "Start", path = "/Root/Start", field = true)
		private LocalDate start;
		@JsonProperty("end")
		@Mapping(name = "End", path = "/Root/End", field = true)
		private LocalDate end;
	}

}

Class To create IDocument (generated at compile time).

public class AnnouncementModelDocumentBuilder extends AbstractDocumentBuilder<AnnouncementModel> {

	public AnnouncementModelDocumentBuilder(final IDocumentFactory documentFactory, final IDocumentRtService documentRtService) {
		super(documentFactory, documentRtService, "Announcement", false);
	}

	@Override
	public AnnouncementModel create() {
		return new AnnouncementModel();
	}

	@Override
	protected void apply(final IDocument document, final AnnouncementModel model, final int[] repetitions) {
		if (model.getId() != null) {
			document.setId(model.getId().toString());
		}
		final int[] repetitionsRootRepetitions = createFieldRepetition(repetitions, 1);
		document.addEntityInstance(createGroupInstance("/Root", repetitionsRootRepetitions));
		final com.mgmtp.chefsculinar.epsap.models.ui.model.announcement.AnnouncementModel.RootModel modelRoot = model.getRoot();
		if (modelRoot.getTitle() != null) {
			document.addEntityInstance(createFieldInstance("/Root/Title", modelRoot.getTitle(), String.class, repetitionsRootRepetitions));
		}
		if (modelRoot.getContent() != null) {
			document.addEntityInstance(createFieldInstance("/Root/Content", modelRoot.getContent(), String.class, repetitionsRootRepetitions));
		}
		if (modelRoot.getType() != null) {
			document.addEntityInstance(createFieldInstance("/Root/Type", modelRoot.getType(), String.class, repetitionsRootRepetitions));
		}
		if (modelRoot.getStart() != null) {
			document.addEntityInstance(createFieldInstance("/Root/Start", modelRoot.getStart(), LocalDate.class, repetitionsRootRepetitions));
		}
		if (modelRoot.getEnd() != null) {
			document.addEntityInstance(createFieldInstance("/Root/End", modelRoot.getEnd(), LocalDate.class, repetitionsRootRepetitions));
		}
	}
}

Class to create AnnouncementModel(generated at compile time).

public class AnnouncementDocumentToModelBuilder extends AbstractModelBuilder<AnnouncementModel> {

	public AnnouncementDocumentToModelBuilder() {
		super(getElementInfoMap());
	}

	private static Map<String, ElementInfo> getElementInfoMap() {
		final Map<String, ElementInfo> elementInfoMap = new HashMap<>();
		elementInfoMap.put("/Root", new GroupInfo<>(AnnouncementModel::getRoot));
		elementInfoMap.put("/Root/Title", new FieldInfo<>(String.class, com.mgmtp.chefsculinar.epsap.models.ui.model.announcement.AnnouncementModel.RootModel::setTitle, false));
		elementInfoMap.put("/Root/Content", new FieldInfo<>(String.class, com.mgmtp.chefsculinar.epsap.models.ui.model.announcement.AnnouncementModel.RootModel::setContent, false));
		elementInfoMap.put("/Root/Type", new FieldInfo<>(String.class, com.mgmtp.chefsculinar.epsap.models.ui.model.announcement.AnnouncementModel.RootModel::setType, false));
		elementInfoMap.put("/Root/Start", new FieldInfo<>(LocalDate.class, com.mgmtp.chefsculinar.epsap.models.ui.model.announcement.AnnouncementModel.RootModel::setStart, true));
		elementInfoMap.put("/Root/End", new FieldInfo<>(LocalDate.class, com.mgmtp.chefsculinar.epsap.models.ui.model.announcement.AnnouncementModel.RootModel::setEnd, true));
		return elementInfoMap;
	}

	@Override
	protected AnnouncementModel createInstance(final IDocument document) {
		final AnnouncementModel model = new AnnouncementModel();
		model.setId(getIdFromDocument(document));
		return model;
	}
}

Typescript description file(generated at compile time).

import { A12Document } from "../A12Document";

export interface Root {
    Title: string;
    Content?: string;
    Type: "ERROR" | "WARNING" | "INFO";
    Start: string;
    End: string;
}
export interface Announcement extends A12Document {
    Root: Root;
}

PS: We dont use the Dataservice.

PPS: If you are interessetest, you can contact me, would be happy to share.

Also we have a development a solr gradle plugin/solr-api what generated Solr Documents at compile time from POJO classes. It would make it possible to save generated POJO classes as flat structure into solr and our solr-api supports delta/partial updates. That would mean, all A12 Documents can be saved as a real SorlDocuments without serialization into json. The solr api supports also delta updates.

In one project, we created a Wrapper around Document by implementing a POJO for each model and use JsonPointer. This POJO provides getter and setter methods for adapting your document.

This solution has the problem, that if the document model changes, your POJO is invalid, but only implicitly. Furthermore you have to adapt the POJO class afterwards, but modelling and implementing is mostly done by different people.

I assume, that you have a similar problem @tim-steep-bit ? But i must admit, that your solutions seems to have a better usage, because document fields are class fields and not methods like

@tobias-static-slope and all of these code snippets are create at compile time ? Is it code which is a12 generic, so everybody could get access to it ? And one last question, why don’t you use Data Services ?

Edit: Additionally i know, that Cosmo use also a Bean-Generator (or POJO-Generator). It is obvious that many big projects are created their own wrapper. It seems that not only utils classes should be made public, also the demand for a POJO. I know there is the Document V2 and i use it for my current PoC Project. It makes things a bit better, but using Pointer for getting fields feels cumbersome. Does anybody have made experience with the Document V2 solution ?

@tjorben-atomic-moss yes all this code is generated at compile time. It is currently only available for all Chefsculinar Projects and is not yet a A12 component. We dont use DataService, because it can not handle large data.

Same for us, but that our code representation is not exaclty equal to the document is by design:
Often only some fields are necessary from a technical perspective so to reduce clutter we only define these fields in our code.
For our usecase this has worked pretty great so far.