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

Attempting to use this to try and re-create the menu action, I can't seem to get it to show up correctly in the JPopupMenu

I get a "New..." option, but no icon, and it doesn't say the Name I provided it.

Any ideas?
image

I also note that my IDE is telling me that the init method is not used, which is making me wonder if I need to be extending some other object to get that init method to execute. Everything else properly works and it creates the file, so I believe the init method is the only thing failing to execute.

EDIT: I now realize this is because init is a kotlin keyword to essentially act as the constructor, so my new problem is how do I add a constructor to an anonymous class!

Try moving the putValue calls into an initializer block.

Hereā€™s an example I can share:


    @Override
    public void addNewResourceActions(ResourceFolderNode folderNode, JPopupMenu menu) {
        menu.add(new NewResourceAction(this, folderNode, getNewConstructor(context.getProject())) {

            {
                putValue(Action.NAME, BundleUtil.i18n("theming.theme.action.new"));
                putValue(Action.SMALL_ICON, new InteractiveSvgIcon(Meta.class, "images/svgicons/theme.svg"));
            }

            @Override
            protected String newResourceName() {
                return BundleUtil.i18n("theming.theme.action.new.defaultName");
            }
        });
    }

Awesome, this solved the first half of the problem!
image

However I am still running into the whole "New ?null?" thing, any ideas?

I took a look through the NewResourceAction class for anything to the affect of getResourceType but didnt see anything.

What does your ResourceWorkspaceā€™s constructor look like?

Currently its this:

public ConfigManagerWorkspace(DesignerContext context) {
        super(context, ResourceDescriptor.builder()
                .resourceType(ConfigResource.RESOURCE_TYPE)
                .nounKey("Config File")
                .rootFolderText("Process Configs")
                .rootIcon(VectorIcons.get("script-configure"))
                .navTreeLocation(999)
                .build());
    }

I didnt have the nounKey section until you mentioned the abstract class and I dug into it, it seems to have gotten me closer, but now I have question marks wrapped around the noun!

I also went ahead and put a copy of the module as it stands right now here GitHub - keith-gamble/ignition-config-manager: An ignition module that manages config files for project use

This isn't meant to be string, but instead a key used for BundleUtil lookup.

Check this out:

1 Like

Updating this solved my problem, Thanks!

1 Like