I am looking for a tutor for module development

Good morning,

I hope this doesn’t break any forum guidelines but I can’t think of a more appropriate place to ask - I’ve looked at other tutoring sites and though I can find people who can help me with Java or Swing, I can’t find any that also know about integrating them with the Ignition ecosystem.

I am looking for someone who can help guide me in the ways of module development for Ignition specifically. Some java specific things like POM files, and things related to the workflow I will need assistance on and how they relate to Ignition. I have some background knowledge Java/Algorithms/Data structures from college but using modern java workflow in practice is not something I have any real knowledge of and will need assistance on.

I am willing to negotiate on price, I don’t expect anyone will help me out for free. I was hoping maybe 1 hour a week for at least 3-4 weeks, preferably on the weekend, so I can work on what we talked about and regroup. All my previous efforts of self-learning eventually end up with me hitting a wall where I don’t know what certain things mean and need someone with experience to help guide me.

Feel free to DM me or leave a reply.

Again apologies if this breaks any terms of this forum but I can’t think of a better pool of people who might be able to help me out with learning this.

1 Like

No apologies required.

I’d offer, but you pointed out things that I refuse to use (maven, pom files), so I can’t help you there. (I’ll jump directly to gradle when I’m no longer supporting v7.9.)

Since you don’t seem to be pressed schedule-wise, why not just ask your questions here?

1 Like

I only say that because using a Maven archetype was the only way I was personally able to get the tutorial / archetypes that Ignition already built as example to work (though I wasn't able to do much more than import the jar file to the gateway and use the example in Designer). If there are other ways I am more than open to it - I don't know nearly enough to be opinionated on this sort of stuff yet lol.

I ask for a tutor because I know so little regarding what seems like basic principles re: java build workflow and integrating with Ignition's codebase that I don't even know where to begin asking questions if I am being honest.

I also seem to learn best with a tutor, it keeps me motivated week to week.

Nah, you're fine. We have occasional job posts/job seekers as well; I think as long as they're not inundating the forum it's not a big deal.

I think at least part of the confusion for people who might be familiar with programming, but struggle to get started with module development, is that you're learning two separate things at the same time.
There's a particular Java build stack (whether you choose Maven or Gradle or whatever), and then attached to it the Ignition API surface, which is fairly massive.

It's also a problem from the other direction: people tell us they want a 'module development' course, but it's almost impossible to target everyone's experience with the different aspects.

I guess I'll start with some very high level principles.
Your build system of choice (generally Gradle or Maven, there's other options but they're certainly the most common these days) is usually in charge of running the Java compiler over your Java source files to generate a jar file. JAR files are just ZIPs full of .class files, which are themselves essentially just JVM bytecode. That's an important point of distinction - once it's a class file, it's not technically Java anymore. That's part of how alternate JVM languages like Kotlin and Scala work - they just have to emit valid bytecode, without the limitations of Java's own syntax and compiler. (There are also tools available to 'decompile' bytecode back to ~the input Java code, but there's always information lost, such as parameter names for method and other issues. IntelliJ has a pretty good decompiler built in, which can help to look into library code you don't have the underlying source for).

By convention, Maven (the big kahuna Java build system for a long time) looks for Java files to compile in src/main/java. Every directory under that root by convention defines your package structure. Package structure is important, because there's a single global namespace in the final JVM - every class must have a unique fully-qualified name. By convention, the package name you use should be a reversed domain name that you control - that allows you to "prove" that you wrote the code, and is useful for library distribution (and part of the verification process on repositories like Maven Central). Gradle follows these conventions precisely because Maven was dominant for such a long time. However, in both Maven and Gradle, you can readily change these things (it's just generally not a good idea unless you have a good reason).

Separate from the actual source file organization, you can organize your projects themselves into "subprojects" (what Gradle calls them, Maven may have a different name). Basically these are just folders at the top-level that contain their own build-system descriptors (build.gradle(.kts)/pom.xml). Somewhere on the top level, you'll define which subprojects you have, and your build system will automatically reach into those subfolders and build their projects too.
For instance, in the Perspective Component example, there's four subprojects: common, designer, gateway, and web. Those are outlined in the settings.gradle at the root.

So that's the very broad overview of Java building. Pivoting a little to Ignition:

That common/gateway/designer split is a common convention in the realm of Ignition, because 1. code re-use is nice, and 2. you almost always want to deliver different artifacts to the different Ignition scopes (because they have different APIs and different capabilities). So you build those subprojects, and each outlines their own dependencies on Ignition; e.g. common depends on Ignition's own common layer: https://github.com/inductiveautomation/ignition-sdk-examples/blob/master/perspective-component/common/build.gradle#L12
And then gateway can depend on both Ignition specific gateway apis and the common subproject:
https://github.com/inductiveautomation/ignition-sdk-examples/blob/master/perspective-component/gateway/build.gradle#L10
That is how you tell the build system (and your IDE) "this code I'm writing in gateway can use anything I've written in common.

