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).
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).
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
}
Thank for your response,
If youāre using the
data.bin
default binary serialization, you should be using the overload ofgetData()
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));
}
Thank you so much, this totally works!!!
Hey @PGriffith, hopefully one last thing regarding this topic
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. ProjectLifecycleFactory
s 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.
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.
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.
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?
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!
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:
Updating this solved my problem, Thanks!