Blog

Follow-up to the Java update post

We had a great response to our recent post on Oracle’s new Java 7 auto-updates, their silent removal of Java 6, and the problems that can cause. We had coverage in The Register, InfoQ, and DZone. The Register article in particular had a great comment thread, and I urge you to check it out.

At the end of that thread, I wrote a response to several points that were brought up, and I thought I’d post a version of that response here.

Why not just support Java 7 and be done with it? Why make people use Java 6?

Our product does handle Java 7 (and 6, and 5, etc — our stuff works with Java back to 1.3.1, although we’ll probably move that up to Java 5 in the next release) just fine. But it’s a tool that customers use to run and deploy their own software — it allows .NET code to communicate with Java code. The Java runs in its own JVM, and the users get to choose whichever JRE they want — it can be any version, it can be 32-bit or 64-bit. It can be from just about any vendor. That’s a good thing, because our users have their own environments, and it’s their own business — we don’t dictate or judge. So, the problem isn’t ours (we’re not making people use Java 6 — but our customers might choose to use Java 6), except that our customers’ problems become our problems, and then we have to scramble. But it bothers me when we have to scramble to solve a problem that really wasn’t caused by us, and which really shouldn’t have been a problem to begin with.

Why not just get version of the latest installed Java from the registry and use that?

The problem is that that only tells us what Java is on the machine — it doesn’t tell us what Java the user wants or needs. Again, we let the user make that decision — checking the registry won’t tell us what we want to know. (Nor will JAVA_HOME, as someone else suggested.)

Why would an enterprise user allow auto-updates, when unexpected things can clearly happen?

The short answer is that they shouldn’t. But clearly it happens — it happened to the customers of our customer. (Our customer is an ISV that uses our product. Their customers are the end users.) And when it happened, our customer heard about it from their customer, and called us, and we had to scramble, and the problem was easily corrected, but it shouldn’t have been a problem in the first place.

Why not just supply the jvm.dll?

First, because it should be up to our users to determine which version they need — we can handle just about any one chosen and don’t dictate. Second, because jvm.dll doesn’t work in isolation and we’d have to supply an entire private JRE — it’s much more than a single file.

Finally, I just want to point out that in our case, the problem is just the validity of a file path — Java 6 and Java 7 reside in different places, and a single path won’t work with both. However, the comment thread on The Register’s article has certainly come up with plenty of examples of Java software that works with Java 6 that simply won’t work with Java 7, so for other users this is a much bigger issue than just an invalid file path.

JNBridgePro and Windows 8: It already works

We were at Microsoft’s Build conference in Anaheim a few weeks ago, where they unveiled their upcoming Windows 8 operating system.  In case you hadn’t caught the news, Windows 8 contains two distinct user experiences: a traditional “Desktop” experience which resembles Windows 7, and a new, touch-centric “Metro” experience.  The Desktop experience allows you to access the full .NET Framework as before; Metro applications run in a much more restricted runtime environment.

We’ve spent some time with Windows 8, and we’re happy to report that JNBridgePro, as it’s currently released, already works just fine with Windows 8 in desktop mode. So, if you’re already using JNBridgePro and want to move your application to Windows 8, or to create a new application for the Windows 8 desktop, JNBridgePro is as easy to use as always.

We’re also happy to report that the JNBridgePro plug-in for Visual Studio will work with the upcoming Visual Studio 11 Developer Preview with only minor changes, which will be incorporated in an upcoming JNBridgePro release.  This means that JNBridgePro will be ready for the new Visual Studio by the time VS 11 is released, and likely sooner.  In the meantime, if you want to use JNBridgePro in conjunction with VS 11 development, we recommend using the standalone proxy generation tool.  If you’d like to be a tester for the JNBridgePro plug-in for VS 11, please contact us.

As might be expected, Metro-style development, along with the more restrictive WinRT and new .NET Metro profile, offer a few challenges.  During the run-up to the Windows 8 release, we will be addressing those challenges.

We’d be interested to know whether any customers or prospective customers are planning to produce Metro apps, and whether they anticipate building .NET/Java interoperability into those applications.  If you are planning to produce such applications, we’d like to hear from you, and work with you, at this early stage in the Windows 8 product cycle — please contact us.