The only fundamental unit you need to provide as a module is some implementation of the appropriate hook class for your desired scope. Basically, you author a class that implements the *Hook (or, better extends the abstract class we provide) for the right scope, e.g. AbstractGatewayModuleHook for the gateway:

There are various lifecycle methods that will be invoked for you - setup, startup, and shutdown. Those are how your module gets access to the rest of Ignition - from the GatewayContext you get in setup, you can get access to the myriad subsystems available in Ignition.

However, just implementing the class and putting it into a jar file isn't enough. Ignition doesn't "know" enough to know how to actually do anything with one or more plain .jar files. So there's one final piece - the actual module .modl file. The actual module file is nothing more than a ZIP file that contains a bunch of .jar files and a module.xml file. That module.xml file is crucial - it tells Ignition how to load your various jar files, what scopes they belong to, and most importantly, what your actual hook classes are:

<?xml version="1.0" encoding="UTF-8"?>
<modules>
	<module>
		<id>com.inductiveautomation.ignition.examples.hce</id>
		<name>Gateway Webpage Example</name>
		<description>Implements a fake "Home Connect" hub status and configuration page</description>
		<version>1.0.0-SNAPSHOT</version>
		<requiredignitionversion>8.1.0-SNAPSHOT</requiredignitionversion>
		<depends scope="G">com.inductiveautomation.opcua</depends>
		<jar scope="G">gateway-webpage-gateway-1.0.0-SNAPSHOT.jar</jar>
		<hook scope="G">com.inductiveautomation.ignition.examples.hce.GatewayHook</hook>
	</module>
</modules>

Generating these XML files by hand would be annoying, which is why we provide Maven and Gradle plugins to build modules for you. The 'new' generation is available here, and is the exact same tool we use internally.
Your build system of choice should provide a place to configure this plugin, e.g. Gradle:
https://github.com/inductiveautomation/ignition-sdk-examples/blob/master/perspective-component/build.gradle#L34
or Maven:

I just kinda rambled that all out, and my hand hurts now, but hopefully that helps a bit to orient.

If you've got particular questions on how to approach something in Ignition, I'd definitely say to ask here on the forums. More general build system questions might be better suited for other places, but it doesn't really hurt to ask here either.

20 Likes

@PGriffith Thank you for the high level overview.

I was able to make the vision-component-archetype so going along with your post I just had a few clarifying questions. Here is what I got by default

image

By convention , Maven (the big kahuna Java build system for a long time) looks for Java files to compile in src/main/java . Every directory under that root by convention defines your package structure. Package structure is important, because there’s a single global namespace in the final JVM - every class must have a unique fully-qualified name.

So I circled two parts with src/main/java - both of these get put into the same jar/module you're saying? That Maven starts at my top level folder "DateSelector", then for every subfolder looks to see if it has a src/main/java and knows that that is what is meant to be turned into byte code yes?

Package structure is important, because there’s a single global namespace in the final JVM - every class must have a unique fully-qualified name. By convention , the package name you use should be a reversed domain name that you control - that allows you to “prove” that you wrote the code, and is useful for library distribution (and part of the verification process on repositories like Maven Central).

Ok reversed domain name used to confuse me but that's just com.bkarabinchak - reversed domain becuase a normal domain is google.com and a reversed domain name is com.google. And this must be unique if the package is every to be distrubted via Maven or Gradle - Maven/Gradle can't have two libraries that are com.something because how would it know which one to give you.

Separate from the actual source file organization, you can organize your projects themselves into “subprojects” (what Gradle calls them, Maven may have a different name). Basically these are just folders at the top-level that contain their own build-system descriptors ( build.gradle(.kts) / pom.xml ). Somewhere on the top level, you’ll define which subprojects you have, and your build system will automatically reach into those subfolders and build their projects too.
For instance, in the Perspective Component example, there’s four subprojects: common , designer , gateway , and web . Those are outlined in the settings.gradle at the root.

Ok so if I wanted to, and knew how, what I could do something like this -
image
and keep my actual module specific code in MySubProject and somehow modify the building instructions so that the MySubProject folder is included in my build process. This also isn't a necessity but is just a much cleaner way to keep your code organized yes (which is important to me but I just am trying to parse what is required and what is convention).

Basically, you author a class that implements the *Hook (or, better extends the abstract class we provide) for the right scope, e.g. AbstractGatewayModuleHook for the gateway:
...
So there’s one final piece - the actual module .modl file. The actual module file is nothing more than a ZIP file that contains a bunch of .jar files and a module.xml file. That module.xml file is crucial - it tells Ignition how to load your various jar files, what scopes they belong to, and most importantly, what your actual hook classes are:

