Configure a module with project resources

Any github/gitlab/etc instance, so I can check out the code. A private repo works, but a public repo is preferred for the sake of community sharing - you could even directly fork my repo, and then we could contribute fixes back to the primary repo (fixing this problem for anyone in the future).

1 Like

Hello @PGriffith,

Unfortunately I’m unable to upload the code in a public repository. Sorry for that. I’ll do my best to try to explain what I’m trying to accomplish and what I have so far.

I have a scripting function that has a lot of parameters (around 20, including lists). Instead of using parameters, I would like to have a project resources tab where I can store the different parameters so I can call the funcion like this afterwards: system.myfunctions.function1(“resource1”).
“Resource1” would ideally get anobject FunctionParameters, and the function would run using it’s getters.

So far I have a swing GUI working as a ResourceEditor and the scripting function. The problem that I’m running into is that I’m unable to save the resource, load the parameters in the swing components when I open the designer after closing it and accessing the object I’m storing in my scripting function.

-Save the resources:
In the class extending ResourceEditor, I’m overriding getObjectForSave like this:

@Override
    protected FunctionParameters getObjectForSave() throws Exception {
        /*
              The different parameters would get the values from the swing components here, all variables are Serializable
         */
        return new FunctionParameters(var1,var2,var3,...);
    }

Do I need to call this method (with a button for example) in order to save the object? Here you can see how I’m serializing and deserializing the object:

@Override
    protected void serializeResource(ProjectResourceBuilder builder, FunctionParameters object){
        builder.putData(FunctionParameters.DATA_KEY,serializeToByte(object));
    }

    @Override
    protected FunctionParameters deserialize(ProjectResource resource) {
        byte[] data = resource.getData(FunctionParameters.DATA_KEY);
        if (data != null && data.length > 0) {
            return deserializeToByte(data);
        } else {
            return new FunctionParameters();
        }
    }

    private static byte[] serializeToByte(FunctionParameters obj){
        try{
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ObjectOutputStream os = new ObjectOutputStream(out);
            os.writeObject(obj);
            return out.toByteArray();

        }catch(Exception e){

        }
        return null;
    }

    private static FunctionParameters deserializeToByte(byte[] data){
        try{
            ByteArrayInputStream in = new ByteArrayInputStream(data);
            ObjectInputStream is = new ObjectInputStream(in);
            return (FunctionParameters) is.readObject();
        }catch(Exception e){

        }
        return null;
    }

Finally, in the class FunctionParameters, other than the variables, constructors and getters I have these fields:

public static final String TYPE_ID = "config";
    public static final String MODULE_ID = "com.mycompany.scriptingFunctionModule";
    public static final ResourceType RESOURCE_TYPE= new ResourceType(MODULE_ID,"config"); //TODO: ??
    public static final String DATA_KEY = "config.md"; 

-Load the resources on the swing components:

My thoughts are that I would need to get the data stored in “config.md” and write the different parameters into the swing components. Where do I do that?
I’ve looked into the class ResourceEditor: ResourceEditor (inductiveautomation.com), when do I need to call commit or updateResource? The goal here is that if I close the designer, I want to see the configuration in the resource editor.

-Accessing the data of a resource:

As I explained above, I wan’t to be able to have different resources (parameters) for the same function. I have the scripting function working but once I solve the problems we mentoned, I would like to access the data. How do I access project resources?

If there is any explanation missing, please, let me know.

Thank you so much, you are really helpful!

This looks fine, as long as FunctionParameters is Java-serializable (implements the Serializable marker interface, plus some other baggage).

You shouldn’t need to implement any logic to manually save this resource, as long as you’re using a correctly set up workspace. Are you extending from the TabbedResourceWorkspace, or is your resource best configured as a pseudo-singleton? If you are using the TRW, then serialization should happen automatically when you close tabs/save in the designer.

As for serialization and deserialization - I would skip the writeObject/readObject calls - if you specifically need to retrieve your resource, look at com.inductiveautomation.ignition.common.util.ResourceUtil#decodeOrNull() - specifically the overload that accepts a ProjectResource. In your scripting module, you can use CommonContext.createDeserializer() (CommonContext is the superinterface for the Gateway, Client, and Designer context objects) to create the XMLDeserializer instance you need to pass in to ResourceUtil.

You would do that in init() in your ResourceEditor subclass. If you leave the data stored as data.bin (I would recommend doing so, since it doesn’t look like your resource can’t be made ‘human-readable’ for diffing), then overriding getObjectForSave should be all you need.

