devxlogo

Extend the JDK Classes with Jakarta Commons, Part III

Extend the JDK Classes with Jakarta Commons, Part III

akarta Commons, the set of reusable classes that various Jakarta projects use, are available as separate components that you can use in your own Java projects. This final installment in a three-part series exploring Jakarta Commons presents sample applications to illustrate how to use four other useful components. (If you haven’t already, read Part I and Part II.) The examples don’t only illustrate the Commons components; they are complete, modular applications that highlight the useful features you can reuse in typical Java projects.

In particular, this installment explores the following components:

  • CLI (Command Line Interface)
  • VFS (Virtual File System)
  • Configuration
  • Pool

The article also includes the complete source code for all the sample applications. You can run it by launching the test cases for each of example with JUnit.

Author Note: A basic knowledge of object-oriented programming (OOP) and the Gang of Four design patterns (Chain and Command) will be very helpful for understanding the Commons components architecture and the examples presented here.

CLI

The CLI (Command Line Interface) component is very useful for parsing the arguments in a command-line application, since writing code to parse these arguments is time consuming and cumbersome. Also, using this component in existing CLI applications makes adding new features easy. This can be achieved by refactoring the code. (The documentation at the Jakarta site provides a pretty good higher-level overview of CLI usage.)

Part I of this series demonstrated the Commons Chain component with an example command-line tool that executed a few network commands. This installment uses the same application to demonstrate Commons CLI. You’ll find the source code for the application in the package in.co.narayanan.commons.cli of the source code download. You can launch the main class CommandLine to experiment with the application’s arguments (see Listing 1, which presents the command options in the sample application).

 Listing 1. Syntax of the Sample Application's Commands Options
// java CommandLine -user admin -password manager -ping {host} // java CommandLine -user admin -password manager -ftp {host} -get {path_to_file} // java CommandLine -user admin -password manager -ftp {host} -ls {path_to_file}

The code uses the Commons Chain and Command pattern in the processing layer. For each command-line network command, it has a class to do the processing. These classes are connected to form a chain. When a command is invoked, the first command in the chain is given the arguments. The control flows through the chain until the desired command is identified to do the processing. The goal is to use Commons CLI to parse the arguments and populate the context object, so that the processing classes in the chain can use it.

Listing 2 contains a code snippet from in.co.narayanan.commons.cli.CommandLine.java, which demonstrates configuring Commons CLI to parse the arguments to the ping command.

 Listing 2. Configuring Commons CLI for Parsing the Ping Command Arguments