Ok so the combination of my doing extend AbstractGatewayModuleHook and the module.xml in conjunction are what let Ignition know Hey this class is to be used on the Gateway context if I am understanding that right?

Stuff like this -

There are various lifecycle methods that will be invoked for you - setup , startup , and shutdown . Those are how your module gets access to the rest of Ignition - from the GatewayContext you get in setup , you can get access to the myriad subsystems available in Ignition.

Is there any documentation where I would find this sort of thing out? I've seen it before in tutorials and other module development questions about having to do the setup/startup/shutdown, but I never found where I would find that if I didn't have access to this forum.

I think maybe it would be helpful for me to talk about the first thing I am trying to build a simple Vision component that is really a combination of other components. It’s something a project I work on has a template for but is buggy and I thought it was a good starter project. It’s something that would be used for setting a datetime span. It would look like this -

image

The top is a dropdown of selections like this week, this month,this year, last week, etc, that when an item is seleted, the bottom two dates reflect the correct time span. The bottom two components are datepickers in the event someone wants to modify the date slight (at which point the top dropdown which is only strings goes to the Custom option).

Now I had started making this directly in Java Swing, made a JFrame with a JPanel with a dropdown on it and two text labels to show the date - because I had learned that Java Swing does not have datepickers.

At this point I was thing well Ignition DOES have very nice Vision Datepickers, how could I use those in this?

Then I thought about how the vision-component-archetype example you have to implement the AbstractVisionComponent, so surely my using of JPanel and JCombobox directly are also no good. But I don’t know 1) is it possible to borrow from the prebuilt components Ignition already has 2) where I would look to see what is available to me in this context - like is there an IgnitionComboBox comboBox I would use instead and if so where do I look to see this information.

I know this is a very simple example but I hope it will give me some experience with all of tis. It would be cool to first get this working, and then see how I could try separating my code into a subproject that is still incorporated into the build, and then anything else that would just make things easier for the build.

And then of course making it a signed module at some point so I could use it in the project without having to change the project’s .conf file to allow unsigned modules but that would be like step 3 or 4 after just getting this to work in a dev environment.

Any insights or answers to my questions on this specific first project would really help. Thanks for all the information already.