The easiest thing to do is cache the resource object you are provided in init() in a field, then modify that object via your swing components (also created in init, then return that modified object in getObjectForSave(). If you do that, then you should be able to globally save in the designer and have things work as expected.

As mentioned above, this would be covered by ResourceUtil - you’ll just need to construct your scripting module to have a context object passed in (in your different scope’s base hook).

1 Like

Hello Again @PGriffith,

I’m still working on getting this to work…
I have written some logs inside the overriden getObjectForSave() and aparently it’s only being called when I close the designer… I thought this would be called when I save the project?
Also when I save the project manually (after editting the resource) it says no changes to save on the bottom left part of the designer. This doesn’t happen when I create a new resource or edit anything else.

Also here is my deserialize method:

@Override
    protected PRResource deserialize(ProjectResource resource) throws Exception{
        byte[] data = resource.getData(PRResource.DATA_KEY);
        //logger.info("Deserializing");

        if (data != null && data.length > 0) {
            log.info("Deserializing with data");
            return deserializeToByte(data);
        } else {
            log.info("Deserializing without data");
            return new PRResource();
        }
    }

private static PRResource deserializeToByte(byte[] data){
        try{
            ByteArrayInputStream in = new ByteArrayInputStream(data);
            ObjectInputStream is = new ObjectInputStream(in);
            return (PRResource) is.readObject();
        }catch(Exception e){

        }
        return null;
    }

Since I’m using the data.bin file I don’t need to override the deserializeResource() method. The problem here is that when I log the output for deserializeToByte(data).toString(), I get the following error:

If I don’t override deserialize and serialize, I also get an error with the XMLSerializer saying " Content is Not Allowed in Prolog"

Do you have any clue about what is wrong here? The method getObjectForSave is never saving any null objects.

Thanks,

If you’re using the data.bin default binary serialization, you should be using the overload of getData() with no parameters. Check the structure of the resource on disk.

How are you implementing your project browser nodes? Do they extend from com.inductiveautomation.ignition.designer.tabbedworkspace.ResourceNode? If so, it looks like you can override various methods on the resource node:

    /**
     * Called whenever the ResourceEditor for this resource has been opened or selected in the tabbed workspace.
     */
    protected void onEditorSelected(ResourceEditor editor) {
        // no-op
    }

    protected void onEditorUnselected() {
        // no-op
    }

    protected void onEditorOpened(ResourceEditor editor) {
        // no-op
    }

    protected void onEditorClosed(ResourceEditor editor) {
        // no-op
    }
1 Like

Thank for your response,

If you’re using the data.bin default binary serialization, you should be using the overload of getData() with no parameters. Check the structure of the resource on disk.

I have 3 data.bin files in my project files, in this directories:

-projects/myproject/com.inductiveautomation.vision/client-event-scripts
-projects/myproject/com.inductiveautomation.vision/client-tags
-projects/myproject/ignition/global-props

How are you implementing your project browser nodes?

I’m not implementing project browser nodes, based on your Markdown Notes module, there is no need to do that right?

I noticed that in my deserialize method, the method deserialiceToByte(data) is always returning null. When I first create the resource this is expected so I changed the deserialize method to this:

@Override
    protected PRResource deserialize(ProjectResource resource) throws Exception{
        byte[] data = resource.getData();
        PRResource deserialicedResource = deserializeToByte(data);
        if(deserialicedResource==null){
            log.info("Deserializing without data");
            return new PRResource();
        }else{
            log.info("Deserializing with data");
            return deserializeToByte(data);
        }

    }

Now when I create the resource, I don’t get an error. The problem is that the designer is never calling my overriden method getObjectForSave() (the method that gets the data from the jComponents and returns the resource object PRResource). As we discussed before, the designer should be calling this method when saving behind the scenes right?

Also, does this consumer look right to you in the TabbedResourceWorkspace?

private Consumer<ProjectResourceBuilder> newPRConfig=builder ->
            builder.putData(PRResource.DATA_KEY, "Enter a configuration".getBytes());

Should it be

private Consumer<ProjectResourceBuilder> newPRConfig=builder ->
            builder.putData(PRResource.DATA_KEY);

?

Thank you so much for helping me!

I pushed a Java serialization example to Github: https://github.com/paul-griffith/markdown-resources/tree/java-serialization

Just skip implementing any deserialize function (either overload) - the ResourceEditor should default to the behavior you want.

Yes, when you close tabs in the tabbed resource workspace commit() should be firing and calling getObjectForSave(), as long as you haven’t overridden commit().

No. Since you’re not using the builder, I would recommend changing your NewResourceAction; take a look at the example I pushed to Github, but basically, override createPrototype() in your subclass to return the ‘default’ instance of your custom resource class. Unfortunately you have to jump through some more hoops to use the welcome panel - you can either avoid it, or do what I did in the example; as Java it would be something like:

private Consumer<ProjectResourceBuilder> newPRConfig = builder -> {
	var serializer = context.createSerializer();
    var prototype = MarkdownResource(123, 456);
    serializer.addObject(prototype);
    builder.putData(serializer.serializeBinary(true));
}
2 Likes

Thank you so much, this totally works!!!

1 Like

Hey @PGriffith, hopefully one last thing regarding this topic :sweat_smile:

You told be to retrieve the resource (in the Gateway context for example) using decodeOrNull​(ProjectResource r, XMLDeserializer deserializer, java.lang.Class<T> clazz) in ResourceUtil. The problem is that my resource is not implementing the ProjectResource interface…

I also found this way of retrieving the resource:

List<ProjectResource> resource=context.getProjectManager().getProject(projectName).get().getResourcesOfType(myResourceType);

I like this option better since I can get all the resources from all the projects. The question is, is it really necessary to implement the ProjectResource interface in my resource? Is there any workarounds?

Edit: Just to make clear, I see no problem in implementing the ProjectResource interface, I’m just curious about the original design.

Thank you again,

No, your actual “resource” class, whatever configuration you’re passing around (I assume it’s the same class on the gateway and designer, as in my example) should not need to implement the ProjectResource interface; that’s considered ‘internal’. Instead you should be asking or retrieving a ProjectResource from the project system directly (as in your second example) and deserializing it into your desired class.

Depending on exactly what you need to do with these resources you’re creating, you might be able to take advantage of the ProjectLifecycleFactory - which, as the name implies, automatically creates ProjectLifecycle instances. ProjectLifecycleFactorys are used in a bunch of different places in our codebase:


Hopefully implementing the abstract/protected methods will be enough to get you started using that (if it fits your needs) - otherwise I might push an example of a lifecycle factory implementation within a couple weeks.

2 Likes

Great, thank you! Is there any rule of thumb to recognize which classes or interfaces are “internal”?

No, no perfect system - a lot of stuff can’t really follow Java visibility rules since we need to access it from other modules/locations in the codebase.

1 Like

ProjectResource is an example of something that isn’t actually internal, it’s necessary that it’s publicly available.

What it should be, though, is sealed, which is something that is possible starting in Java 15.

2 Likes