private void createPingCmdOptions() { // ping command // java CommandProcessor -user admin -password manager -ping {host} pingOptions = new Options(); Option user = createUserOption(); pingOptions.addOption(user); Option passwd = createPasswordOption(); pingOptions.addOption(passwd); Option ping = OptionBuilder.withArgName("ping") .hasArg() .isRequired() .withDescription("Ping a remote system") .create("ping"); pingOptions.addOption(ping);}

The code creates instances of the Option class, which describes each of the command-line options, and adds it to the instance of the Options class. The class OptionBuilder facilitates Option class creation with a series of static methods. Listing 2 intends to create a mandatory option named ping, which has an argument. It provides a description for the option as well. The string it passes to the withArgName method will be used to fetch the value of the argument once parsing is complete. (You will see how that is done shortly.) You can directly instantiate the Option class to get more fine-grained control over the options as well.

The methods createUserOption and createPasswordOption return a new instance to represent the user and password arguments in the command line. These methods are used when creating Option class instances for other commands in the example.

Listing 3 presents the code for configuring the Commons CLI for the ftp command.

 Listing 3. Configuring Commons CLI for the ftp Command Arguments
Option ftp = OptionBuilder.withArgName("ftp") .hasArg() .isRequired() .withDescription("File transfer protocol") .create("ftp"); ftpOptions.addOption(ftp); // For additional ftp commands like ls, put, and mget, a OptionGroup needs to be created // to indicate the options are mutually exclusive Option get = OptionBuilder.withArgName("get") .hasArg() .withDescription("Get a file from the server") .create("get"); Option ls = OptionBuilder.withArgName("ls") .hasArg() .withDescription("List the folder contents in the server") .create("ls"); OptionGroup ftpArgs = new OptionGroup(); ftpArgs.addOption(get); ftpArgs.addOption(ls); ftpArgs.setRequired(true); ftpOptions.addOptionGroup(ftpArgs);

In this example, the ftp network command contains mutually exclusive get and ls commands. The code uses the OptionGroup class to represent mutually exclusive options. Listing 3 called the setRequired method of the OptionGroup class by passing true to indicate that the mutually exclusive options are mandatory. So the user will be forced to enter either of the mutually exclusive commands.

This entire configuration lets the Commons CLI framework to do the following:

  • Parse the command-line argument string array and populate the instantiated Option classes with the values passed
  • Perform validations (For instance, if the user misses a mandatory argument, an exception is thrown.)
  • Print the usage message if the user didn’t pass the right set of arguments

Listing 4 demonstrates invoking the parse method in a parser class.

 Listing 4. Initiating the Parser to Perform the Parsing
public void process(String args[]) { // remaining code CommandLineParser parser = new BasicParser(); org.apache.commons.cli.CommandLine line = null; Context chainContext = null; // remaining code case PING : { try { line = parser.parse(pingOptions, args); chainContext = getPingContext(line); } catch (ParseException e) { System.out.println(e.toString()); HelpFormatter formatter = new HelpFormatter(); formatter.printHelp("Ping options", pingOptions); } } break; // remaining code} private Context getPingContext(org.apache.commons.cli.CommandLine line) { String user = line.getOptionValue("user"); String passwd = line.getOptionValue("password"); String host = line.getOptionValue("ping"); return new CommandlineContext(user, passwd, new CLICommand("-ping", new String[] {host}));}

The highlighted code is very important for understanding how to use the parser. It begins by instantiating the appropriate parser class, using BasicParser. For Unix-type CLI commands, you need to use PosixParser. For custom requirements, you can extend the org.apache.commons.cli.Parser class or implement the org.apache.commons.cli.CommandlineParser interface to create a complete parser from scratch.

The call to the parse method returns a reference to org.apache.commons.cli.CommandLine, which can be used to fetch the command-line option values. The code then instantiates CommandlineContext and populates it with the values retrieved for passing it to the chain for further processing.

If the Commons CLI encounters a problem while parsing the arguments passed, it throws an exception in the parse method. In this case, it uses the org.apache.commons.cli.HelpFormatter class to print a formatted help message that informs the user of the error.

The Commons CLI framework enables parsing the options to only one command. If a CLI application contains commands of commands with options?like in the example application here, a bit of preprocessing needs to be done to classify the command type (see Listing 5).

 Listing 5. Classifying the Commands
private int classifyCommand(String args[]) throws CommandLineException { if(args != null && args.length > 0) { for(String arg : args) { if(arg.equals("-ping")) { return PING; } if(arg.equals("-ftp")) { return FTP; } } } else { throw new CommandLineException("Invalid command options. See usage."); } throw new CommandLineException("Invalid command options. See usage");}

In the sample application, ping and ftp are distinct commands, which can have different command-line options. Hence, it cannot represent the command-line arguments in one instance of the Options class. It initially classifies the command type and uses the appropriate Options instance for parsing further.

Commons CLI is a neat API that is essential for every Java CLI application. It saves a lot of time and simplifies further enhancements to the application as well. The Jakarta Ant project uses Commons CLI for processing command-line arguments.

VFS

Commons Virtual File System (VFS) provides an abstraction layer for accessing various file systems uniformly. This component can be configured to connect to one or more file systems at the same time. This is analogous to the mount facility in the Linux operating system.

VFS supports the following file systems:

  • Local files ? The local files and folders (file://)
  • Zip, jar, tar, tgz, tbz2, gzip, bzip2 ? Various compression formatted files that can be processed transparently (zip://, jar://, etc.)
  • CIFS ? Samba server or a Windows share (smb://)
  • FTP ? FTP server (ftp://)
  • HTTP and HTTPS (http://)
  • SFTP ? SSH or SCP server (sftp://)
  • Temporary files (tmp://)
  • WebDAV ? Web-based Distributed Authoring and Versioning (webdav://)
  • Resource from class loader ? Loading classes or other resources using the class loader (res://)

You’ll find the complete syntax for the supported protocols URI here and the uses for the API at a high level here.

This component is very useful for applications that require seamless access to various file types. For instance, a desktop search tool is a very good analogy for this framework. It lets a user search for a file or file contents in various file formats. Integrating a Windows Explorer-like feature to a Java application is another good analogy.

The sample application is a tool that uses Commons VFS for searching in folders as well as in zip and jar files. The application doesn’t have a user interface, but a test case demonstrates it well. You’ll find the application and test sources in the package in.co.narayanan.commons.vfs. In order to run the sample application, download the source archive and run the Ant build script to create the Commons VFS library. The Ant script is smart enough to download other dependant library files. You can start the sample application by running the JUnit test case class in.co.narayanan.commons.vfs.TestSearchBuddy.

The basic idea of using the Commons VFS is to create providers for each supported file type and add to the DefautFileSystemManager instance (referred to as manager hereafter). A reference to the FileObject instance needs to fetched, using resolveFile methods in the manager for further processing. The manager and the FileObject have various other useful methods that are worth exploring in the javadocs. The following paragraphs describe how you can use the Commons VFS API in a search tool.

Listing 6 presents a code snippet from the in.co.narayanan.commons.vfs.SearchBuddy class used to initialize the DefaultFileSystemManager class.

 Listing 6. Initializing the File System Manager
/** * Initialize the DefaultFileSystemManager to support * file, zip and jar providers. A virtual file system * is created and passed to the SearchableVirtualFileSystem * decorator class. * * @throws SearchException Error in initializing the file * FileSystemManager */private void init() throws SearchException { defFileSysMgr = new DefaultFileSystemManager(); try { defFileSysMgr.addProvider("file", new DefaultLocalFileProvider()); defFileSysMgr.addProvider("zip", new ZipFileProvider()); defFileSysMgr.addProvider("jar", new JarFileProvider()); defFileSysMgr.init(); // Create the virtual file system VirtualFileSystem vfs = (VirtualFileSystem)defFileSysMgr.createVirtualFileSystem("vfs://").getFileSystem(); searchableVfs = new SearchableVirtualFileSystem(vfs); } catch (FileSystemException e) { throw new SearchException("Unable to initialize the FileSystemManager.", e); }}

The highlighted lines add providers for searching in the local file system, zip files, and jar files. The code creates an instance of a VirtualFileSystem, which it will use for mounting other file systems. (You will see more details on this and the SearchableVirtualFileSystem class shortly.)

Listing 7 is a code snippet from the test case class TestSearchBuddy. It illustrates how the sample application can be used to search files.

 Listing 7. Usage of the Search Tool
/** * Adds the folder, zip, and a jar file to search * * @throws Exception Error in the test. */public void testSearchInZips() throws Exception { SearchBuddy searchTool = new SearchBuddy(); searchTool.addSearchableZip("testroot.zip"); searchTool.addSearchableJar("testjar.jar"); searchTool.addSearchableFolder("."); System.out.println("Searching for news.txt"); searchTool.search("news", "txt"); System.out.println("Searching for Range.class"); searchTool.search("range", "class"); System.out.println("Searching for test.xml"); searchTool.search("test", "xml"); System.out.println("Searching for *.properties"); searchTool.search(null, "properties"); searchTool.close();}

The highlighted lines add the zip and jar files to search. The search method is later called with the file name and extension for which to search.

Listing 8 presents a code snippet from the SearchBuddy class that demonstrates mounting various file types to the virtual file system.

 Listing 8. Code for Mounting a Zip File to the Virtual File System
/** * Mount a zip file to the searchable virtual * file system. * * @param pathToFolder Absolute or relative path to the zip file. * @throws SearchException Error while adding the zip file to the virtual file system. */public void addSearchableZip(String pathToZip) throws SearchException { File zipFile = new File(pathToZip); if(!zipFile.exists()) { throw new SearchException("Invalid zip file path"); } try { FileObject zipFileObject = defFileSysMgr.toFileObject(zipFile); searchableVfs.addJunction("/" + zipFile, defFileSysMgr.resolveFile("zip:" + zipFileObject + "!/")); } catch (FileSystemException e) { throw new SearchException("Unable to add zip file to the virtual file system", e); }}

The zip file is mounted by calling the addJunction method in the VirtualFileSystem class. The junctions (or the mount points) are cached in the decorator class SearchableVirtualFileSystem, which will be used later during the search operation.

Listing 9 presents the code used for searching the mounted file systems.

 Listing 9. Search Operation
Class SearchBuddy/** * Delegate the search call to the Searchable virtual file * system decorator. * * @param fileNamePart Name of the file. * @param extension Extension to search * @throws SearchException Error from the VirtualFileSystem when searching for file. */public void search(String fileNamePart, String extension) throws SearchException { searchableVfs.search(fileNamePart, extension);} Class SearchableVirtualFileSystem/** * Iterate the junctions to search for the given file name. * * @param fileNamePart File name to search * @param extension Extension to search * @throws SearchException */public void search(String fileNamePart, String extension) throws SearchException { try { Iterator searchPoints = junctions.iterator(); FileObject matchingFiles[]; while(searchPoints.hasNext()) { String searchPoint = searchPoints.next(); FileObject searchRoot = vfs.resolveFile(searchPoint); filter.setExtension(extension); filter.setFileNamePart(fileNamePart); matchingFiles = searchRoot.findFiles(filter); for(FileObject file : matchingFiles) { System.out.println("Result:" + file); } } } catch (FileSystemException e) { throw new SearchException("Search failed", e); }}

The search method iterates the mounted file systems, resolves the root, and calls the findFiles method in the FileObject class by passing a search filter. The result gets printed to the console. The primary responsibility of the decorator class SearchableVirtualFileSystem is to remember the mounted file systems. Other method calls are delegated to the wrapped VirtualFileSystem instance.

The source files in the sample application contain a good amount of documentation to explain the code.

Using the Common VFS framework in desktop and server applications totally isolates the consumer program from file type-specific code, thereby improving the application’s modularity and making development easier.

Configuration

Commons Configuration addresses the need for enterprise software to access property files. Using this function lets an application uniformly see the configurations loaded from various sources.

The following are some of the more useful features in this API:

  • Transparent access to configuration properties stored in JNDI, databases, text files, XML files, memory, the System properties, Applets, and Servlet initialization properties
  • Ability to automatically reload the properties based on a reloading strategy that you can customize by writing strategy classes
  • Ability to persist the modified properties back to the storage
  • Ability to access the XML configurations using XPath-like syntax

If the module context in a software application contains a reference to the org.apache.commons.configuration.Configuration instance, the properties that the application needs can easily be made available to all the classes in that module.

The Jakarta site gives a good introduction to this API with a primitive example. Saving the properties back to the storage is supported only for the configuration sources of type files.

The sample application for this API synchs the system time to a remote SNTP (Simple Network Time Protocol) server. It doesn’t implement the method that does the actual sync to the time server, but it is a skeleton that works well for demonstrating Commons Configuration. Find the complete source in the package in.co.narayanan.commons.config.

Listing 10 presents the configuration requirements for the application, including the following:

  • syncintervalhours ? Integer property to represent the interval between syncs
  • enablesync ? Boolean property to turn the synching operation on or off
  • ? String array property that represents the name of the SNTP servers to which the tool can connect
  • lastsync ? Time stamp string of the last successful sync (This is written at the completion of every sync operation.)

 Listing 10. Configuration Files Used for the Application
application.propertiessyncintervalhours=12enablesync=true#This property is set after the first runlastsync= sntpservers.xml server1 server2 server3

Listing 11 is an interface that provides access to the application’s configuration files using Commons Configuration implementation.

 Listing 11. Interface for Accessing the Application Configuration Needs
public interface IConfiguration { void setStringConfig(String nameSpace, String key, String value); String getStringConfig(String nameSpace, String key); String[] getStringArrayConfig(String nameSpace, String key); void setBooleanConfig(String nameSpace, String key, boolean value); boolean getBooleanConfig(String nameSpace, String key); void setIntConfig(String nameSpace, String key, int value); int getIntConfig(String nameSpace, String key);}

The interface isolates the application classes that are accessing all the features in the Commons Configuration API. It provides methods to read and store the properties of type string, integer, Boolean, and string array.

Listing 12 is a code snippet from the ApplicationConfiguration class that implements the interface.

 Listing 12. Loading the Properties Using Commons Configuration API
/** * Get the config property value from configuration storage. The * properties are loaded if it is not already loaded for the * given namespace. * * @param nameSpace Name of the configuration group * @param key Unique key within the namespace * @return Sring value of the property. Null if the property is not found * or if the configuration cannot be loaded */public String getStringConfig(String nameSpace, String key) { Configuration config = getConfiguration(nameSpace); if(config != null) { return config.getString(key); } return null;} private synchronized Configuration getConfiguration(String nameSpace) { Configuration config = configs.get(nameSpace); if(config == null) { try { if("application".equals(nameSpace)) { config = new PropertiesConfiguration("application.properties"); } else if("sntpservers".equals(nameSpace)) { config = new XMLConfiguration(getClass().getResource("sntpservers.xml")); } configs.put(nameSpace, config); } catch (ConfigurationException e) { System.out.println("Unable to load the configuration:" + e.getMessage()); } } return config;}

The highlighted lines indicate that the code lazy loads the properties from the text and XML files when a getter method is called. The Configuration objects are stored in an internal map per nameSpace, which lets the configurations be loaded and stored independently without mixing the properties. If the properties can be mixed and are from different sources, the class org.apache.commons.configuration.CompositeConfiguration can be used. (Find more details here.)

Listing 13 shows how the configuration can be saved back to storage.

 Listing 13. Saving the Configuration Back to the Text File
private synchronized void save(Configuration config) { if(config instanceof FileConfiguration) { try { // If reloading strategy is set, then the properties // doesn't get saved ((FileConfiguration)config).save(); } catch (ConfigurationException e) { System.out.println("Config not saved. Error while saving." + e.getMessage()); } } else { System.out.println("Config not saved. Not supported."); }}

Note: I encountered a problem in saving the properties to the text file when the reload strategy was set to the Configuration instance. (Find more about property file usage here.)

Listing 14 shows how the configuration properties are accessed in an application class via the context.

 Listing 14. Accessing the Configuration Using the Application Context
void syncTime() { if(shouldSyncNow()) { context.getLogger().log(Level.INFO, "Sync started"); sync(); recordSyncTime(); } else { context.getLogger().log(Level.INFO, "Sync skipped. Time not yet arrived"); }} private boolean shouldSyncNow() { boolean enableSync = context.getConfiguration() .getBooleanConfig("application", "enablesync"); context.getLogger().log(Level.INFO, "enablesync:" + enableSync); if(enableSync) { if(getCurrentTime() > readLastSync() + getInterval()) { return true; } } return false;} /** * Use SNTP API to sync the time. */private void sync() { String servers[] = context.getConfiguration() .getStringArrayConfig("sntpservers", "name"); for(String server : servers) { context.getLogger().log(Level.INFO, "Time Server:" + server); } // Use the list of server to sync the system time. // sntp API for java can be used here context.getLogger().log(Level.INFO, "Syncing time..");}

The code obtains a reference to the logger and configuration class to fetch the necessary property. Consider looking at the in.co.narayanan.commons.config.SyncTime class for all the properties’ uses.

Listing 15 is the test case class for testing the SyncTime class.

 Listing 15. Test Case Class for Verifying the Behavior of the SyncTime Class
public class TestSyncTime extends TestCase {public void testSyncTime() { Logger consoleLogger = Logger.getAnonymousLogger(); ApplicationContext context = new ApplicationContext(); context.setLogger(consoleLogger); context.setConfiguration(new ApplicationConfiguration()); SyncTime sync = new SyncTime(context); sync.syncTime(); sync.syncTime();}}

The test case class initializes a Java logger and the configuration class, sets them to the application context, and injects the dependency to the SyncTime class.

Commons Configuration enables you to access external properties from various sources. Although useful, it has some room for improvement since enterprise software requires saving properties back to a directory server using JNDI and a database. However, for reading from simple text and XML files, this framework is a good option.

Pool

Commons Pool is a cool framework that lets you pool any object. Ideally, you will find it useful for pooling JDBC connections, threads, business objects, and JNDI connections.

The sample application utilizes a JNDI connection pool. You need a JNDI-compatible directory server to run the application. This example uses Active Directory Application Mode (ADAM), which you can download here. The following paragraphs explain how the sample application uses Commons Pool to pool JNDI connections.

Listing 16 is the interface that exposes methods for performing a JNDI operation.

 Listing 16. Interface That Exposes Methods to Perform a JNDI Operation
public interface IJNDIConnection { /** * Execute the given query against the directory server. * * @param query JNDI query of the form ((objectclass=user)(cn=john)) * @return Search result * @throws NamingException Directory server error */NamingEnumeration executeQuery(String query) throws NamingException; /** * Fetch the attributed of a given DN of an object. * * @param dn Distinguished name of the object * @return Attributes of an object * @throws NamingException Directory server error */Attributes getAttributes(String dn) throws NamingException;}

The class in.co.narayanan.commons.pool.JNDIConnection implements the interface and uses the javax.naming.directory.DirContext class to perform a requested JNDI operation. Listing 17 presents a code snippet taken from the TestPool test case class to demonstrate how the sample application fetches and uses a pooled connection.

 Listing 17. Connection Pool Usage
String host = "localhost"; String port = "389"; String binDN = "CN=guest,CN=Users,DC=narayanan,DC=co,DC=in"; String password = "guest"; String baseDN = "DC=narayanan,DC=co,DC=in"; JNDIConnectionManager manager = JNDIConnectionManager.getJNDIConnectionManager(host, port, binDN, password, baseDN); IJNDIConnection connection = manager.getConnection(); String query = "(objectclass=container)"; NamingEnumeration users = connection.executeQuery(query); while (users.hasMoreElements()) { System.out.println(users.next()); }

The code obtains a reference to IJNDIConnection from the connection pool using the JNDIConnectionManager, and then executes a simple search query. The basic steps for utilizing the Commons Pool are to implement the interface org.apache.commons.pool.PoolableObjectFactory methods and pass the reference to an implementation of the org.apache.commons.pool.ObjectPool interface. In most cases, the org.apache.commons.pool.impl.GenericObjectPool class?the default implementation of ObjectPool?will suffice. Listing 18 is a code snippet from JNDIConnectionFactory that creates JNDI connections when the Commons Pool requests a new object.

 Listing 18. JNDIConnectionFactory Implementation
public class JNDIConnectionFactory implements PoolableObjectFactory {private JNDIConnectionInfo connInfo;public JNDIConnectionFactory(String host, String port, String bindDN, String password, String baseDN) { connInfo = new JNDIConnectionInfo(host, port, bindDN, password, baseDN);} public Object makeObject() throws Exception { Hashtable dirProps = connInfo.getJNDIConnectionProps(); DirContext dirContext = new InitialDirContext(dirProps); return dirContext;} public void destroyObject(Object context) throws Exception { DirContext dirContext = (DirContext)context; dirContext.close();}// remaining methods// inner class JNDIConnectionInfo}

The highlighted lines show how a new DirContext is created and destroyed on a request from the Commons Pool. The ObjectPool-implemented class will call the makeObject and destroyObject appropriately. Listing 19 presents the code snippet from the class JNDIConnectionManager.

 Listing 19. Manager Class, Which Fetches a Connection from the Pool
public class JNDIConnectionManager { // remaining declarations private GenericObjectPool pool; private PoolableObjectFactory factory; private JNDIConnectionManager(String host, String port, String bindDN, String password, String baseDN) { factory = new JNDIConnectionFactory(host, port, bindDN, password, baseDN); pool = new GenericObjectPool(factory);} // remaining code public IJNDIConnection getConnection() { try { return new JNDIConnection((DirContext)pool.borrowObject()); } catch (Exception e) { return null; }} public void returnConnection(IJNDIConnection connection) { try { pool.returnObject(((JNDIConnection)connection).getContext()); } catch (Exception e) { System.out.println("Unable return the object back to pool"); }} }

The code passes the JNDIConnectionFactory reference to the GenericObjectPool to let the Commons pool create JNDI connections. The highlighted lines show how the connection is fetched and returned to the pool.

Commons Pool can definitely be useful for all Java applications since resource pooling is a common requirement. You can further customize the GenericObjectPool class to suite your needs. (Find more details in the javadoc of the class.)

So What Have You Learned?

Now that you’ve completed this final installment of the Jakarta Commons series, you know the following:

  • The rich features provided by various Commons components
  • Where in your Java projects you can use a particular component or method from a utility class
  • The packages and classes contained in various APIs

Now, when you design or develop a Java application, you’ll be able to pick a useful class and use it appropriately.

In Case You Missed It

  • Extend the JDK Classes with Jakarta Commons, Part I
  • Extend the JDK Classes with Jakarta Commons, Part II
  • devxblackblue

    About Our Editorial Process

    At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

    See our full editorial policy.

    About Our Journalist