Since you've played with Swing, I presume you've gathered that Swing is really a layer cake, where JComponents are nested within other JComponents as needed to yield the desired appearance/behavior. So yes, you can embed anything you find in the Ignition JavaDocs that is any kind of subclass of JComponent. Some notes:

  • Your user-accessible component must implement the VisionComponent interface in order to be added to any Vision container. Extending AbstractVisionComponent pulls that in for you, but you can extend some other Swing object if you implement that interface yourself.

  • Nothing nested within your component will be exposed to users automatically, except that they can call .getComponent() with integers to traverse your inner hierarchy. You should expose getters/setters on the main component that delegate to your inner components intelligently.

  • Ignition won't layout your inner components. You should use MigLayout to place and scale your inner components as the user manipulates the main component's size.

  • You don't have to use inner components for everything--you can paint visual details as you please.

  • There are optional interfaces for supporting custom properties and/or custom methods. Save that for later. (:

2 Likes

Yes at least for the pure Swing project I've been trying to learn with I find it's many JPanels inside of JPanels where the inner/second JPanel will have my components.

find in the Ignition JavaDocs

Is this the current site? ignition 7.9.3 API
The project I want to make this custom vision component for is 8.0.14, is there a separate javadoc link for that or is this one suitable?

Nothing nested within your component will be exposed to users automatically, except that they can call .getComponent() with integers to traverse your inner hierarchy. You should expose getters/setters on the main component that delegate to your inner components intelligently.

Ok so my something like this

import java.util.Date
public DateSelector extends AbstractVisionComponent {
    public DateSelector () {//set up components here I think}
    public Date getStartDate() {
     //Some logic to get start date from inner components   
        return theStartDate;
    }

    public Date getEndDate() {
       //logic to get EndDate from inner components
        return theEndDate;
 }

implements the VisionComponent interface already - I don't need to do anything else re: the interface?
And secondly the getStartDate / getEndDate would be how I expose the dates that I am showing in the bottom two datepickers to the person using Designer such that they can be binded with?

You don’t have to use inner components for everything–you can paint visual details as you please.

Meaning manually paint the 2D graphics? That's good to know though I don't think/hope that what I presented doesn't need that yet - would be good to learn but I am hoping that this doesn't need it - hopefully I can just focus on composing the components together for now in a nice layout.

There are optional interfaces for supporting custom properties and/or custom methods. Save that for later. (:

Haha thanks, I already feel like there's a mountain of stuff I don't know regarding all of this. One step at a time.

-client and -designer are already distinct subprojects. There's no code in the top level Maven project (there could be, but it doesn't make sense for Ignition packaging).

Maven and Gradle don't really distribute anything. Distinct from Maven (the build system) there's Maven Central, the de-facto standard repository for Java artifacts: https://search.maven.org/ - both Gradle and Maven typically fetch artifacts from Maven Central, but you can tell them to fetch artifacts from anywhere (see build.gradle or pom.xml where you have to manually specify the Inductive Automation repositories to fetch our dependencies from.
The driver for globally unique class names is the JVM itself - reverse-qualified domain names are just a convention that's easy to follow and easy enough to enforce that it's stuck around.

You absolutely could, yes - you would just then need to make sure that your code was marked as required (in the module.xml) for whatever scopes your module needs, so that at runtime, in Ignition itself, it loads successfully.

Right - there's 'scopes' built in to the module.xml declaration - you can say something should be loaded on the gateway, in the designer, in the client, or on a mix of those (see also com.inductiveautomation.ignition.common.model.ApplicationScope).
When you load a module, we look for a module.xml file. Once it's found and parsed, we look for the hook declarations. Once they're found, we create a classloader for your module (only on the gateway), load up your dependencies, then ask the classloader to instantiate the fully-qualified class name you provided in the module.xml. If the name was invalid, couldn't be found, or doesn't implement the appropriate *Hook class, it'll throw an error and your module won't load.
Once your hook class instance has been created, we'll automatically call the setup, startup, and shutdown methods on it for you. You don't 'decide' when the module loads (with some exceptions) - you just respond to whatever Ignition does with your code.

The short answer is no - there's some documentation available, but it's hard to tell what's still applicable and what's wildly out of date. The easiest thing to do is usually to start from an example.

Yes, definitely.

For complicated legacy reasons, most components have a PMI prefix (from FactoryPMI). Most of the built-in Vision components are listed in the com.inductiveautomation.factorypmi.application.components package:
https://files.inductiveautomation.com/sdk/javadoc/ignition81/8.1.13/com/inductiveautomation/factorypmi/application/components/package-summary.html
I think you probably want PMIDateTimePopupSelector (I think that's the right class) based on your graphic. The top dropdown should probably be a vanilla JComboBox.

No, that's it for the interface (the IDE and compiler will complain at you if you haven't implemented some required methods).

You can do initialization in your constructor, but there's some tricky ramifications w.r.t Java serialization, which Vision still relies on. Depending on what you have to do during initialization, you might want to use onStartup and onShutdown instead (automatically provided by AbstractVisionComponent):
image

Almost.
Again, complicated Java legacy is involved here, but basically every property you can use for binding should have 3 things:

  1. A no-argument getter
  2. A single-argument setter, of the same type as the getter
  3. Fire a propertyChange event with the name of the property when the setter is invoked.

For a couple of reasons, include upgrading, Vision components typically use an external BeanInfo file, that describes the "Java Beans" available on a given component instance, and allows you to customize which properties are available and other meta-information for the designer. See the BeanInfos package in the example.

The first-party Oracle Swing Tutorials are genuinely good resources, even if some of it is dated in terms of the rest of the Java ecosystem by now.

2 Likes

Than you for the very in depth answer. I am going to try my hand at this so I can come back with some code and more questions.

One last question though before I try my hand -

You can do initialization in your constructor, but there’s some tricky ramifications w.r.t Java serialization, which Vision still relies on. Depending on what you have to do during initialization, you might want to use onStartup and onShutdown instead (automatically provided by AbstractVisionComponent ):

So I would leave my constructor blank and in onStartup is where I would initialize my swing components/Ignition components/layouts within a panel?

And onShutdown I'm not sure what would go on there.

Yea I learned just enough swing to be dangerous lol. I learned enough swing to make a window and make buttons that do things or do things on component via ActionListeners, then figured I should see how things work with Ignition, hence this post when I realized I don't know much. I definitely need to finish that tutorial.

Hopefully will have some code to post to this in a day or two. Thanks again!

Ok I started trying my hand and the first issue I run into is that I can’t seem to import the premade Ignition components.

I made my project template from
image

and haven’t really touched anything besides importing a few other things like java.util.Date.

I tried

but it seems like it cannot find this. How would could I make com.inductiveautomation.factorypmi.application.components usable to me in my module? So that I can then use the PMIDateTimePopupSelector.

@PGriffith You can ignore the previous post.

I typed in private PMIDateTimePopupSelector fromDate; just to start and it did import com.inductiveautomation.factorypmi.application.components.PMIDateTimePopupSelector; for my automatically no problem.

I laid out a couple of components in a JPanel with MigLayout, no other logic going on yet.

I was able to build the jar and MODL file.

When I tried importing it to my local gateway I get the following errors -

ModuleManager 18Jan2022 09:52:57 Error installing module.

java.lang.Exception: Exception parsing "module.xml"

at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.parseModuleInfo(ModuleManagerImpl.java:1703)

at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.extractModuleInfo(ModuleManagerImpl.java:1696)

at com.inductiveautomation.ignition.gateway.web.pages.config.ModuleInstallPage.eulaCertCheck(ModuleInstallPage.java:120)

at com.inductiveautomation.ignition.gateway.web.pages.config.ModuleInstallPage$1.onSubmitInternal(ModuleInstallPage.java:71)

at com.inductiveautomation.ignition.gateway.web.components.CsrfPreventingForm.onSubmit(CsrfPreventingForm.java:67)

at org.apache.wicket.markup.html.form.Form$9.component(Form.java:1248)

at org.apache.wicket.markup.html.form.Form$9.component(Form.java:1242)

at org.apache.wicket.util.visit.Visits.visitPostOrderHelper(Visits.java:274)

at org.apache.wicket.util.visit.Visits.visitPostOrder(Visits.java:245)

at org.apache.wicket.markup.html.form.Form.delegateSubmit(Form.java:1241)

at org.apache.wicket.markup.html.form.Form.process(Form.java:923)

at org.apache.wicket.markup.html.form.Form.onFormSubmitted(Form.java:769)

at org.apache.wicket.markup.html.form.Form.onFormSubmitted(Form.java:702)

at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)

at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)

at java.base/java.lang.reflect.Method.invoke(Unknown Source)

at org.apache.wicket.RequestListenerInterface.internalInvoke(RequestListenerInterface.java:258)

at org.apache.wicket.RequestListenerInterface.invoke(RequestListenerInterface.java:216)

at org.apache.wicket.core.request.handler.ListenerInterfaceRequestHandler.invokeListener(ListenerInterfaceRequestHandler.java:240)

at org.apache.wicket.core.request.handler.ListenerInterfaceRequestHandler.respond(ListenerInterfaceRequestHandler.java:226)

at org.apache.wicket.request.cycle.RequestCycle$HandlerExecutor.respond(RequestCycle.java:814)

at org.apache.wicket.request.RequestHandlerStack.execute(RequestHandlerStack.java:64)

at org.apache.wicket.request.cycle.RequestCycle.execute(RequestCycle.java:253)

at org.apache.wicket.request.cycle.RequestCycle.processRequest(RequestCycle.java:210)

at org.apache.wicket.request.cycle.RequestCycle.processRequestAndDetach(RequestCycle.java:281)

at org.apache.wicket.protocol.http.WicketFilter.processRequest(WicketFilter.java:188)

at org.apache.wicket.protocol.http.WicketFilter.doFilter(WicketFilter.java:245)

at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1596)