Over the coming months, we’ll periodically post new blog entries discussing interesting technical aspects of Metro and WinRT, as they affect interoperability.  We expect to learn a lot during this process, and we look forward to sharing it with you.

Announcing JNBridgePro v6.1

We’re excited to announce the release of JNBridgePro v6.1. The new version connects Java and the .NET Framework both on the ground and in the cloud, and has all the features of previous versions of JNBridgePro, plus the following significant new features:

  • We’ve added support for the new Visual Studio 2012 and .NET Framework 4.5. The JNBridgePro Visual Studio plugin will now work just as well in VS 2012 as it does in VS 2005, 2008, and 2010.
  • We also now support all versions of Eclipse from version 3.2 up through 4.2, the latest version.
  • JNBridgePro now supports cross-platform access of protected object and class members. Previously, only access of public members was supported. This will provide more extensive support for extending Java or .NET classes on the other platform.
  • Java code can now call .NET methods that contain ref or out parameters, thereby making even more .NET code accessible cross-platform. While this was previously possible through a workaround, it can now be done directly.
  • The JNBridgePro installers are now signed. This will help enterprise users whose IT organizations’ policies require signed installers.

The new JNBridgePro v6.1 is available immediately for download.

The full announcement can be found here.

Customer questions answered: Handling optional parameters in JNBridgePro

We received a support question recently concerning methods in .NET with optional parameters, and how to call them from Java. This is something that I think would be of interest to a number of our users.

If a method has optional parameters, they can be left out of the arguments when the method is called. For example, in the method

Microsoft.VisualBasic.Collection.Add(Object Item, string key, Object Before, Object After),

the last three parameters are optional and can be omitted. Optional parameters are most common in Visual Basic, although they also appear in C# starting with C# 2010. They can be used in methods, indexers, constructors, and delegates.

The original support question asked how one would call Collection.Add() when one wanted to leave out one or more of the optional parameters. When Collection.Add() is proxied, only the four-parameter version is proxied. Making the call with only, say, two parameters leads to a compilation error.

In order to know what to do when calling methods with optional parameters from Java, it makes sense to first understand what is really happening when such methods are called. If a method is called without an argument for an optional parameter, a default value is passed in place of the missing argument. The default value is chosen according to the following rules:

  • If the parameter is of type Object, the default value is Type.Missing
  • If the parameter is any other reference type, the default value is null
  • If the parameter is a value type, the default of the particular value type is passed

In the case of Collection.Add(), the full four-parameter version of the method is always called (it’s the only one that actually exists), and the compiler automatically and transparently inserts default values for the missing arguments.

Knowing that, it’s easy to see what to do when calling such methods from Java. Call the full-parameter version of the method (which is what has been proxied), and use the default values for any parameters whose values you ordinarily wouldn’t pass. In the case of Object parameters, pass Type.Get_Missing() – System.Type is always proxied, and Missing is a property, so it’s accessed using its getter method. Note that since the proxied Type class is in the System.* namespace, you’ll need to do something special to avoid compilation errors – see the knowledge base article here for more information.

Do you have a question about how to do something using JNBridgePro? Contact us at info@jnbridge.com.

Customer questions answered: Cross-platform method overrides in JNBridgePro

Here’s another interesting question from the support inbox.

Let’s say that you have a .NET class that contains a virtual method, and you want to override it in your Java code. The immediate answer would seem to be to proxy the .NET class, then write a Java class that extends the proxy class and override the method in the Java subclass. The problem is that while this will certainly override the method when the Java class is accessed from Java code, if the Java object is passed back to the .NET side – for example, as an argument in a method call – the new Java method won’t be seen; any calls to the virtual method will be handled by the original .NET class’s method.

Overrides are not yet directly supported by JNBridgePro, but there is a way to work around this issue and allow the underlying .NET code to see the Java-based override method, through the use of callbacks. Note that while this example assumes a Java-to-.NET project, the equivalent thing can be done in .NET-to-Java projects.

Let’s say that you have a C# class:

public class C
{

public virtual int myVirtualMethod(int i)
{

// code goes here

}

}

You will need to write some additional C# code:

public delegate int OverrideMethod(int i);

public class C2 : C
{

public OverrideMethod overrideMethod = null;

public override int myVirtualMethod(int i)
{

if (overrideMethod == null)
{

return base.myVirtualMethod(i);

}
else
{

return overrideMethod(i);

}

}

}