at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:545)

at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)

at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:590)

at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)

at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:235)

at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1607)

at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233)

at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1297)

at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188)

at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:485)

at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1577)

at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186)

at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1212)

at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)

at org.eclipse.jetty.server.handler.HandlerList.handle(HandlerList.java:59)

at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)

at org.eclipse.jetty.rewrite.handler.RewriteHandler.handle(RewriteHandler.java:322)

at org.eclipse.jetty.server.handler.HandlerList.handle(HandlerList.java:59)

at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:146)

at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)

at org.eclipse.jetty.server.Server.handle(Server.java:500)

at org.eclipse.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:383)

at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:547)

at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:375)

at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:270)

at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311)

at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)

at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:117)

at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:336)

at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:313)

at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:171)

at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:129)

at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:388)

at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:806)

at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:938)

at java.base/java.lang.Thread.run(Unknown Source)

Caused by: java.lang.IllegalArgumentException: Invalid version: "1.0-SNAPSHOT"

at com.inductiveautomation.ignition.common.model.Version.(Version.java:93)

at com.inductiveautomation.ignition.common.model.Version.parse(Version.java:252)

at com.inductiveautomation.ignition.common.modules.ModuleInfoParser.endElement(ModuleInfoParser.java:138)

at java.xml/com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.endElement(Unknown Source)

at java.xml/com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanEndElement(Unknown Source)

at java.xml/com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl$FragmentContentDriver.next(Unknown Source)

at java.xml/com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(Unknown Source)

at java.xml/com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(Unknown Source)

at java.xml/com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(Unknown Source)

at java.xml/com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(Unknown Source)

at java.xml/com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(Unknown Source)

at java.xml/com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.parse(Unknown Source)

at java.xml/com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl$JAXPSAXParser.parse(Unknown Source)

at com.inductiveautomation.ignition.common.modules.ModuleInfoParser.fromXML(ModuleInfoParser.java:242)

at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.parseModuleInfo(ModuleManagerImpl.java:1701)

followed by

ModuleInstallPage 18Jan2022 09:52:57 java.lang.Exception: Exception parsing "module.xml"

java.lang.Exception: Exception parsing "module.xml"

at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.parseModuleInfo(ModuleManagerImpl.java:1703)

at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.extractModuleInfo(ModuleManagerImpl.java:1696)

at com.inductiveautomation.ignition.gateway.web.pages.config.ModuleInstallPage.eulaCertCheck(ModuleInstallPage.java:120)

at com.inductiveautomation.ignition.gateway.web.pages.config.ModuleInstallPage$1.onSubmitInternal(ModuleInstallPage.java:71)

at com.inductiveautomation.ignition.gateway.web.components.CsrfPreventingForm.onSubmit(CsrfPreventingForm.java:67)

at org.apache.wicket.markup.html.form.Form$9.component(Form.java:1248)

at org.apache.wicket.markup.html.form.Form$9.component(Form.java:1242)

at org.apache.wicket.util.visit.Visits.visitPostOrderHelper(Visits.java:274)

at org.apache.wicket.util.visit.Visits.visitPostOrder(Visits.java:245)

at org.apache.wicket.markup.html.form.Form.delegateSubmit(Form.java:1241)

at org.apache.wicket.markup.html.form.Form.process(Form.java:923)

at org.apache.wicket.markup.html.form.Form.onFormSubmitted(Form.java:769)

at org.apache.wicket.markup.html.form.Form.onFormSubmitted(Form.java:702)

at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)

at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)

at java.base/java.lang.reflect.Method.invoke(Unknown Source)

at org.apache.wicket.RequestListenerInterface.internalInvoke(RequestListenerInterface.java:258)

at org.apache.wicket.RequestListenerInterface.invoke(RequestListenerInterface.java:216)

at org.apache.wicket.core.request.handler.ListenerInterfaceRequestHandler.invokeListener(ListenerInterfaceRequestHandler.java:240)

at org.apache.wicket.core.request.handler.ListenerInterfaceRequestHandler.respond(ListenerInterfaceRequestHandler.java:226)

at org.apache.wicket.request.cycle.RequestCycle$HandlerExecutor.respond(RequestCycle.java:814)

at org.apache.wicket.request.RequestHandlerStack.execute(RequestHandlerStack.java:64)

at org.apache.wicket.request.cycle.RequestCycle.execute(RequestCycle.java:253)

at org.apache.wicket.request.cycle.RequestCycle.processRequest(RequestCycle.java:210)

at org.apache.wicket.request.cycle.RequestCycle.processRequestAndDetach(RequestCycle.java:281)

at org.apache.wicket.protocol.http.WicketFilter.processRequest(WicketFilter.java:188)

at org.apache.wicket.protocol.http.WicketFilter.doFilter(WicketFilter.java:245)

at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1596)

at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:545)

at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)

at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:590)

at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)

at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:235)

at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1607)

at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233)

at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1297)

at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188)

at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:485)

at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1577)

at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186)

at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1212)

at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)

at org.eclipse.jetty.server.handler.HandlerList.handle(HandlerList.java:59)

at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)

at org.eclipse.jetty.rewrite.handler.RewriteHandler.handle(RewriteHandler.java:322)

at org.eclipse.jetty.server.handler.HandlerList.handle(HandlerList.java:59)

at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:146)

at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)

at org.eclipse.jetty.server.Server.handle(Server.java:500)

at org.eclipse.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:383)

at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:547)

at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:375)

at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:270)

at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311)

at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)

at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:117)

at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:336)

at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:313)

at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:171)

at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:129)

at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:388)

at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:806)

at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:938)

at java.base/java.lang.Thread.run(Unknown Source)

Caused by: java.lang.IllegalArgumentException: Invalid version: "1.0-SNAPSHOT"

at com.inductiveautomation.ignition.common.model.Version.(Version.java:93)

at com.inductiveautomation.ignition.common.model.Version.parse(Version.java:252)

at com.inductiveautomation.ignition.common.modules.ModuleInfoParser.endElement(ModuleInfoParser.java:138)

at java.xml/com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.endElement(Unknown Source)

at java.xml/com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanEndElement(Unknown Source)

at java.xml/com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl$FragmentContentDriver.next(Unknown Source)

at java.xml/com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(Unknown Source)

at java.xml/com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(Unknown Source)

at java.xml/com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(Unknown Source)

at java.xml/com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(Unknown Source)

at java.xml/com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(Unknown Source)