The class C2 must be a concrete class — that is, it cannot be abstract. This is important: you must be able to instantiate it, or you will see a different problem.

If you don’t have access to the C# source code so that you can modify the original dll, place C2 in a new dll that you create, and which you include in your application.

Then, proxy C, C2, and OverrideMethod, and make your new Java class extend C2.  As part of the new Java class’s constructor, create and register a callback for overrideMethod:

public class MyOverrideMethod implements OverrideMethod
{

public int Invoke(int i)
{

// the override code goes here

}

}

public class J extends C2
{

public J()
{

super.set_overrideMethod(new MyOverrideMethod());

}

}

Now, the overrides should work as expected.

Do you have a question about how to do something using JNBridgePro? Contact us at info@jnbridge.com.

Build 2012 Recap

We were in Redmond last week for the Build conference, where Microsoft offered deep dives into their latest technologies. Unlike last year, where the emphasis was on in-depth looks at lower-level technologies like Windows RT, .NET 4.5, and Windows 8 internals, this year’s conference concentrated on higher-level application-oriented APIs like Microsoft Account (formerly Windows Live ID) and Windows Store, as well as more peripheral (to us) technologies like Windows Phone 8. In fact, if we had seen the list of sessions in advance, we might have decided to skip the conference and watch any relevant sessions online (although it was nice to receive the Surface RT, Nokia Lumia 920, and 100GB of SkyDrive capacity that all attendees received). Even so, there were a number of interesting sessions that were relevant to our work on interoperability. (All Build sessions can be seen here: http://channel9.msdn.com/events/build/2012.)

There was an interesting session that unveiled details of Microsoft’s previously announced Hadoop on Windows Azure offering (now called HDInsight). Since the offering has been by invitation only, there haven’t been too many details. It’s interesting to contrast Microsoft’s approach to Hadoop/.NET integration, which uses .NET streaming but conceals it with the artful use of wrappers, with our approach of direct API calls through JNBridgePro (here and here). Each approach can be useful in certain situations.

Microsoft offered more details on their new Windows Azure Virtual Machines, which brings to Windows Azure the capabilities already found in Amazon’s EC2. Microsoft claims advantages over Amazon’s offerings, particularly in the areas of administration and automation. For us and for our users, this is interesting because it makes it even easier to create applications that use JNBridgePro and deploy them to Windows Azure. It had been possible, but there were a number of complexities in setting up and starting the applications in the cloud; now it’s as easy in Windows Azure as it’s already been with Amazon EC2. In addition, Microsoft will be offering virtual machine images containing BizTalk Server 2010 R2 CTP, and you will be able to use JNBridge’s JMS adapter for BTS with those images.

A talk on the evolution of .NET covered both the history of the platform, including all of the earlier milestones, and possible future directions in which the platform can go. The speaker made the very interesting point that the typical PC of 1998 (when the .NET project began) or even 2000 (when it was unveiled) is very different from the typical PC of today, in terms of processing power, memory and storage, user interface, and connectivity, and any future .NET implementations will need to reflect that. We can only wonder what that will entail, but it’s encouraging to learn that Microsoft still considers .NET to be an essential platform for their future offerings.

One of the more surprising things we learned had to do with Windows Phone 8, which we really hadn’t been tracking, since it didn’t seem relevant to our mission. Windows Phone 8’s runtime is actually a version of the .NET CLR called CoreCLR, which is really based on the existing SilverLight CLR. We haven’t supported SilverLight, both because of its slow adoption, and because it has been constrained in what it can do, but we were interested to learn that in response to requests from developers, the CoreCLR will allow Windows Phone 8 applications to access existing native (read C++) gaming engines. Since Java Runtime Environments are also native C++ libraries, does that mean that a JVM can be hosted in a Windows Phone 8 app’s process? If so, it might be possible to support shared memory interoperability in Windows Phone 8 applications. It’s certainly something we’ll be looking into. Will it be possible to do something similar in Windows 8 “Metro” apps? That remains to be seen.

Did you attend Build, or watch sessions online? If so, did you see something that you’d like to call our attention to? If so, please let us know in the comments.

Groovy-to-.NET integration

Lately, much of the action in the Java world has been in the development and use of non-Java languages that run on the JVM. That’s why we were pleased to hear from a customer who wanted to use JNBridgePro to call .NET assemblies from code written in Groovy.

For those of you unaware, Groovy is a dynamic language that runs on the JVM. Groovy classes compile to Java binary .class files, and Groovy code can call (and be called from) any conventional Java class. In our customer’s case, they wanted to call .NET classes from Groovy, which really meant calling proxies. Since proxied .NET classes are just Java classes, everything should just work, and it did.

It’s really easy to illustrate how this is done. Let’s assume we have a very simple test class written in C#:

namespace GroovyTestDotNetSide
{
   public class SimpleObject
   {
      public SimpleObject(int i)
      {
         Console.WriteLine("In SimpleObject.ctor: value supplied is " + i);
      }
   }
}

First, we proxy it and generate a proxy jar file proxies.jar. Now, if we create a Groovy project and include proxies.jar in the build classpath, along with jnbcore.jar, bcel-5.1-jnbridge.jar (all of which would be included in any Java-to-.NET project using JNBridgePro), and groovy-all-1.7.5.jar (or something similar, depending on which version of Groovy is being used), we can write Groovy code to instantiate SimpleObject:

import GroovyTestDotNetSide.SimpleObject
import com.jnbridge.jnbcore.DotNetSide

class SimpleTest {

   static main(args) {

   // set up JNBridge
   DotNetSide.init(args[0])

   def simpleObject = new SimpleObject(3)

   println "Done!"
}
}

Just as in a conventional Java-to-.NET project, you need to initialize the Java side through a call to DotNetSide.init(), supplying the path to the Java-side properties file, or an equivalent Properties object. Once that is done, you can make any valid call to a proxy. In this case, we instantiate SimpleObject, but we can also call static or instance methods of SimpleObject, or access SimpleObject fields or properties or events.

Once the project is build, you will have a binary file SimpleTest.class, and you can run the application in the usual way (assuming all the jar, .class, and .properties files are in the current folder):

java -cp “proxies.jar;jnbcore.jar;bcel-5.1-jnbridge.jar;groovy-all-1.7.5.jar;. ” SimpleTest props.properties

The properties file can configure for tcp/binary, http/soap, or shared memory; if you are using tcp/binary or http/soap, you will or course need to run a properly configured .NET side.

As you can see, this just works in exactly the same way you’d expect if you were using Java.

While we have not yet tried it, interoperability with Groovy should also work in the .NET-to-Groovy direction. Similarly, JNBridgePro should also work when integrating .NET with other JVM-based languages like Scala, JRuby, and Jython.

Are you thinking about using JNBridgePro with JVM-based languages other than Java? If so, let us know in the comments or by sending us email at info@jnbridge.com.

Looking to the future

Our tenth anniversary festivities will soon be drawing to a close. Past installments on this blog have looked toward the past or the present, and now we’d like to spend a little bit of time thinking about the future.

Specifically, where do you think we should take JNBridge next? There are lots of scenarios where we can potentially apply JNBridgePro interoperability technologies, but we’d like to learn more about interoperability challenges you are facing, or new technologies that you might be using in the future that could present obstacles to interoperability. Here’s a list of technologies where we could conceivably extend the JNBridge footprint. Are these scenarios that might be of interest to you?

    • Metro and WinRT: Are you thinking about writing Metro or WinRT apps that need to call Java? How about Java apps that need to access Metro/WinRT DLLs or Portable Class Libraries?

 

    • Mono: Would you like to write Mono applications that call Java libraries, or Java applications that call .NET code running on Mono? Would the Mono be running on Windows, Linux, Mac OS X, or some other platform?

 

    • Mobility: There are lots of possible scenarios here:

 

      • Windows Phone 8: Are you considering writing WP8 apps that might call Java libraries? Would you want that Java to reside on a server, or on the phone?

 

      • Android: How about calling .NET libraries from Android’s Java? Or calling Java libraries from .NET/Mono running on Android?

 

      • iOS: Is there any kind of interoperability scenario involving iOS that you’re considering?

 

    • JVM-based dynamic languages: In a previous blog post, we talked about Groovy-to-.NET integration, and how it just works. Are there other interoperability scenarios involving dynamic languages that you’d like to see?

 

    • JavaScript and Node.js: JavaScript is a different platform than Java, but that doesn’t mean that there isn’t a place for JNBridgePro-style integration. Do you have any prospective projects involving integration between JavaScript or Node.js and .NET?

 

  • Additional Microsoft products: Our BizTalk adapter for JMS is very popular, but it’s not the only Microsoft product where JNBridge technologies could be used for interoperability. Are you thinking of integrating Java code with SharePoint? System Center Operations Manager? Visual Studio Tools for Office (VSTO)? Windows Azure?

The sky’s the limit: Any other interoperability scenarios you’d like to see us tackle? Please drop us a note, either to info@jnbridge.com or in the (moderated) comments below.

Java 7 update “silently” deletes Java 6, breaks applications

Software updates shouldn’t do unexpected things. They particularly shouldn’t remove software other than what they’re ostensibly updating, and they shouldn’t break running applications. It’s even worse when this all happens automatically and without warning.

The other day, one of our customers, an ISV that uses JNBridgePro in one of their applications that includes both Java and .NET, told us that several of their customers had reported that their applications stopped working after the customers updated their installations of Java 7. The strange thing is that the applications didn’t use Java 7; they used Java 6. The problem was fixed by reconfiguring JNBridgePro on those machines to point to Java 7 rather than Java 6. Our customer asked us whether JNBridgePro had problems with this update, or with Java 7. We answered that there should be no problem: JNBridgePro works fine with both Java 6 and Java 7, including the latest updates.

Something odd was going on, so we started digging deeper. Running the auto-installer for the new Java 7 update, we saw the following screen, with the relevant message buried in it:

Then we found the following notice on the Oracle website:

About the Java 6 Auto-Update to Java 7

Oracle will start auto-updating Windows 32-bit, Java Runtime Environment (JRE) users from JRE 6 to JRE 7 in December 2012.

The Java auto-update mechanism is designed to keep Java users up-to-date with the latest security fixes. To achieve this goal Windows users that rely on Java’s auto-update mechanism will have their JRE 6 replaced with JRE 7.

In December 2012 Oracle will start to auto-update a sample of users from JRE 6 to JRE 7 to evaluate the auto-update mechanism, user experience and seamless migration. Oracle will then start auto-updating all Windows 32-bit users from JRE 6 to JRE 7 with the update release of Java, Java SE 7 Update 11 (Java SE 7u11), due in February 2013.

    • JRE 7 has been the default version on Java.com since April 2012 and is now being used by millions of users.
    • As we did when JRE 5 was replaced by JRE 6, we will auto-update users of the older release to the newer version of Java.
    • As always, all users are encouraged to update to the most recent Java versions available for public download.
    • In February 2011 Oracle announced the End of Public Updates for their Java SE 6 products for July 2012. In February 2012 Oracle extended the End of Public Updates for 4 months, to November 2012. See:

• Oracle is now extending the End of Public Updates again for 4 additional months to provide developers and users with additional time to migrate to Java 7. The last publicly available release of Java 6 will be in February of 2013 with the release of Java SE 6 Update 39 (Java SE 6u39).

Java 6 End of Public Updates extended to February 2013

(Emphasis ours.)

This is absolutely astonishing. Oracle has decided that, in order to fix extensively-reported security problems, they will not only update Java 7 (their latest version of Java), they will also completely delete a completely separate product. Yes, Java 6 is a separate product from Java 7. They can be installed side-by-side, and many users have both Java 6 and Java 7 installed on their machines. Some of their applications depend on Java 6, and others might depend on Java 7, and these dependencies are typically hard-coded or configured to point to the correct, and different, file locations. Can you imagine if Microsoft released an update to .NET 4.0 that also removed .NET 2.0? This is just as serious.

Worse, it appears that they are taking it upon themselves to replace installations of Java 6 with Java 7 even if the users have only Java 6 on their machines.

How is this different from, say, Microsoft updating Office by replacing one version by another? That’s an update-in-place, so hard-coded paths will often still work. Even so, updating one version of Office to another likely won’t involve an auto-update, but rather an explicit re-installation, and one would expect dependencies to break. Java 6 and 7, on the other hand, are side-by-side installations, and one doesn’t expect an update to one to affect the other in any way.

Let’s look at this from Oracle’s point of view. The security holes that they plugged in Java 7 likely also exist in Java 6, and they have stopped providing new updates to Java 6. Why not replace Java 6 with Java 7 and fix these problems?

This strategy might make sense for less sophisticated users who only use Java inside their browsers. They likely do not know which version of Java they have, or even if they Java at all. In this case, it makes sense to fix the problem by updating the Java installations, so that machines aren’t infected with malware by visiting rogue websites.

However, most of our customers aren’t using Java in their browsers. Their Java is running on servers, or in self-contained desktop applications that, if they connect to the Internet, only connect to specific sites. Their applications depend on specific versions of Java, or on Java files being in specific places. JNBridgePro’s shared memory mechanism, in particular, depends on an absolute path to a specific jvm.dll, but that’s not the only case where dependencies like this occur. With their update, Oracle has silently pulled the rug out from under many running applications.

Why do I say “silently”? Even though the update installer mentions that Java 6 “might” be removed, and the notice on the website says it “will” be removed, very few people will read the text in the installer; they will likely just click through it, since nobody expects a Java 7 updater to remove Java 6. And almost nobody will read the notice on the website unless they are specifically searching for it.

One could say that IT shops should turn off automatic updates, and apply updates in a controlled process after extensive testing. That’s true, but clearly auto-updates can still happen; it’s not reasonable to assume that all business users have sufficient IT support. After all, it happened to our customer’s customers. It’s also the case that a situation like Java 6 being removed in a Java 7 update might not be found in a controlled test, since most such tests will only try to see whether applications that use Java 7 will be affected. To make matters more difficult, the mechanism for turning off automatic Java updates isn’t obvious.

If you find yourself with a broken application that uses JNBridgePro and Java 6 after updating Java 7, here’s what you can do:

  • You can reconfigure your application (and particularly the JNBridgePro component) to use Java 7 rather than Java 6. JNBridgePro will have absolutely no problem with Java 7. Whether your Java code will work with Java 7 is something that you will need to determine yourself.
  • You can go to the Oracle Java website and download and reinstall Java 6. Then, you’ll be back where you were.

Finally, you should strongly consider turning off automatic Java updates. As I said it isn’t immediately obvious how to do this, since the Java control panel, by default, doesn’t display the Update tab that contains the switch that turns off updating. The Update tab only appears when the control panel is run as administrator. You can turn off the auto-update switch as follows:

  1. In Windows Explorer, navigate to your JRE’s bin folder (for example, C:Program Files (x86)Javajre7bin, although it might be different on your machine).
  2. Once you’re there, find javacpl.exe. Right-click on it, and select “Run as administrator.”
  3. Inside the control panel, you can now see the Update tab. Select it, then uncheck the “Check for Updates Automatically” checkbox.

The control panel will ask if you really want to do this. Trust me, you do. Then click on the OK button.

Note that if you do this, it’s your responsibility to make sure that your Java installations are up to date, and that you engage in good security practices. You will need to keep track of the latest Java security problems and the latest updates when they become available, and you can download them from Oracle’s Java site. The downloaded updaters will only update the specific Java versions, and they won’t pull the rug out from under you by removing completely different versions of Java that your software might depend on.

In summary, Oracle’s latest automatic Java update is dangerous and irresponsible because it “silently” removes software other than the software it ostensibly updates, thereby breaking running code. By all means update and secure the Java running inside browsers, but leave our server and desktop software alone.

JNBridgePro 6.0 is available! Integrate Java & .NET in the cloud.

Whew! We’ve made our committed deadline: JNBridgePro version 6.0 is now available for download.

This new version supports Java/.NET interoperability projects where one or both of the end points are in the cloud. JNBridgePro 6.0 enables you to build and distribute integrated applications anywhere, including:

  • Intra-cloud, where both end points reside in the same cloud, either in the same or separate instances
  • Inter-cloud, where the instances belong to different clouds — even across cloud vendors
  • Ground-to-cloud and cloud-to-ground, where one end point is in a cloud instance and the other is an application running on the ground

JNBridgePro 6.0 extends its full set of interoperability features from the ground to the cloud, so now you can build integrated applications that run anywhere. Read more details and check out some sample use case scenarios.

You may have recently seen a wee bit of press about our launch. As Michael Coté from RedMonk says: “There’s so much valuable data and process locked in Java and .Net applications that can’t just be left behind in whatever cloud-y future is out there – and refactoring all of that to be cloud friendly would be an onerous task. Instead, you need tools that help modernize those pools.”

Try it out for yourself! We’re eager to hear what you think.