at java.xml/com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.parse(Unknown Source)

at java.xml/com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl$JAXPSAXParser.parse(Unknown Source)

at com.inductiveautomation.ignition.common.modules.ModuleInfoParser.fromXML(ModuleInfoParser.java:242)

at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.parseModuleInfo(ModuleManagerImpl.java:1701)

Not sure which of the POM files or other files you would need to see from me to trouble shoot this but let me know and I can show you anything.

Probably just the root POM. You're specifying a version that's not a valid semantic version - should be 1.0.0-SNAPSHOT, or something similar - major.minor.patch.

1 Like

Ok I changed it in this POM file only

to

    <groupId>org.example</groupId>
    <artifactId>DateSelector</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>pom</packaging>

And now when I run mvn package I get

[INFO] Scanning for projects...
[ERROR] [ERROR] Some problems were encountered while processing the POMs:
[FATAL] Non-resolvable parent POM for org.example:DateSelector-build:1.0-SNAPSHOT: Could not find artifact org.example:DateSelector:pom:1.0-SNAPSHOT and 'parent.relativePath' points at wrong local POM @ line 7, column 13
[FATAL] Non-resolvable parent POM for org.example:DateSelector-client:1.0-SNAPSHOT: Could not find artifact org.example:DateSelector:pom:1.0-SNAPSHOT and 'parent.relativePath' points at wrong local POM @ line 7, column 13
[FATAL] Non-resolvable parent POM for org.example:DateSelector-designer:1.0-SNAPSHOT: Could not find artifact org.example:DateSelector:pom:1.0-SNAPSHOT and 'parent.relativePath' points at wrong local POM @ line 7, column 13
 @
[ERROR] The build could not read 3 projects -> [Help 1]
[ERROR]
[ERROR]   The project org.example:DateSelector-build:1.0-SNAPSHOT (/mnt/c/Users/bkarabinchak/IdeaProjects/DateSelector/DateSelector-build/pom.xml) has 1 error
[ERROR]     Non-resolvable parent POM for org.example:DateSelector-build:1.0-SNAPSHOT: Could not find artifact org.example:DateSelector:pom:1.0-SNAPSHOT and 'parent.relativePath' points at wrong local POM @ line 7, column 13 -> [Help 2]
[ERROR]
[ERROR]   The project org.example:DateSelector-client:1.0-SNAPSHOT (/mnt/c/Users/bkarabinchak/IdeaProjects/DateSelector/DateSelector-client/pom.xml) has 1 error
[ERROR]     Non-resolvable parent POM for org.example:DateSelector-client:1.0-SNAPSHOT: Could not find artifact org.example:DateSelector:pom:1.0-SNAPSHOT and 'parent.relativePath' points at wrong local POM @ line 7, column 13 -> [Help 2]
[ERROR]
[ERROR]   The project org.example:DateSelector-designer:1.0-SNAPSHOT (/mnt/c/Users/bkarabinchak/IdeaProjects/DateSelector/DateSelector-designer/pom.xml) has 1 error
[ERROR]     Non-resolvable parent POM for org.example:DateSelector-designer:1.0-SNAPSHOT: Could not find artifact org.example:DateSelector:pom:1.0-SNAPSHOT and 'parent.relativePath' points at wrong local POM @ line 7, column 13 -> [Help 2]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/ProjectBuildingException
[ERROR] [Help 2] http://cwiki.apache.org/confluence/display/MAVEN/UnresolvableModelException

The inner pom files still all have the the version as 1.0-SNAPSHOT.

Not a maven guy - maybe you will need to bump all the inner versions too.

1 Like

I had to do replace in files to replace every instance but then it worked. I was able to load it. Though I don’t see anything when I drag it into a project
image

Here is my current code. Should I be extending AbstractVisionPanel instead and attaching everything to that panel instead of what making a JPanel inside of AbstractVisionComponent and adding to that?

Also, not sure what I am supposed to do in my onShutdown - perhaps nothign yet since it’s so barebones. I want to make it look and appear correct before I start adding the logic.

Code:

package org.example.client;

import com.inductiveautomation.factorypmi.application.components.PMIDateTimePopupSelector;
import com.inductiveautomation.vision.api.client.components.model.AbstractVisionComponent;
import net.miginfocom.swing.MigLayout;

import javax.swing.*;
import java.awt.*;
import java.util.Date;

//Should this be extending AbstractVisionPanel instead? 
public class DateSelectorComponent extends AbstractVisionComponent {

    private JPanel container;
    private JLabel dropdownLabel;
    private JComboBox<String> dateSelections;
    private JLabel startDateLabel;
    private JLabel toLabel;
    private PMIDateTimePopupSelector startDate;
    private PMIDateTimePopupSelector endDate;
    private String[] dateSelectionOptions = {"All", "Today", "This Week", "This Month", "This Year", "Last Week", "Last Month", "Last Year", "Last 3 Months", "Last 6 Months", "Last 12 Months", "Two Week Period", "Post Sept. 2014"};

    public DateSelectorComponent() {
        //Leftover from archetype example.  setPreferredsize seems to do stuff, but unsure what else is appropriate to go here over onStartup.
        setFont(new Font("Dialog", Font.PLAIN, 16));
        setPreferredSize(new Dimension(250, 100));
    }

    protected void onStartup() {
        container = new JPanel(new MigLayout("wrap 4"));
        //Dropdown row
        dropdownLabel = new JLabel("Date:");
        dateSelections = new JComboBox<>(dateSelectionOptions);
        container.add(dropdownLabel, "grow");
        container.add(dateSelections, "span");
        //Datetime row
        startDateLabel = new JLabel("Start Date:");
        startDate = new PMIDateTimePopupSelector();
        toLabel = new JLabel("to");
        endDate = new PMIDateTimePopupSelector();
        container.add(startDateLabel, "grow");
        container.add(startDate, "grow");
        container.add(toLabel, "grow");
        container.add(endDate, "grow");
        //Is this necessary?
        container.setVisible(true);
    }

    public void onShutdown() {

    }

//    public Date getStartDate() {
//        //TODO implement later
//        return new Date();
//    }
}

You’re creating a container, but not actually adding that container to your primary component.

I would say you can probably put all of your field initializations into the constructor (and make them final, IntellIJ will probably tell you do that anyways).

Then at the end of the constructor, make sure to this.add(container) or similar.

onStartup is more useful for stuff like creating polling mechanisms or RPC calls or more complex stuff. You also want to be careful about e.g. setPreferredSize in the constructor - if you look at the window XML now, you should see a call to your constructor, and a setFont and setPreferredSize method call below that. It’s fine for generic JComponent methods, but when you start adding your own bean properties it can get tricky. You can also implement DesignerInitializable to have a designer-specific runtime hook.

Whoops. I make this mistake too often in swing.

I did what you said re: putting it all in the constructor. I also had to change it to extends AbstractVisionPanel instead of AbstractVisionComponent - the former works but the latter does not.

However, having said that -

image

It's alive! Now for the fun part of programming date logic :sweat_smile:

On that note though - as these date calculations will take place when a new dropdown selection is made - where would I put my JComboBox ActionListener? The constructor? onStartup?

Here's the current code (onStartup and onShutdown are empty)

public class DateSelectorComponent extends AbstractVisionPanel {

    private final JPanel container;
    private final JLabel dropdownLabel;
    private final JComboBox<String> dateSelections;
    private final JLabel startDateLabel;
    private final JLabel toLabel;
    private final PMIDateTimePopupSelector startDate;
    private final PMIDateTimePopupSelector endDate;
    private final String[] dateSelectionOptions = {"All", "Today", "This Week", "This Month", "This Year", "Last Week", "Last Month", "Last Year", "Last 3 Months", "Last 6 Months", "Last 12 Months", "Two Week Period", "Post Sept. 2014"};

    public DateSelectorComponent() {
        //Leftover from archetype example.  setPreferredsize seems to do stuff, but unsure what else is appropriate to go here over onStartup.
        setFont(new Font("Dialog", Font.PLAIN, 16));
//        setPreferredSize(new Dimension(250, 100));
        container = new JPanel(new MigLayout("wrap 4"));
        //Dropdown row
        dropdownLabel = new JLabel("Date:");
        dateSelections = new JComboBox<>(dateSelectionOptions);
        container.add(dropdownLabel, "grow");
        container.add(dateSelections, "span");
        //Datetime row
        startDateLabel = new JLabel("Start Date:");
        startDate = new PMIDateTimePopupSelector();
        toLabel = new JLabel("to");
        endDate = new PMIDateTimePopupSelector();
        container.add(startDateLabel, "grow");
        container.add(startDate, "grow");
        container.add(toLabel, "grow");
        container.add(endDate, "grow");
        //Is this necessary?
        container.setVisible(true);
        this.add(container);
        //not sure if the following is necessary
        this.setVisible(true);
    }

Exited this thing's actually coming to fruition now - thanks for all the help.

The constructor is fine; it's a permanent part of the component and it doesn't make sense to have a component without the listener wired up.

If you want a slightly more ergonomic Swing experience, you can pass a lambda to any of the static methods on the Listen class, e.g:

Listen.toCombobox(dateSelections, newValue -> updateDropdowns(newValue));

And, no, you don't have to explicitly setVisible(true) on your components/containers.

1 Like