From cb7e47f11f43726648f01e1668e8a40bb9725cac Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Thu, 14 Sep 2023 23:31:06 +0700 Subject: [PATCH 01/36] Reorganize the properties files * Deleted all properties files * Initial new property file called `setup.properties` * Now all setup properties will be in `setup.properties` file and no longer used the XML file --- src/main/resources/configuration/config.xml | 9 ----- .../resources/configuration/setup.properties | 14 ++++++++ .../resources/contents/additional.content | 3 -- src/main/resources/contents/help.content | 33 ------------------- 4 files changed, 14 insertions(+), 45 deletions(-) delete mode 100644 src/main/resources/configuration/config.xml create mode 100644 src/main/resources/configuration/setup.properties delete mode 100644 src/main/resources/contents/additional.content delete mode 100644 src/main/resources/contents/help.content diff --git a/src/main/resources/configuration/config.xml b/src/main/resources/configuration/config.xml deleted file mode 100644 index f39d035c..00000000 --- a/src/main/resources/configuration/config.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - ${package.name} - v${package.version.core} - ${package.betaNum} - ${project.groupId}.${project.artifactId} - ${author.name} - diff --git a/src/main/resources/configuration/setup.properties b/src/main/resources/configuration/setup.properties new file mode 100644 index 00000000..8ac526e8 --- /dev/null +++ b/src/main/resources/configuration/setup.properties @@ -0,0 +1,14 @@ +# JMatrix :: Setup Properties +# +# Copyright (c) 2023 Ryuu Mitsuki. +# JMatrix is licensed under the Apache License 2.0 and later. +# + +JM-Name = ${project.name} +JM-Version = ${project.version} +JM-Url = ${project.url} +JM-GroupId = ${project.groupId} +JM-ArtifactId = ${project.artifactId} +JM-Author = ${project.developers[0].name} +JM-License = ${project.licenses[0].name} +JM-BuildTime = ${maven.build.timestamp} diff --git a/src/main/resources/contents/additional.content b/src/main/resources/contents/additional.content deleted file mode 100644 index f08cdc3d..00000000 --- a/src/main/resources/contents/additional.content +++ /dev/null @@ -1,3 +0,0 @@ -# Don't change this file directly -4A4D6174726978202D20436F707972696768742028432920323032332052797575204D697473756B69 -417061636865204C6963656E736520322E30 diff --git a/src/main/resources/contents/help.content b/src/main/resources/contents/help.content deleted file mode 100644 index 59c0666b..00000000 --- a/src/main/resources/contents/help.content +++ /dev/null @@ -1,33 +0,0 @@ -USAGE: - java -jar [options] - -OPTIONS: - -h, help display this help - -cr, copyright print the copyright - -V, ver, version display version number with release type - -DESCRIPTION: - JMatrix is a Java library designed to simplify matrix operations. - It provides a set of intuitive methods to perform common matrix - operations with ease. - - With JMatrix, you can create matrices of various dimensions, - initialize them with values, and perform some basic matrix operations. - It supports both square and rectangular matrix. - - JMatrix provides some basic matrix operations, such as: - - - Addition - - Subtraction - - Multiplication - - Transposition - -ISSUES: - Report any issues or suggestions, and help improve JMatrix: - https://github.com/mitsuki31/jmatrix/issues/new - -AUTHOR: - Ryuu Mitsuki - -LICENSE: - Apache-2.0 License From 8a6226829da1ecf6b6ec54aa6962a64ea448a7bf Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Sat, 16 Sep 2023 11:23:02 +0700 Subject: [PATCH 02/36] Deletion, rename and reorganize the internal packages * Deleted the `OSUtils.java` (this class are no longer used) * Moved all internal classes to directory (package) called `internal` * Renamed the `Options` class to `JMatrixUtils`, improving clarity --- .../JMatrixUtils.java} | 129 +----------------- .../jmatrix/{util => internal}/XMLParser.java | 2 +- .../com/mitsuki/jmatrix/util/OSUtils.java | 121 ---------------- 3 files changed, 8 insertions(+), 244 deletions(-) rename src/main/java/com/mitsuki/jmatrix/{util/Options.java => internal/JMatrixUtils.java} (82%) rename src/main/java/com/mitsuki/jmatrix/{util => internal}/XMLParser.java (99%) delete mode 100644 src/main/java/com/mitsuki/jmatrix/util/OSUtils.java diff --git a/src/main/java/com/mitsuki/jmatrix/util/Options.java b/src/main/java/com/mitsuki/jmatrix/internal/JMatrixUtils.java similarity index 82% rename from src/main/java/com/mitsuki/jmatrix/util/Options.java rename to src/main/java/com/mitsuki/jmatrix/internal/JMatrixUtils.java index e56ee775..fc09fea0 100644 --- a/src/main/java/com/mitsuki/jmatrix/util/Options.java +++ b/src/main/java/com/mitsuki/jmatrix/internal/JMatrixUtils.java @@ -1,6 +1,6 @@ -// ------------------- // -/* Options */ -// ------------------- // +// ---------------------- // +/* JMatrixUtils */ +// ---------------------- // /* Copyright (c) 2023 Ryuu Mitsuki * @@ -17,7 +17,7 @@ * limitations under the License. */ -package com.mitsuki.jmatrix.util; +package com.mitsuki.jmatrix.internal; import com.mitsuki.jmatrix.exception.JMatrixBaseException; @@ -31,133 +31,18 @@ import java.util.Arrays; /** - * This class provides all requirements for JMatrix library. + * This class provides all neccessary utilities for JMatrix library. * * @author * Ryuu Mitsuki - * @version 1.33, 17 August 2023 + * @version 1.5, 16 September 2023 * @since 1.0.0b.1 * @license * Apache License 2.0 * * @see com.mitsuki.jmatrix.util.XMLParser */ -public class Options -{ - - /** - * An {@code Enum} that contains all available options. - * - * @since 1.0.0b.1 - * @see #getOptions(String) - */ - public enum OPT { - - /** - * Represents the "version" option. Users can retrieve this option by using one of the following inputs: - * - * - * - * @see #OPT(String...) - */ - VERSION("-V", "version", "ver"), - - /** - * Represents the "help" option. Users can retrieve this option by using the following input: - * - * - * - * @see #OPT(String...) - */ - HELP("-h", "help"), - - /** - * Represents the "copyright" option. Users can retrieve this option by using the following input: - * - * - * - * @see #OPT(String...) - */ - COPYRIGHT("-cr", "copyright"); - - /** - * A {@link List} of string to stores all options aliases. - */ - private final List aliases; - - /** - * Constructs an option with the given aliases. - * - * @param aliases the aliases that represent this option. - * - * @since 1.0.0b.1 - * @see java.util.Arrays#asList - */ - OPT(String ... aliases) { - this.aliases = Arrays.asList(aliases); - } - } - - /** - * Stores the static object of {@link XMLParser} class. - */ - private static XMLParser XML = new XMLParser(XMLParser.XMLType.CONFIG); - - /** - * Stores a string that represents the program name. - */ - private static String PROGNAME = XML.getProperty("programName").toLowerCase(); - - /** - * Stores a string that represents the path to "contents" directory. - */ - private static String contentsPath = "contents/"; - - /** - * Method that checks the input argument then returns specific option. - * - * @param inputOpt the {@code String} that wants to be checked. - * - * @return the corresponding {@code OPT} value. - * - * @throws IllegalArgumentException if the given option name does not - * match with any known options. - * This exception will be thrown as the causing exception. - * - * @since 1.0.0b.1 - */ - public static OPT getOptions(String inputOpt) { - for (OPT opt : OPT.values()) { - if (opt.aliases.contains(inputOpt)) { - return opt; - } - } - - raiseError(new JMatrixBaseException( - new IllegalArgumentException( - String.format("Unknown argument option for input \"%s\"", inputOpt) - ) - ), 0); - - System.err.println(System.lineSeparator() + getHelpMsg()[0]); - System.err.println(" " + - "java -jar [-h|-V|-cr]"); - System.exit(-1); - - // This never get executed, but to suppress missing return statement error - return null; - } - +public class JMatrixUtils { ///// ---------------------- ///// /// Class & Packages /// diff --git a/src/main/java/com/mitsuki/jmatrix/util/XMLParser.java b/src/main/java/com/mitsuki/jmatrix/internal/XMLParser.java similarity index 99% rename from src/main/java/com/mitsuki/jmatrix/util/XMLParser.java rename to src/main/java/com/mitsuki/jmatrix/internal/XMLParser.java index 8f48877d..41f03925 100644 --- a/src/main/java/com/mitsuki/jmatrix/util/XMLParser.java +++ b/src/main/java/com/mitsuki/jmatrix/internal/XMLParser.java @@ -17,7 +17,7 @@ * limitations under the License. */ -package com.mitsuki.jmatrix.util; +package com.mitsuki.jmatrix.internal; import com.mitsuki.jmatrix.exception.JMatrixBaseException; diff --git a/src/main/java/com/mitsuki/jmatrix/util/OSUtils.java b/src/main/java/com/mitsuki/jmatrix/util/OSUtils.java deleted file mode 100644 index cfad6ab6..00000000 --- a/src/main/java/com/mitsuki/jmatrix/util/OSUtils.java +++ /dev/null @@ -1,121 +0,0 @@ -// ------------------- // -/* OSUtils */ -// ------------------- // - -/* Copyright (c) 2023 Ryuu Mitsuki - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.mitsuki.jmatrix.util; - -import java.io.File; - -/** - * This class provides operating system utilities. - * - * @author - * Ryuu Mitsuki - * @version 1.1, 26 June 2023 - * @since 1.0.0b.1 - * @license - * Apache License 2.0 - * @see com.mitsuki.jmatrix.util.Options - * @see com.mitsuki.jmatrix.util.XMLParser - */ -public class OSUtils -{ - - /** - * List names of operating system. - * - *

Use {@link #getOSName()} to retrieve the name of current operating system. - * - * @since 1.0.0b.1 - * @see #getOSName() - */ - public enum OS { - /** - * Indicates the Windows OS. - */ - WINDOWS, - - /** - * Indicates the Linux OS. - */ - LINUX, - - /** - * Indicates the Mac OS. - */ - MAC, - - /** - * Indicates the Solaris OS. - */ - SOLARIS, - - /** - * Indicates unknown OS. - */ - OTHER - }; - - /** - * Stores the static variable of {@link OSUtils.OS}. - */ - private static OS os = null; - - /** - * Returns path separator based on operating system. - * - * @since 1.0.0b.1 - * @see File#separator - */ - public final static String sep = File.separator; - - /** - * Retrieves and returns the name of current operating system. - * - *

If the operating system name unknown, then it will returns {@code OTHER}. - * - * @return the name of current operating system - * - * @since 1.0.0b.1 - * @see System#getProperty(String) - */ - public static OS getOSName() { - if (os == null) { - final String osName = System.getProperty("os.name").toLowerCase(); - - if (osName.contains("win")) { - os = OS.WINDOWS; - } - else if (osName.contains("nix") || osName.contains("nux") || - osName.contains("aix")) { - os = OS.LINUX; - } - else if (osName.contains("mac")) { - os = OS.MAC; - } - else if (osName.contains("sunos")) { - os = OS.SOLARIS; - } - else { - os = OS.OTHER; // unknown operating system - } - } - - return os; - } -} From bcf1f172924d821eb3b34174d43f748089cffce8 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Sat, 16 Sep 2023 20:09:41 +0700 Subject: [PATCH 03/36] Refactor the main class * Changed the main class to `MainClass.java` * Introduced a new final class called `ArgumentsParser` in main class * Added several new methods to `ArgumentsParser` class, including: - `contains` - `get` - `getArguments` - `iterator` (from `Iterable` interface) --- pom.xml | 2 +- src/main/java/com/mitsuki/jmatrix/Main.java | 141 ---------------- .../mitsuki/jmatrix/internal/MainClass.java | 155 ++++++++++++++++++ 3 files changed, 156 insertions(+), 142 deletions(-) delete mode 100644 src/main/java/com/mitsuki/jmatrix/Main.java create mode 100644 src/main/java/com/mitsuki/jmatrix/internal/MainClass.java diff --git a/pom.xml b/pom.xml index c30ab35f..3906e555 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ Ryuu Mitsuki JMatrix 1.2.2 - com.mitsuki.jmatrix.Main + com.mitsuki.jmatrix.internal.MainClass stable 0 LICENSE diff --git a/src/main/java/com/mitsuki/jmatrix/Main.java b/src/main/java/com/mitsuki/jmatrix/Main.java deleted file mode 100644 index 8717fd25..00000000 --- a/src/main/java/com/mitsuki/jmatrix/Main.java +++ /dev/null @@ -1,141 +0,0 @@ -/* Copyright (c) 2023 Ryuu Mitsuki - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.mitsuki.jmatrix; - -import com.mitsuki.jmatrix.util.Options; -import com.mitsuki.jmatrix.util.XMLParser; - -/** - * Main class for JMatrix library that provides some information such as JMatrix version. - * - *

This class does not provides APIs for build the matrix, - * it's just useless class if get imported. - * - *

Usage:

- * - *
 
- *   java -jar path/to/jmatrix-<version>.jar [-h|-V|-cr]
- * 
- * - * @author - * Ryuu Mitsuki - * @version 1.32, 19 July 2023 - * @since 1.0.0b.1 - * @see com.mitsuki.jmatrix.Matrix - */ -public class Main -{ - /** - * Stores the static object class of {@link XMLParser} class. - */ - private static XMLParser XML = new XMLParser(XMLParser.XMLType.CONFIG); - - /** - * Stores a string that represents the concatenation of: - * - *
    - *
  • The program name - *
  • (a space) - *
  • The version number - *
- */ - private static String programVersion = XML.getProperty("programName") + " " + getVersion(); - - /** - * Main program for JMatrix library. - * - * @param args a {@code String} array containing arguments from command line. - * - * @since 1.0.0b.1 - */ - public static void main(String[ ] args) { - String arg1 = null; - - if (args.length >= 1 && args.length < 2) { - arg1 = args[0]; - } else if (args.length > 1) { - Options.raiseErrorMsg( - new IllegalArgumentException("Too much arguments"), - 0 // no exit - ); - - System.out.println(Options.getHelpMsg()[0]); - System.out.println(" " + - "java -jar [-h|-V|-cr]"); - System.exit(1); - } - - if (arg1 == null) { - String helps[ ] = Options.getHelpMsg(); - Options.raiseErrorMsg( - new NullPointerException("Null argument"), - 0 // no exit - ); - - for (int i = 0; i < helps.length; i++) { - if (i >= 7) break; - System.out.println(helps[i]); - } - System.exit(1); - } - - switch (Options.getOptions(arg1)) { - case VERSION: - String pkgName = String.format(" <%s>", XML.getProperty("packageName")); - - System.out.println(programVersion + pkgName); - break; - - case HELP: - String lines = System.lineSeparator(); - for (byte i = 0; i < (byte) programVersion.length(); i++) lines += "-"; - - System.out.println(programVersion + lines + System.lineSeparator()); - for (String s : Options.getHelpMsg()) System.out.println(s); - break; - - case COPYRIGHT: - for (String s : Options.getCopyright()) System.out.println(s); - break; - } - } - - - /** - * Gets the version number and concanate it with the release type. - * And concanate again with the beta number if and only if the release type is "beta". - * - * @return a string represents the version number. - * - * @since 1.0.0 - */ - private static String getVersion() { - String ver = XML.getProperty("version"); - - // Only retrieve the release type if not release and stable release - if ( !(XML.getProperty("releaseType").equals("release") || - XML.getProperty("releaseType").equals("stable")) ) { - ver += "-" + XML.getProperty("releaseType"); - - // This will only retrieve a non-zero beta number - if (!XML.getProperty("betaNum").equals("0")) { - ver += "." + XML.getProperty("betaNum"); - } - } - - return ver; - } -} diff --git a/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java b/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java new file mode 100644 index 00000000..d4489a02 --- /dev/null +++ b/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java @@ -0,0 +1,155 @@ +// ------------------- // +/* MainClass */ +// ------------------- // + +/* Copyright (c) 2023 Ryuu Mitsuki + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mitsuki.jmatrix.internal; + +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.LinkedHashSet; +import java.util.Set; +import java.lang.Iterable; + + +/** + * Main class for JMatrix library that provides some information + * such as JMatrix version. If this class is imported, it will be + * not valuable because it does not provides APIs to construct the matrices. + * + * As of JMatrix 1.5, the class name has changed from + * {@code Main} to {@code MainClass}. + * + * @author Ryuu Mitsuki + * @version 1.4, 16 September 2023 + * @since 1.0.0b.1 + * @see com.mitsuki.jmatrix.Matrix + */ +public class MainClass { + + /** + * Main method for JMatrix library. + * + * @param args a {@code String} array containing arguments from command line. + * + * @since 1.0.0b.1 + */ + public static void main(String[] args) { + ArgumentsParser argsParser = new ArgumentsParser<>(args); + List argsList = argsParser.getArguments(); + + argsList.forEach((arg) -> { + System.out.print(arg + argsList.indexOf(arg) ", "); + }); + System.out.print(System.lineSeparator()); + } +} + + +/** + * An utility class for parsing and managing a set of unique arguments. + * It is designed only for {@link MainClass} class to parse the + * command line arguments. + * + * @param The type of object stored in this class. + * + * @author Ryuu Mitsuki + * @version 1.0, 16 September 2023 + * @since 1.5.0 + */ +final class ArgumentsParser implements Iterable { + private List arguments; + + /** + * Sole constructor. Constructs this class with the specified array of arguments. + * Duplicate elements are removed, and the order is preserved. + * + * @param args The array of arguments to parse. + * + * @since 1.5.0 + */ + ArgumentsParser(E[] args) { + // Using LinkedHashSet to remove duplicate elements and + // preserve the order of elements + Set argsSet = new LinkedHashSet(Arrays.asList(args)); + + // Convert to List after removing all duplicate elements + this.arguments = new ArrayList(argsSet); + } + + + /** + * Checks whether this parser contains the specified element. + * + * @param o element whose presence in this list is to be tested. + * + * @return {@code true} if the element is present, {@code false} otherwise. + * + * @since 1.5.0 + */ + boolean contains(Object o) { + return this.arguments.contains(o); + } + + /** + * Returns the element at the specified position in this list of arguments. + * + * @param index The index of the element to retrieve + * (index > 0 && index < size()). + * + * @return The element at the specified index in this list. + * + * @since 1.5.0 + * + * @throws IndexOutOfBoundsException + * if the index is out of range (index < 0 || index > size()). + */ + E get(int index) { + return this.arguments.get(index); + } + + /** + * Returns an unmodifiable view of the arguments contained in this class. + * Modifications to the returned list result in an + * {@code UnsupportedOperationException}. + * + * @return An unmodifiable view of the arguments. + * + * @since 1.5.0 + */ + List getArguments() { + // Return the properties with read-only attribute, + // this means users cannot modify the elements. + return Collections.unmodifiableList(this.arguments); + } + + /** + * Returns an iterator over the elements in this list of arguments + * in proper sequence. + * + * @return An iterator over the elements. + * + * @since 1.5.0 + */ + @Override + public Iterator iterator() { + return this.arguments.iterator(); + } +} From 49266cae0485a151c6d09404016332678b283246 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Sat, 16 Sep 2023 21:01:01 +0700 Subject: [PATCH 04/36] Introduce `PropertiesParser` a new parser class This class provides methods to parse the properties file with ease. Please note, do not use constructor to create an instance of this class, use the `load(String)` (static) method instead and use the `getProperties` method (after created the instance class) to get the instance of Properties (including all properties data) from this instance. For example: PropertiesParser propsParser = PropertiesParser.load("config.properties"); Properties configProps = propsParser.getProperties(); --- .../jmatrix/internal/PropertiesParser.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java diff --git a/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java b/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java new file mode 100644 index 00000000..762e58af --- /dev/null +++ b/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java @@ -0,0 +1,54 @@ +// ---------------------- // +/* PropertiesParser */ +// ---------------------- // + +/* Copyright (c) 2023 Ryuu Mitsuki + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mitsuki.jmatrix.internal; + +import java.io.InputStream; +import java.io.IOException; +import java.util.Properties; + +public final class PropertiesParser { + private String propertyFile; + private Properties properties; + + private PropertiesParser(InputStream inStream, + String propertyPath) throws IOException { + this.propertyFile = propertyPath; + this.properties = new Properties(); + this.properties.load(inStream); + } + + public static PropertiesParser load(String file) throws IOException { + if (file == null) { + throw new NullPointerException("The file path cannot be null"); + } + + // Get the property using InputStream + InputStream propertyStream = JMatrixUtils.getFileAsStream(file); + if (propertyStream == null) { + throw new IOException(String.format("'%s' not found", file)); + } + + return new PropertiesParser(propertyStream, file); + } + + public Properties getProperties() { + return this.properties; + } +} From 51c8d88be3ce482b896ee303f2a7ddb52ed7a625 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Sat, 16 Sep 2023 21:14:53 +0700 Subject: [PATCH 05/36] Introduce `SetupProperties` subclass in `PropertiesParser` This class provides access to get the database from the setup properties file. This class is designed to be synchronized and thread-safe. Its access modifier is "package" and will only be visible to the package it is stored in. The setup properties are retrieved from the file statically using the `static` block, which means that when the program starts it will immediately retrieve the properties from the file. So this class only provides access to the property data. Example usage: Properties setupProps = SetupProperties.getSetupProperties(); --- .../jmatrix/internal/PropertiesParser.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java b/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java index 762e58af..139f8b86 100644 --- a/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java +++ b/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java @@ -19,8 +19,11 @@ package com.mitsuki.jmatrix.internal; +import java.io.File; import java.io.InputStream; import java.io.IOException; +import java.io.FileNotFoundException; +import java.nio.file.AccessDeniedException; import java.util.Properties; public final class PropertiesParser { @@ -52,3 +55,49 @@ public Properties getProperties() { return this.properties; } } + + +class SetupProperties { + private static final String setupFile = "configuration/setup.properties"; + private static Properties setupProperties; + + static { + File setupFile_FileObj = new File(setupFile); + + // Store the exception and will be thrown later if not null + Throwable causeException = null; + + if (!setupFile_FileObj.exists()) { + causeException = new FileNotFoundException( + String.format("Cannot found '%s' file", setupFile) + ); + } else if (setupFile_FileObj.exists() && + !setupFile_FileObj.canRead()) { + causeException = new AccessDeniedException(setupFile, + null, "Read access is denied" + ); + } + + // It is a good practice to throw occurred exception immediately, + // especially after exiting a code block (e.g., if-else statement block) + if (causeException != null) + throw new ExceptionInInitializerError(causeException); + + // Although the Properties class has been designed to be synchronized, + // the code below further ensures that it is implicitly synchronized + if (setupProperties == null) { + try (InputStream inStream = SetupProperties.class + .getClassLoader() + .getResourceAsStream(setupFile)) { + setupProperties = new Properties(); + setupProperties.load(inStream); + } catch (final IOException ioe) { + throw new ExceptionInInitializerError(ioe); + } + } + } + + static Properties getSetupProperties() { + return setupProperties; + } +} From 3415b740ec17c9e943e948726c3e2a77c2171ba0 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Sun, 17 Sep 2023 12:26:20 +0700 Subject: [PATCH 06/36] Javadocs addition and changes * Added javadocs for all classes in `PropertiesParser.java` * Added and updated some javadoc in `MainClass.java` * Removed test code in `main` method --- .../mitsuki/jmatrix/internal/MainClass.java | 17 ++- .../jmatrix/internal/PropertiesParser.java | 109 +++++++++++++++++- 2 files changed, 116 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java b/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java index d4489a02..1b22b811 100644 --- a/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java +++ b/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java @@ -41,6 +41,8 @@ * @version 1.4, 16 September 2023 * @since 1.0.0b.1 * @see com.mitsuki.jmatrix.Matrix + * @license + * Apache License 2.0 */ public class MainClass { @@ -52,13 +54,6 @@ public class MainClass { * @since 1.0.0b.1 */ public static void main(String[] args) { - ArgumentsParser argsParser = new ArgumentsParser<>(args); - List argsList = argsParser.getArguments(); - - argsList.forEach((arg) -> { - System.out.print(arg + argsList.indexOf(arg) ", "); - }); - System.out.print(System.lineSeparator()); } } @@ -75,6 +70,10 @@ public static void main(String[] args) { * @since 1.5.0 */ final class ArgumentsParser implements Iterable { + + /** + * Stores a list of arguments with duplicate elements removed. + */ private List arguments; /** @@ -112,14 +111,14 @@ boolean contains(Object o) { * Returns the element at the specified position in this list of arguments. * * @param index The index of the element to retrieve - * (index > 0 && index < size()). + * (index > 0 && index < size()). * * @return The element at the specified index in this list. * * @since 1.5.0 * * @throws IndexOutOfBoundsException - * if the index is out of range (index < 0 || index > size()). + * if the index is out of range (index < 0 || index > size()). */ E get(int index) { return this.arguments.get(index); diff --git a/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java b/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java index 139f8b86..3552601c 100644 --- a/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java +++ b/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java @@ -26,10 +26,58 @@ import java.nio.file.AccessDeniedException; import java.util.Properties; + +/** + * A class that provides utilities (methods) to parse specific properties file. + * Do not use the constructor to create the instance of this class, use + * the {@link PropertiesParser#load(String)} method instead. + * + *

For example: + * + *

 
+ *   PropertiesParser parser = PropertiesParser.load("example.properties");
+ *   Properties properties = parser.getProperties();
+ * 
+ * + *

Or with one variable: + * + *

 
+ *   Properties properties = PropertiesParser.load("example.properties")
+ *                                           .getProperties();
+ * 
+ * + * @author Ryuu Mitsuki + * @version 1.0, 16 September 2023 + * @since 1.5.0 + * @license + * Apache License 2.0 + */ public final class PropertiesParser { + + /** + * A path to the specified property file (initialized inside constructor). + */ private String propertyFile; + + /** + * Stores the properties data from the specified properties file. + */ private Properties properties; + /** + * A sole constructor. Users should never use this constructor to create a new + * instance of this class. + * + * @param inStream The (@link InputStream} object to be loaded by + * {@link Properties}. + * @param propertyPath The string path to the property file. + * + * @throws IOException + * If an I/O error occurs while loading the properties. + * + * @since 1.5.0 + * @see #load(String) + */ private PropertiesParser(InputStream inStream, String propertyPath) throws IOException { this.propertyFile = propertyPath; @@ -37,6 +85,22 @@ private PropertiesParser(InputStream inStream, this.properties.load(inStream); } + /** + * Loads and retrieves the properties from the specified file path. + * + * @param file The file path reference to a property file. + * + * @return A new instance of this class with parsed properties. + * To get the properties, users can use {@link #getProperties()}. + * + * @throws IOException + * If an I/O error occurs while loading the properties. + * + * @throws NullPointerException + * If the given file path is {@code null}. + * + * @since 1.5.0 + */ public static PropertiesParser load(String file) throws IOException { if (file == null) { throw new NullPointerException("The file path cannot be null"); @@ -51,16 +115,51 @@ public static PropertiesParser load(String file) throws IOException { return new PropertiesParser(propertyStream, file); } + /** + * Gets the instance of {@link Properties} class with parsed properties + * from this class. + * + * @return The instance of {@link Properties} with parsed properties. + * + * @since 1.5.0 + */ public Properties getProperties() { return this.properties; } } -class SetupProperties { +/** + * This class provides access to get the setup properties and it is designed + * to be synchronized and thread-safe. + * + *

The setup properties are retrieved from the file statically using + * the {@code static} block, which means that when the program starts it will + * immediately retrieve the properties from the file. + * + * @author Ryuu Mitsuki + * @version 1.0, 16 September 2023 + * @since 1.5.0 + * @license + * Apache License 2.0 + */ +final class SetupProperties { + + /** + * A string represents a file path to the setup properties stored in. + */ private static final String setupFile = "configuration/setup.properties"; + + /** + * A {@code Properties} object that stores all properties data + * from setup properties file. + */ private static Properties setupProperties; + /* Immediately search and check the setup properties file, then retrieve + * all properties data from it. Before retrieving the properties, + * it will checks whether the properties file is exist and readable. + */ static { File setupFile_FileObj = new File(setupFile); @@ -97,6 +196,14 @@ class SetupProperties { } } + /** + * Gets the synchronized setup properties from this class. + * + * @return A synchronized instance of {@link Properties} from this + * class containing all setup properties data. + * + * @since 1.5.0 + */ static Properties getSetupProperties() { return setupProperties; } From ae0eb2d6685b3900401ffe2500cf264ef5156c1e Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Sun, 17 Sep 2023 12:53:52 +0700 Subject: [PATCH 07/36] Temporarily deactivate the `tests` CI Decativated due to maintaining the internal code. This CI will also be improved to support with future changes. --- .github/workflows/tests.yml | 169 +++++++++++++++++------------------- 1 file changed, 80 insertions(+), 89 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 57f29ca2..2a71e80c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -61,29 +61,20 @@ jobs: # Install deps - name: Install dependencies if: ${{ steps.cache-maven.outputs.cache-hit != true && env.DEBUG != true }} - run: mvn install -DskipTests + run: mvn clean install -DskipTests - name: Install dependencies (Debug) if: ${{ steps.cache-maven.outputs.cache-hit != true && env.DEBUG == true }} - run: mvn install -DskipTests -X + run: mvn clean install -DskipTests -X - # Packaging with source files - - name: Package source + # Packaging and testing + - name: Packaging and testing if: ${{ env.DEBUG != true }} - run: mvn package -P include-src + run: mvn test package -P include-src - name: Package source (Debug) if: ${{ env.DEBUG == true }} - run: mvn package -P include-src -X - - # Test - - name: Test project - if: ${{ env.DEBUG != true }} - run: mvn test - - - name: Test project (Debug) - if: ${{ env.DEBUG == true }} - run: mvn test -X + run: mvn test package -P include-src -X # Clean up - name: Clean up the project @@ -91,77 +82,77 @@ jobs: # ::---:: Make Test ::---:: # - make-test: - name: Make Test - runs-on: ubuntu-latest - continue-on-error: true - - strategy: - matrix: - py-ver: ['3.7', '3.x'] - - env: - arch: x64 - DEPS_FILE: 'requirements.txt' - DEBUG: ${{ inputs.debug }} - - steps: - # Checkout - - name: Checkout repository - uses: actions/checkout@v3 - - # Setup Python - - name: Setup Python ${{ matrix.py-ver }} - id: setup-py - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.py-ver }} - architecture: ${{ env.arch }} - cache: 'pip' - cache-dependency-path: '**/${{ env.DEPS_FILE }}' - - # Install deps - - name: Install dependencies - if: ${{ steps.setup-py.outputs.cache-hit != true }} - run: | - if [ $DEBUG = 'true' ]; then - python -m pip install -r $DEPS_FILE --debug - else - python -m pip install -r $DEPS_FILE - fi - - # Sadly, Make cannot tests the project thoroughly due to unavailability - # of necessary packages (e.g "org.junit"), so here it just tests - # the project on compiling, packaging, and generating docs. - - # Compile - - name: Compile the project - run: | - [ -d target/classes ] && make clean - make compile VERBOSE=$DEBUG LINT=true - - # Package - - name: Packaging the project - run: | - make package VERBOSE=$DEBUG - - - name: Packaging the project (with source) - run: | - make package INCLUDE-SRC=true VERBOSE=$DEBUG - - # Build docs - - name: Build the docs - run: | - # Build docs - # For more information on debugging, we prefer to change it - # to "all" mode. - if [ $DEBUG = 'true' ]; then - make build-docs VERBOSE=all - else - make build-docs - fi - - # Clean up - - name: Clean up the project - run: | - [ -d target ] && echo "Clean the project" && make clean +# make-test: +# name: Make Test +# runs-on: ubuntu-latest +# continue-on-error: true +# +# strategy: +# matrix: +# py-ver: ['3.7', '3.x'] +# +# env: +# arch: x64 +# DEPS_FILE: 'requirements.txt' +# DEBUG: ${{ inputs.debug }} +# +# steps: +# # Checkout +# - name: Checkout repository +# uses: actions/checkout@v3 +# +# # Setup Python +# - name: Setup Python ${{ matrix.py-ver }} +# id: setup-py +# uses: actions/setup-python@v3 +# with: +# python-version: ${{ matrix.py-ver }} +# architecture: ${{ env.arch }} +# cache: 'pip' +# cache-dependency-path: '**/${{ env.DEPS_FILE }}' +# +# # Install deps +# - name: Install dependencies +# if: ${{ steps.setup-py.outputs.cache-hit != true }} +# run: | +# if [ $DEBUG = 'true' ]; then +# python -m pip install -r $DEPS_FILE --debug +# else +# python -m pip install -r $DEPS_FILE +# fi +# +# # Sadly, Make cannot tests the project thoroughly due to unavailability +# # of necessary packages (e.g "org.junit"), so here it just tests +# # the project on compiling, packaging, and generating docs. +# +# # Compile +# - name: Compile the project +# run: | +# [ -d target/classes ] && make clean +# make compile VERBOSE=$DEBUG LINT=true +# +# # Package +# - name: Packaging the project +# run: | +# make package VERBOSE=$DEBUG +# +# - name: Packaging the project (with source) +# run: | +# make package INCLUDE-SRC=true VERBOSE=$DEBUG +# +# # Build docs +# - name: Build the docs +# run: | +# # Build docs +# # For more information on debugging, we prefer to change it +# # to "all" mode. +# if [ $DEBUG = 'true' ]; then +# make build-docs VERBOSE=all +# else +# make build-docs +# fi +# +# # Clean up +# - name: Clean up the project +# run: | +# [ -d target ] && echo "Clean the project" && make clean From 7deda926dfa16e672394b07f19ef6fb96f4a2524 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Sun, 17 Sep 2023 12:58:47 +0700 Subject: [PATCH 08/36] Fix several errors during compilation The errors is occurred due to missing some variables, error imports, and etc. --- src/main/java/com/mitsuki/jmatrix/Matrix.java | 100 +++++++++--------- .../jmatrix/internal/JMatrixUtils.java | 77 ++------------ .../mitsuki/jmatrix/internal/XMLParser.java | 8 +- 3 files changed, 62 insertions(+), 123 deletions(-) diff --git a/src/main/java/com/mitsuki/jmatrix/Matrix.java b/src/main/java/com/mitsuki/jmatrix/Matrix.java index beda3eee..bb65feb8 100644 --- a/src/main/java/com/mitsuki/jmatrix/Matrix.java +++ b/src/main/java/com/mitsuki/jmatrix/Matrix.java @@ -24,7 +24,7 @@ import com.mitsuki.jmatrix.exception.JMatrixBaseException; import com.mitsuki.jmatrix.exception.MatrixArrayFullException; import com.mitsuki.jmatrix.exception.NullMatrixException; -import com.mitsuki.jmatrix.util.Options; +import com.mitsuki.jmatrix.internal.JMatrixUtils; import com.mitsuki.jmatrix.core.MatrixUtils; import java.util.Arrays; @@ -228,7 +228,7 @@ public Matrix(int rows, int cols) { } // Throw the exception if got one - if (cause != null) Options.raiseError(cause); + if (cause != null) JMatrixUtils.raiseError(cause); // Copy the sizes from input parameters this.ROWS = rows; @@ -290,7 +290,7 @@ public Matrix(int rows, int cols, double val) { } // Throw the exception if got one - if (cause != null) Options.raiseError(cause); + if (cause != null) JMatrixUtils.raiseError(cause); // Copy the sizes from input parameters this.ROWS = rows; @@ -353,7 +353,7 @@ public Matrix(int rows, int cols, double val) { public Matrix(double[ ][ ] arr) { // Raise the exception immediately if given array is null if (arr == null || arr.length == 0) { - Options.raiseError(new NullMatrixException( + JMatrixUtils.raiseError(new NullMatrixException( "Given two-dimensional array is null. Please ensure the array has valid elements.")); } @@ -425,7 +425,7 @@ public void create(int rows, int cols) { } // Throw the exception if got one - if (cause != null) Options.raiseError(cause); + if (cause != null) JMatrixUtils.raiseError(cause); // Copy the sizes from input parameters this.ROWS = rows; @@ -453,7 +453,7 @@ public void create(int rows, int cols) { public void create(double[ ][ ] arr) { // Raise the exception immediately if given array is null if (arr == null || arr.length == 0) { - Options.raiseError(new NullMatrixException( + JMatrixUtils.raiseError(new NullMatrixException( "Given two-dimensional array is null. Please ensure the array has valid elements.")); } @@ -511,7 +511,7 @@ public void create(double[ ][ ] arr) { public static Matrix identity(int n) { // Check for negative value on input argument if (n < 1) { - Options.raiseError(new IllegalMatrixSizeException( + JMatrixUtils.raiseError(new IllegalMatrixSizeException( "Sizes of identity matrix cannot be lower than 1.")); } @@ -571,10 +571,10 @@ else if (values.length < this.COLS) { try { throw new JMatrixBaseException(iae); } catch (final JMatrixBaseException jme) { - Options.raiseError(jme); + JMatrixUtils.raiseError(jme); } } catch (final RuntimeException re) { - Options.raiseError(re); + JMatrixUtils.raiseError(re); } // Iterate values list and fill elements of matrix array @@ -619,7 +619,7 @@ else if (this.index >= this.ROWS) { "Cannot add values anymore, Matrix is already full"); } } catch (final RuntimeException re) { - Options.raiseError(re); + JMatrixUtils.raiseError(re); } // Creates list of repeated value @@ -761,7 +761,7 @@ else if (this.ROWS != m.ROWS || this.COLS != m.COLS) { } // Throw the exception if got one - if (cause != null) Options.raiseError(cause); + if (cause != null) JMatrixUtils.raiseError(cause); // Create new matrix for the result double[ ][ ] result = new double[this.ROWS][m.COLS]; @@ -845,7 +845,7 @@ else if (this.ROWS != arr.length || this.COLS != arr[0].length) { } // Throw the exception if got one - if (cause != null) Options.raiseError(cause); + if (cause != null) JMatrixUtils.raiseError(cause); // Create new matrix for the result double[ ][ ] result = new double[this.ROWS][arr[0].length]; @@ -932,7 +932,7 @@ else if (a.length != b.length || a[0].length != b[0].length) { } // Throw the exception if got one - if (cause != null) Options.raiseError(cause); + if (cause != null) JMatrixUtils.raiseError(cause); // Create a new array for the result double[ ][ ] result = new double[a.length][b[0].length]; @@ -1020,7 +1020,7 @@ else if (a.getSize()[0] != b.getSize()[0] || a.getSize()[1] != b.getSize()[1]) { } // Throw the exception if got one - if (cause != null) Options.raiseError(cause); + if (cause != null) JMatrixUtils.raiseError(cause); // Create new matrix object Matrix matrixRes = new Matrix(a.getSize()[0], b.getSize()[1]); @@ -1106,7 +1106,7 @@ else if (this.ROWS != m.ROWS || this.COLS != m.COLS) { } // Throw the exception if got one - if (cause != null) Options.raiseError(cause); + if (cause != null) JMatrixUtils.raiseError(cause); // Create new matrix for the result double[ ][ ] result = new double[this.ROWS][m.COLS]; @@ -1186,7 +1186,7 @@ else if (this.ROWS != arr.length || this.COLS != arr[0].length) { } // Throw the exception if got one - if (cause != null) Options.raiseError(cause); + if (cause != null) JMatrixUtils.raiseError(cause); // Create new matrix for the result double[ ][ ] result = new double[this.ROWS][arr[0].length]; @@ -1272,7 +1272,7 @@ else if (a.length != b.length || a[0].length != b[0].length) { ); } - if (cause != null) Options.raiseError(cause); + if (cause != null) JMatrixUtils.raiseError(cause); // Create a new matrix array double[ ][ ] result = new double[a.length][b[0].length]; @@ -1357,7 +1357,7 @@ else if (a.getSize()[0] != b.getSize()[0] || a.getSize()[1] != b.getSize()[1]) { ); } - if (cause != null) Options.raiseError(cause); + if (cause != null) JMatrixUtils.raiseError(cause); // Create new matrix object Matrix matrixRes = new Matrix(a.getSize()[0], b.getSize()[1]); @@ -1448,7 +1448,7 @@ else if (this.COLS != m.ROWS) { } // Throw the exception if got one - if (cause != null) Options.raiseError(cause); + if (cause != null) JMatrixUtils.raiseError(cause); // Create new matrix array double[ ][ ] result = new double[this.ROWS][m.COLS]; @@ -1534,7 +1534,7 @@ else if (this.COLS != a.length) { } // Throw the exception if got one - if (cause != null) Options.raiseError(cause); + if (cause != null) JMatrixUtils.raiseError(cause); // Create new matrix array double[ ][ ] result = new double[this.ROWS][a[0].length]; @@ -1620,7 +1620,7 @@ else if (a[0].length != b.length) { } // Throw the exception if got one - if (cause != null) Options.raiseError(cause); + if (cause != null) JMatrixUtils.raiseError(cause); double[ ][ ] result = new double[a.length][b[0].length]; @@ -1707,7 +1707,7 @@ else if (a.getSize()[1] != b.getSize()[0]) { } // Throw the exception if got one - if (cause != null) Options.raiseError(cause); + if (cause != null) JMatrixUtils.raiseError(cause); // Create new matrix object Matrix result = new Matrix(a.getSize()[0], b.getSize()[1]); @@ -1762,7 +1762,7 @@ else if (a.getSize()[1] != b.getSize()[0]) { public void mult(double x) { // Throw the exception immediately if this matrix has null entries if (this.ENTRIES == null) { - Options.raiseError(new NullMatrixException( + JMatrixUtils.raiseError(new NullMatrixException( "This matrix is null. " + "Please ensure the matrix are initialized before performing scalar multiplication." )); @@ -1812,7 +1812,7 @@ public void mult(double x) { public static Matrix mult(Matrix m, double x) { // Throw the exception immediately if given matrix has null entries if (m == null || m.getEntries() == null) { - Options.raiseError(new NullMatrixException( + JMatrixUtils.raiseError(new NullMatrixException( "Given matrix is null. " + "Please ensure the matrix are initialized before performing scalar multiplication." )); @@ -1890,7 +1890,7 @@ public static Matrix mult(Matrix m, double x) { public void transpose() { // Throw the exception immediately if this matrix has null entries if (this.ENTRIES == null) { - Options.raiseError(new NullMatrixException( + JMatrixUtils.raiseError(new NullMatrixException( "This matrix is null. " + "Please ensure the matrix are initialized before performing transposition." )); @@ -1926,7 +1926,7 @@ public void transpose() { */ public static double[ ][ ] transpose(double[ ][ ] arr) { if (arr == null || arr.length == 0) { - Options.raiseError(new NullMatrixException( + JMatrixUtils.raiseError(new NullMatrixException( "Given array is null. " + "Please ensure the array has valid elements before performing transposition." )); @@ -1997,7 +1997,7 @@ public void transpose() { public static Matrix transpose(Matrix m) { // Throw the exception immediately if the given matrix has null entries if (m == null || m.getEntries() == null) { - Options.raiseError(new NullMatrixException( + JMatrixUtils.raiseError(new NullMatrixException( "Given matrix is null. " + "Please ensure the matrix are initialized before performing transposition." )); @@ -2084,7 +2084,7 @@ public boolean isSquare() { */ public static boolean isSquare(double[ ][ ] arr) { if (arr == null || arr.length == 0) { - Options.raiseError(new NullMatrixException( + JMatrixUtils.raiseError(new NullMatrixException( "Given array is null. Please ensure the array has valid elements.")); } @@ -2111,7 +2111,7 @@ public static boolean isSquare(double[ ][ ] arr) { */ public static boolean isSquare(Matrix m) { if (m == null || m.getEntries() == null) { - Options.raiseError(new NullMatrixException( + JMatrixUtils.raiseError(new NullMatrixException( "Matrix is null. Please ensure the matrix are initialized.")); } @@ -2164,7 +2164,7 @@ public boolean isDiagonal() { */ public static boolean isDiagonal(Matrix m) { if (!m.isSquare()) { - Options.raiseError(new IllegalMatrixSizeException( + JMatrixUtils.raiseError(new IllegalMatrixSizeException( "Matrix is non-square type. " + "Please ensure the matrix has the same number of rows and columns." )); @@ -2201,7 +2201,7 @@ public static boolean isDiagonal(Matrix m) { */ public static boolean isDiagonal(double[ ][ ] arr) { if (!Matrix.isSquare(arr)) { - Options.raiseError(new IllegalMatrixSizeException( + JMatrixUtils.raiseError(new IllegalMatrixSizeException( "Given array is non-square type. " + "Please ensure the array has the same number of rows and columns." )); @@ -2314,14 +2314,14 @@ public boolean isLowerTriangular() { */ public static boolean isLowerTriangular(Matrix m) { if (MatrixUtils.isNullEntries(m)) { - Options.raiseError(new NullMatrixException( + JMatrixUtils.raiseError(new NullMatrixException( "Matrix is null. Please ensure the matrix have been initialized.") ); } // The matrix must be square else if (!m.isSquare()) { - Options.raiseError(new IllegalMatrixSizeException( + JMatrixUtils.raiseError(new IllegalMatrixSizeException( "Matrix is non-square type. " + "Please ensure the matrix has the same number of rows and columns." )); @@ -2383,14 +2383,14 @@ else if (!m.isSquare()) { */ public static boolean isLowerTriangular(double[ ][ ] arr) { if (arr == null || arr.length == 0) { - Options.raiseError(new NullMatrixException( + JMatrixUtils.raiseError(new NullMatrixException( "Array is null. Please ensure the array has valid elements.") ); } // The two-dimensional array must be square else if (!Matrix.isSquare(arr)) { - Options.raiseError(new IllegalMatrixSizeException( + JMatrixUtils.raiseError(new IllegalMatrixSizeException( "Array is non-square type. " + "Please ensure the array has the same number of rows and columns." )); @@ -2503,14 +2503,14 @@ public boolean isUpperTriangular() { */ public static boolean isUpperTriangular(Matrix m) { if (MatrixUtils.isNullEntries(m)) { - Options.raiseError(new NullMatrixException( + JMatrixUtils.raiseError(new NullMatrixException( "Matrix is null. Please ensure the matrix have been initialized.") ); } // The matrix must be square else if (!m.isSquare()) { - Options.raiseError(new IllegalMatrixSizeException( + JMatrixUtils.raiseError(new IllegalMatrixSizeException( "Matrix is non-square type. " + "Please ensure the matrix has the same number of rows and columns." )); @@ -2572,14 +2572,14 @@ else if (!m.isSquare()) { */ public static boolean isUpperTriangular(double[ ][ ] arr) { if (arr == null || arr.length == 0) { - Options.raiseError(new NullMatrixException( + JMatrixUtils.raiseError(new NullMatrixException( "Array is null. Please ensure the array has valid elements.") ); } // The matrix must be square else if (!Matrix.isSquare(arr)) { - Options.raiseError(new IllegalMatrixSizeException( + JMatrixUtils.raiseError(new IllegalMatrixSizeException( "Array is non-square type. " + "Please ensure the array has the same number of rows and columns." )); @@ -2636,7 +2636,7 @@ public Matrix copy() { "Matrix is null. Please ensure the matrix are initialized."); } } catch (final NullMatrixException nme) { - Options.raiseError(nme); + JMatrixUtils.raiseError(nme); } // Create new and copy the matrix @@ -2724,7 +2724,7 @@ else if (index > this.ROWS - 1) { "Given index is too larger than number of rows."); } - if (cause != null) Options.raiseError(cause); + if (cause != null) JMatrixUtils.raiseError(cause); this.selectedIndex = index; this.hasSelect = true; @@ -2800,7 +2800,7 @@ else if (!this.hasSelect) { } // Throw the exception if got one - if (cause != null) Options.raiseError(cause); + if (cause != null) JMatrixUtils.raiseError(cause); // Change values of matrix column with values from argument parameter for (int i = 0; i < this.COLS; i++) { @@ -2855,7 +2855,7 @@ public void change(double value) { // Check if the user have not select any index row // If user have not then it will immediately raise the exception if (!this.hasSelect) { - Options.raiseError(new InvalidIndexException( + JMatrixUtils.raiseError(new InvalidIndexException( "Selected index is null. " + "Please ensure you have already called \"select(int)\" method." )); @@ -2894,7 +2894,7 @@ public void change(double value) { */ public void clear() { if (this.ENTRIES == null) { - Options.raiseError(new NullMatrixException( + JMatrixUtils.raiseError(new NullMatrixException( "Matrix is null. Please ensure the matrix have been initialized.")); } @@ -2924,7 +2924,7 @@ public void clear() { */ public void sort() { if (this.ENTRIES == null) { - Options.raiseError(new NullMatrixException( + JMatrixUtils.raiseError(new NullMatrixException( "This matrix is null. Please ensure the matrix are initialized.")); } @@ -2954,7 +2954,7 @@ public void sort() { */ public static void sort(double[ ][ ] arr) { if (arr == null || arr.length == 0) { - Options.raiseError(new NullMatrixException( + JMatrixUtils.raiseError(new NullMatrixException( "Given array is null. Please ensure the array has valid elements.")); } @@ -2981,7 +2981,7 @@ public static void sort(double[ ][ ] arr) { public static Matrix sort(Matrix m) { // Check for matrix with null entries if (m == null || m.getEntries() == null) { - Options.raiseError(new NullMatrixException( + JMatrixUtils.raiseError(new NullMatrixException( "Given matrix is null. Please ensure the matrix are initialized.")); } @@ -3100,7 +3100,7 @@ public double get(int row, int col) { } // Throw the exception if got one - if (cause != null) Options.raiseError(cause); + if (cause != null) JMatrixUtils.raiseError(cause); // Check for negative index for both inputs if (row < 0) { @@ -3204,7 +3204,7 @@ else if (index > this.ROWS - 1) { } // Throw the exception if got one - if (cause != null) Options.raiseError(cause); + if (cause != null) JMatrixUtils.raiseError(cause); System.out.println(Arrays.toString(this.ENTRIES[index])); } else { @@ -3277,7 +3277,7 @@ final public static void display(double[ ][ ] arr, int index) { } // Throw the exception if got one - if (cause != null) Options.raiseError(cause); + if (cause != null) JMatrixUtils.raiseError(cause); System.out.println(Arrays.toString(arr[index])); } diff --git a/src/main/java/com/mitsuki/jmatrix/internal/JMatrixUtils.java b/src/main/java/com/mitsuki/jmatrix/internal/JMatrixUtils.java index fc09fea0..f102d14a 100644 --- a/src/main/java/com/mitsuki/jmatrix/internal/JMatrixUtils.java +++ b/src/main/java/com/mitsuki/jmatrix/internal/JMatrixUtils.java @@ -33,17 +33,16 @@ /** * This class provides all neccessary utilities for JMatrix library. * - * @author - * Ryuu Mitsuki + * @author Ryuu Mitsuki * @version 1.5, 16 September 2023 * @since 1.0.0b.1 - * @license + * @license * Apache License 2.0 - * - * @see com.mitsuki.jmatrix.util.XMLParser */ public class JMatrixUtils { + private static final String PROGNAME = "JMatrix"; + ///// ---------------------- ///// /// Class & Packages /// ///// ---------------------- ///// @@ -54,7 +53,7 @@ public class JMatrixUtils { *

For example:

* *
 
-     *     Options.getPackageName(MyClass.class);
+     *     JMatrixUtils.getPackageName(MyClass.class);
      * 
* * @param cls the {@link Class} object. @@ -74,7 +73,7 @@ public static String getPackageName(Class cls) { *

For example:

* *
 
-     *     Options.getClassName(MyClass.class);
+     *     JMatrixUtils.getClassName(MyClass.class);
      * 
* * @param cls the {@link Class} object. @@ -242,7 +241,7 @@ public static long getLinesFile(InputStream stream) { * @see java.io.InputStream */ public static InputStream getFileAsStream(String filePath) { - return Options.class.getClassLoader().getResourceAsStream(filePath); + return JMatrixUtils.class.getClassLoader().getResourceAsStream(filePath); } /** @@ -264,73 +263,13 @@ public static boolean writeToFile(String filePath, String[ ] contents) { return true; } catch (final IOException ioe) { - Options.raiseError(new JMatrixBaseException(ioe), 0); + JMatrixUtils.raiseError(new JMatrixBaseException(ioe), 0); } return false; } - ///// ------------------ ///// - /// Contents /// - ///// ------------------ ///// - - /** - * Returns a list containing the help message contents. - * - * @return the contents of help message. - * - * @since 1.0.0b.1 - * @see #readFile(InputStream) - * @see #getFileAsStream(String) - * @see #removeComment(String[]) - */ - public static String[ ] getHelpMsg() { - return removeComment(readFile(getFileAsStream(contentsPath + "help.content"))); - } - - /** - * Returns a list containing the copyright contents. - * - * @return the contents of copyright. - * - * @since 1.0.0b.1 - * @see #readFile(InputStream) - * @see #getFileAsStream(String) - * @see java.lang.StringBuilder - */ - public static String[ ] getCopyright() { - final String[ ] contents = readFile(getFileAsStream(contentsPath + "additional.content")); - String[ ] copyright = new String[contents.length]; - - for (int i = 0; i < contents.length; i++) { - // Ignore the comment - if (!contents[i].startsWith("#")) { - StringBuilder sb = new StringBuilder(); - for (int j = 0; j < contents[i].length(); j += 2) { - String bytes = contents[i].substring(j, j + 2); - sb.append((char) Integer.parseInt(bytes, 16)); - } - copyright[i] = sb.toString(); - } else { - copyright[i] = contents[i]; - } - } - - for (int i = 0; i != contents.length; i++) { - if (copyright[i].contains("${PACKAGE_NAME}")) { - copyright[i] = copyright[i].replace("${PACKAGE_NAME}", XML.getProperty("programName")); - } - - if (copyright[i].contains("${AUTHOR}")) { - copyright[i] = copyright[i].replace("${AUTHOR}", XML.getProperty("author")); - } - } - - return removeComment(copyright); - } - - /** * Removes comment lines from an array of strings. * diff --git a/src/main/java/com/mitsuki/jmatrix/internal/XMLParser.java b/src/main/java/com/mitsuki/jmatrix/internal/XMLParser.java index 41f03925..f6e80f94 100644 --- a/src/main/java/com/mitsuki/jmatrix/internal/XMLParser.java +++ b/src/main/java/com/mitsuki/jmatrix/internal/XMLParser.java @@ -119,7 +119,7 @@ static String getData(final String choice) { break; default: - Options.raiseError( + JMatrixUtils.raiseError( new JMatrixBaseException( new IllegalArgumentException( "Cannot retrieve data for input \"" + choice + "\"" @@ -143,7 +143,7 @@ static String getData(final String choice) { * @since 1.0.0b.1 * @license * Apache License 2.0 - * @see com.mitsuki.jmatrix.util.Options + * @see com.mitsuki.jmatrix.internal.JMatrixUtils */ public class XMLParser implements XMLData { @@ -187,7 +187,7 @@ public XMLParser(XMLType type) { break; default: - Options.raiseError( + JMatrixUtils.raiseError( new JMatrixBaseException( new IllegalArgumentException( "Invalid XML type: \"" + type + "\"" @@ -216,7 +216,7 @@ public XMLParser(XMLType type) { XMLConfig.betaNum = xml.getElementsByTagName("beta_num").item(0).getTextContent().strip(); } } catch (final Exception e) { - Options.raiseError(new JMatrixBaseException(e), -1); + JMatrixUtils.raiseError(new JMatrixBaseException(e), -1); } } From bd8d304e26bc2b46bdade6914f164bfab1f8f043 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Sun, 17 Sep 2023 16:47:10 +0700 Subject: [PATCH 09/36] Fix errors during properties initializer Removed several checks from checking the setup properties file This will only occurs error because the `File` class checks the file from the current directory. Replaced by checking the `InputStream` whether it is null, null is indicates that InputStream cannot found the specified file inside JAR's directory tree. --- .../jmatrix/internal/PropertiesParser.java | 31 +++++-------------- 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java b/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java index 3552601c..5f462a27 100644 --- a/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java +++ b/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java @@ -19,11 +19,9 @@ package com.mitsuki.jmatrix.internal; -import java.io.File; +import java.io.FileNotFoundException; import java.io.InputStream; import java.io.IOException; -import java.io.FileNotFoundException; -import java.nio.file.AccessDeniedException; import java.util.Properties; @@ -161,27 +159,6 @@ final class SetupProperties { * it will checks whether the properties file is exist and readable. */ static { - File setupFile_FileObj = new File(setupFile); - - // Store the exception and will be thrown later if not null - Throwable causeException = null; - - if (!setupFile_FileObj.exists()) { - causeException = new FileNotFoundException( - String.format("Cannot found '%s' file", setupFile) - ); - } else if (setupFile_FileObj.exists() && - !setupFile_FileObj.canRead()) { - causeException = new AccessDeniedException(setupFile, - null, "Read access is denied" - ); - } - - // It is a good practice to throw occurred exception immediately, - // especially after exiting a code block (e.g., if-else statement block) - if (causeException != null) - throw new ExceptionInInitializerError(causeException); - // Although the Properties class has been designed to be synchronized, // the code below further ensures that it is implicitly synchronized if (setupProperties == null) { @@ -190,6 +167,12 @@ final class SetupProperties { .getResourceAsStream(setupFile)) { setupProperties = new Properties(); setupProperties.load(inStream); + + if (setupProperties == null) { + throw new FileNotFoundException(String.format( + "InputStream cannot found '%s' file", setupFile + )); + } } catch (final IOException ioe) { throw new ExceptionInInitializerError(ioe); } From bcd0b0ec848997333f58c64b9276429447a40db6 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Sun, 17 Sep 2023 18:10:32 +0700 Subject: [PATCH 10/36] Enhance the setup properties parser The setup properties parser has improved in several factors, one of which is checking the existence of the file. --- .../jmatrix/internal/PropertiesParser.java | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java b/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java index 5f462a27..004b7180 100644 --- a/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java +++ b/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java @@ -19,10 +19,12 @@ package com.mitsuki.jmatrix.internal; +import java.io.File; import java.io.FileNotFoundException; import java.io.InputStream; import java.io.IOException; import java.util.Properties; +import java.net.URL; /** @@ -155,24 +157,30 @@ final class SetupProperties { private static Properties setupProperties; /* Immediately search and check the setup properties file, then retrieve - * all properties data from it. Before retrieving the properties, - * it will checks whether the properties file is exist and readable. + * all properties data from it. Throw IOException if I/O errors occurred. */ static { // Although the Properties class has been designed to be synchronized, // the code below further ensures that it is implicitly synchronized if (setupProperties == null) { - try (InputStream inStream = SetupProperties.class + try { + URL urlSetupFile = SetupProperties.class .getClassLoader() - .getResourceAsStream(setupFile)) { - setupProperties = new Properties(); - setupProperties.load(inStream); + .getResource(setupFile); - if (setupProperties == null) { + // Return an error if the resource file cannot be found + if (urlSetupFile == null) { throw new FileNotFoundException(String.format( - "InputStream cannot found '%s' file", setupFile + "ClassLoader cannot found '%s' file", setupFile )); } + + // Convert the URL to InputStream + InputStream inStream = urlSetupFile.openStream(); + + // Load the properties file + setupProperties = new Properties(); + setupProperties.load(inStream); } catch (final IOException ioe) { throw new ExceptionInInitializerError(ioe); } From edc1672f3ffd343c0297d8a0abc24aae7814086f Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Sun, 17 Sep 2023 20:15:57 +0700 Subject: [PATCH 11/36] Refactor the main method on parsing several arguments * Added new method called `getFirstArgument` that gets the first known argument. The known arguments in this changes (the arguments are not different with the previous ones): "-V", "--version", "ver", "version" : Display the JMatrix version "-cr", "--copyright", "copyright" : Display the copyright and license * Added several necessary private members --- .../mitsuki/jmatrix/internal/MainClass.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java b/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java index 1b22b811..ed4b41ad 100644 --- a/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java +++ b/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java @@ -25,6 +25,7 @@ import java.util.Iterator; import java.util.List; import java.util.LinkedHashSet; +import java.util.Properties; import java.util.Set; import java.lang.Iterable; @@ -46,6 +47,16 @@ */ public class MainClass { + private static Properties setupProperties = SetupProperties.getSetupProperties(); + + private static List versionArgs = Arrays.asList( + "-V", "--version", "ver", "version" + ); + + private static List copyrightArgs = Arrays.asList( + "-cr", "--copyright", "copyright" + ); + /** * Main method for JMatrix library. * @@ -54,6 +65,52 @@ public class MainClass { * @since 1.0.0b.1 */ public static void main(String[] args) { + final String name = setupProperties.getProperty("JM-Name"); + final String version = setupProperties.getProperty("JM-Version"); + final String author = setupProperties.getProperty("JM-Author"); + final String license = setupProperties.getProperty("JM-License"); + final String groupId = setupProperties.getProperty("JM-GroupId"); + final String artifactId = setupProperties.getProperty("JM-ArtifactId"); + + // Parse the command line arguments, removing all duplicate arguments + List parsedArgs = new ArgumentsParser(args) + .getArguments(); + + // Return immediately if the parsed args is empty + if (parsedArgs.isEmpty()) return; + + StringBuilder sb = new StringBuilder(); + final String firstArg = getFirstArgument(parsedArgs); + boolean hasOutput = false; + + // Check for version arguments + if (versionArgs.contains(firstArg)) { + sb.append(String.format("%s v%s", name, version)) + .append(String.format(" <%s:%s>", groupId, artifactId)); + hasOutput = true; + + // Check for copyright arguments + } else if (copyrightArgs.contains(firstArg)) { + sb.append(String.format("%s - Copyright (C) 2023 %s", name, author)) + .append(System.lineSeparator()) + .append(String.format("Licensed under the \"%s\"", license)); + hasOutput = true; + } + + if (hasOutput) + System.out.println(sb.toString()); + } + + + static String getFirstArgument(List args) { + List allKnownArgs = new ArrayList<>(); + allKnownArgs.addAll(versionArgs); + allKnownArgs.addAll(copyrightArgs); + + return args.stream() + .filter(arg -> allKnownArgs.contains(arg)) + .findFirst() // Get the first index + .orElse(null); // Get and return the value, null if not present } } From 20935c60942aa60debd08ce01c4b65e0077f8f70 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Sun, 17 Sep 2023 21:11:45 +0700 Subject: [PATCH 12/36] Add the javadocs for undocumented members * Added documentations for undocumented members * Bump version for classes that has been modified For `ArgumentsParser` class, it has been modified but forgot to bump the version. See 3415b74 --- .../mitsuki/jmatrix/internal/MainClass.java | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java b/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java index ed4b41ad..6111b0b4 100644 --- a/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java +++ b/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java @@ -39,7 +39,7 @@ * {@code Main} to {@code MainClass}. * * @author Ryuu Mitsuki - * @version 1.4, 16 September 2023 + * @version 1.45, 17 September 2023 * @since 1.0.0b.1 * @see com.mitsuki.jmatrix.Matrix * @license @@ -47,12 +47,21 @@ */ public class MainClass { + /** + * A {@link Properties} object reference to synchronized setup properties. + */ private static Properties setupProperties = SetupProperties.getSetupProperties(); + /** + * A list of known version arguments. + */ private static List versionArgs = Arrays.asList( "-V", "--version", "ver", "version" ); + /** + * A list of known copyright arguments. + */ private static List copyrightArgs = Arrays.asList( "-cr", "--copyright", "copyright" ); @@ -102,6 +111,22 @@ public static void main(String[] args) { } + /** + * Searches and returns the first known argument from a list of arguments. + * This method compares the provided list of arguments with a predefined + * list of known version and copyright arguments. + * + *

If a known argument is found, it is returned as the first + * known argument. If none of the provided arguments match the known ones, + * it returns {@code null}. + * + * @param args A list of arguments to search for known arguments. + * + * @return The first known argument found, or {@code null} + * if none are known. + * + * @since 1.5.0 + */ static String getFirstArgument(List args) { List allKnownArgs = new ArrayList<>(); allKnownArgs.addAll(versionArgs); @@ -123,7 +148,7 @@ static String getFirstArgument(List args) { * @param The type of object stored in this class. * * @author Ryuu Mitsuki - * @version 1.0, 16 September 2023 + * @version 1.2, 17 September 2023 * @since 1.5.0 */ final class ArgumentsParser implements Iterable { From c32307db25985e802bafae6313a9cd95b4ecafac Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Tue, 12 Dec 2023 17:10:09 +0700 Subject: [PATCH 13/36] Fix warning due to unspecified encoding Deleted `build.resources` (default) and replaced to use the Maven Resources Plugin for reliability and ensure the properties encoding are specified with `propertiesEncoding` configuration. Additionally, all necessary files also would be copied during validation using of goal `copy-resources`. --- pom.xml | 55 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/pom.xml b/pom.xml index 3906e555..76a3009a 100644 --- a/pom.xml +++ b/pom.xml @@ -104,24 +104,6 @@ - - - ${paths.resourcesDir} - true - - - ${basedir} - - LICENSE - - - - ${basedir}/META-INF - true - - - - @@ -154,6 +136,43 @@ + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + ${_encoding} + + + + copy-resources + validate + + copy-resources + + + ${paths.classesDir} + + + ${paths.resourcesDir} + true + + + ${basedir} + + LICENSE + + + + ${basedir}/META-INF + true + + + + + + From 86bbddd0ebbbc4a41a32c8d062450f93156c5754 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Tue, 12 Dec 2023 17:15:08 +0700 Subject: [PATCH 14/36] Fix main class attempt to access package-private We resolved this by adding a new file to separate SetupProperties class from the public one, its name the same as its contained class. --- .../jmatrix/internal/PropertiesParser.java | 75 -------------- .../jmatrix/internal/SetupProperties.java | 98 +++++++++++++++++++ 2 files changed, 98 insertions(+), 75 deletions(-) create mode 100644 src/main/java/com/mitsuki/jmatrix/internal/SetupProperties.java diff --git a/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java b/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java index 004b7180..cc4992d2 100644 --- a/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java +++ b/src/main/java/com/mitsuki/jmatrix/internal/PropertiesParser.java @@ -19,12 +19,9 @@ package com.mitsuki.jmatrix.internal; -import java.io.File; -import java.io.FileNotFoundException; import java.io.InputStream; import java.io.IOException; import java.util.Properties; -import java.net.URL; /** @@ -127,75 +124,3 @@ public Properties getProperties() { return this.properties; } } - - -/** - * This class provides access to get the setup properties and it is designed - * to be synchronized and thread-safe. - * - *

The setup properties are retrieved from the file statically using - * the {@code static} block, which means that when the program starts it will - * immediately retrieve the properties from the file. - * - * @author Ryuu Mitsuki - * @version 1.0, 16 September 2023 - * @since 1.5.0 - * @license - * Apache License 2.0 - */ -final class SetupProperties { - - /** - * A string represents a file path to the setup properties stored in. - */ - private static final String setupFile = "configuration/setup.properties"; - - /** - * A {@code Properties} object that stores all properties data - * from setup properties file. - */ - private static Properties setupProperties; - - /* Immediately search and check the setup properties file, then retrieve - * all properties data from it. Throw IOException if I/O errors occurred. - */ - static { - // Although the Properties class has been designed to be synchronized, - // the code below further ensures that it is implicitly synchronized - if (setupProperties == null) { - try { - URL urlSetupFile = SetupProperties.class - .getClassLoader() - .getResource(setupFile); - - // Return an error if the resource file cannot be found - if (urlSetupFile == null) { - throw new FileNotFoundException(String.format( - "ClassLoader cannot found '%s' file", setupFile - )); - } - - // Convert the URL to InputStream - InputStream inStream = urlSetupFile.openStream(); - - // Load the properties file - setupProperties = new Properties(); - setupProperties.load(inStream); - } catch (final IOException ioe) { - throw new ExceptionInInitializerError(ioe); - } - } - } - - /** - * Gets the synchronized setup properties from this class. - * - * @return A synchronized instance of {@link Properties} from this - * class containing all setup properties data. - * - * @since 1.5.0 - */ - static Properties getSetupProperties() { - return setupProperties; - } -} diff --git a/src/main/java/com/mitsuki/jmatrix/internal/SetupProperties.java b/src/main/java/com/mitsuki/jmatrix/internal/SetupProperties.java new file mode 100644 index 00000000..9ea6d881 --- /dev/null +++ b/src/main/java/com/mitsuki/jmatrix/internal/SetupProperties.java @@ -0,0 +1,98 @@ +// --------------------- // +/* SetupProperties */ +// --------------------- // + +/* Copyright (c) 2023 Ryuu Mitsuki + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mitsuki.jmatrix.internal; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import java.net.URL; + +/** + * This class provides access to get the setup properties and it is designed + * to be synchronized and thread-safe. + * + *

The setup properties are retrieved from the file statically using + * the {@code static} block, which means that when the program starts it will + * immediately retrieve the properties from the file. + * + * @author Ryuu Mitsuki + * @version 1.1, 12 December 2023 + * @since 1.5.0 + * @license + * Apache License 2.0 + */ +final class SetupProperties { + + /** + * A string represents a file path to the setup properties stored in. + */ + private static final String setupFile = "configuration/setup.properties"; + + /** + * A {@code Properties} object that stores all properties data + * from setup properties file. + */ + private static Properties setupProperties; + + /* Immediately search and check the setup properties file, then retrieve + * all properties data from it. Throw IOException if I/O errors occurred. + */ + static { + // Although the Properties class has been designed to be synchronized, + // the code below further ensures that it is implicitly synchronized + if (setupProperties == null) { + try { + URL urlSetupFile = SetupProperties.class + .getClassLoader() + .getResource(setupFile); + + // Return an error if the resource file cannot be found + if (urlSetupFile == null) { + throw new FileNotFoundException(String.format( + "ClassLoader cannot found '%s' file", setupFile + )); + } + + // Convert the URL to InputStream + InputStream inStream = urlSetupFile.openStream(); + + // Load the properties file + setupProperties = new Properties(); + setupProperties.load(inStream); + } catch (final IOException ioe) { + throw new ExceptionInInitializerError(ioe); + } + } + } + + /** + * Gets the synchronized setup properties from this class. + * + * @return A synchronized instance of {@link Properties} from this + * class containing all setup properties data. + * + * @since 1.5.0 + */ + static Properties getSetupProperties() { + return setupProperties; + } +} From 120e58891b3c733b692ca401239acc62a08f79a7 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Tue, 12 Dec 2023 17:39:19 +0700 Subject: [PATCH 15/36] Refactor the `JMatrixUtils` class Now it uses the SetupProperties class to get the program name instead. In addition, we also added the Javadoc for undocumented variables. --- .../jmatrix/internal/JMatrixUtils.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/mitsuki/jmatrix/internal/JMatrixUtils.java b/src/main/java/com/mitsuki/jmatrix/internal/JMatrixUtils.java index f102d14a..cbe3c7fe 100644 --- a/src/main/java/com/mitsuki/jmatrix/internal/JMatrixUtils.java +++ b/src/main/java/com/mitsuki/jmatrix/internal/JMatrixUtils.java @@ -29,19 +29,28 @@ import java.io.IOException; import java.util.List; import java.util.Arrays; +import java.util.Properties; /** * This class provides all neccessary utilities for JMatrix library. * * @author Ryuu Mitsuki - * @version 1.5, 16 September 2023 + * @version 1.6, 12 December 2023 * @since 1.0.0b.1 * @license * Apache License 2.0 */ public class JMatrixUtils { - private static final String PROGNAME = "JMatrix"; + /** + * A {@link Properties} object reference to synchronized setup properties. + */ + private static final Properties setupProperties = SetupProperties.getSetupProperties(); + + /** + * A string variable holding the program name, retrieved from {@link SetupProperties}. + */ + private static final String PROGNAME = setupProperties.getProperty("JM-Name"); ///// ---------------------- ///// /// Class & Packages /// @@ -128,9 +137,9 @@ public static String getClassNameFromTemplate(T template) { - ///// -------------------- ///// - /// Files Options /// - ///// -------------------- ///// + ///// --------------------- ///// + /// Files Utilities /// + ///// --------------------- ///// /** * Returns the total lines of specified file. From d34c165bebdc3d1a9b239ce6047d8c4806b316a7 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Tue, 12 Dec 2023 21:00:25 +0700 Subject: [PATCH 16/36] Introduce a new list of help args and function * Added a new variable to holds several help arguments * Included the `helpArgs` to all known arguments in `getFirstArgument` method * Introduced a new private function to print the help message --- .../mitsuki/jmatrix/internal/MainClass.java | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java b/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java index 6111b0b4..80743558 100644 --- a/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java +++ b/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java @@ -35,11 +35,11 @@ * such as JMatrix version. If this class is imported, it will be * not valuable because it does not provides APIs to construct the matrices. * - * As of JMatrix 1.5, the class name has changed from + * As of JMatrix 1.5.0, the class name has changed from * {@code Main} to {@code MainClass}. * * @author Ryuu Mitsuki - * @version 1.45, 17 September 2023 + * @version 1.5, 12 December 2023 * @since 1.0.0b.1 * @see com.mitsuki.jmatrix.Matrix * @license @@ -66,6 +66,13 @@ public class MainClass { "-cr", "--copyright", "copyright" ); + /** + * A list of known help arguments. + */ + private static List helpArgs = Arrays.asList( + "-h", "--help", "help", "?" /* << Exclusive to help arguments */ + ); + /** * Main method for JMatrix library. * @@ -131,12 +138,48 @@ static String getFirstArgument(List args) { List allKnownArgs = new ArrayList<>(); allKnownArgs.addAll(versionArgs); allKnownArgs.addAll(copyrightArgs); + allKnownArgs.addAll(helpArgs); return args.stream() .filter(arg -> allKnownArgs.contains(arg)) .findFirst() // Get the first index .orElse(null); // Get and return the value, null if not present } + + /** + * Prints the help message to the standard output stream. + * + * @since 1.5.0 + */ + private static void printHelpMessage() { + final String newline = System.lineSeparator(), // A newline + dblNewline = newline + newline; // Double newlines + + final String header = String.format("%s v%s", + setupProperties.getProperty("JM-Name"), + setupProperties.getProperty("JM-Version") + ); + + final StringBuilder sb = new StringBuilder(), + lineSb = new StringBuilder(); + + // Create a line with length equals to header length + for (int i = 0; i < header.length(); i++) lineSb.append('-'); + + sb.append(header + newline) + .append(lineSb.toString() + dblNewline) + .append(String.format( + "Usage:%s java -jar path/to/jmatrix.jar [options]", newline)) + .append(String.format("%sOptions:%s", dblNewline, newline)) + .append(" ver, version, -V, --version" + newline) + .append(" Print the current version of JMatrix." + dblNewline) + .append(" copyright, -cr, --copyright" + newline) + .append(" Print the copyright and license." + dblNewline) + .append(" ?, help, -h, --help" + newline) + .append(" Print this help message."); + + System.out.println(sb.toString()); + } } From ce3b224e24ea45e5316f30e6a639912f0f092373 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Tue, 12 Dec 2023 21:16:13 +0700 Subject: [PATCH 17/36] Refactor the command-line arguments checker * If no arguments specified, print the help message then return * Implemented the help arguments checker * Added a capability to print the version only by specifying "--version-only" as first argument --- .../mitsuki/jmatrix/internal/MainClass.java | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java b/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java index 80743558..8a068a4e 100644 --- a/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java +++ b/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java @@ -92,29 +92,34 @@ public static void main(String[] args) { List parsedArgs = new ArgumentsParser(args) .getArguments(); - // Return immediately if the parsed args is empty - if (parsedArgs.isEmpty()) return; + // Print the help message if the parsed args is empty + if (parsedArgs.isEmpty()) { + printHelpMessage(); + return; // then return to stop further execution + } StringBuilder sb = new StringBuilder(); - final String firstArg = getFirstArgument(parsedArgs); - boolean hasOutput = false; + final String firstArg = getFirstArgument(parsedArgs), + newline = System.lineSeparator(); // Check for version arguments if (versionArgs.contains(firstArg)) { sb.append(String.format("%s v%s", name, version)) .append(String.format(" <%s:%s>", groupId, artifactId)); - hasOutput = true; - // Check for copyright arguments } else if (copyrightArgs.contains(firstArg)) { - sb.append(String.format("%s - Copyright (C) 2023 %s", name, author)) - .append(System.lineSeparator()) - .append(String.format("Licensed under the \"%s\"", license)); - hasOutput = true; + sb.append(String.format("%s - Copyright (C) 2023 %s%s", + name, author, newline)) + .append(String.format("Licensed under the %s", license)); + // Check for help arguments + } else if (helpArgs.contains(firstArg)) { + printHelpMessage(); + } else if (firstArg.equals("--version-only")) { + sb.append(version); } - if (hasOutput) - System.out.println(sb.toString()); + // Print to the console (standard output stream) + if (sb.toString().length() != 0) System.out.println(sb.toString()); } @@ -139,6 +144,7 @@ static String getFirstArgument(List args) { allKnownArgs.addAll(versionArgs); allKnownArgs.addAll(copyrightArgs); allKnownArgs.addAll(helpArgs); + allKnownArgs.add("--version-only"); return args.stream() .filter(arg -> allKnownArgs.contains(arg)) From de53e2bd2bbcf7f77cc904a35e607e49b2677928 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Wed, 13 Dec 2023 18:06:32 +0700 Subject: [PATCH 18/36] Add link reference for issues on help message --- .../com/mitsuki/jmatrix/internal/MainClass.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java b/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java index 8a068a4e..149e959c 100644 --- a/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java +++ b/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java @@ -174,15 +174,21 @@ private static void printHelpMessage() { sb.append(header + newline) .append(lineSb.toString() + dblNewline) - .append(String.format( - "Usage:%s java -jar path/to/jmatrix.jar [options]", newline)) - .append(String.format("%sOptions:%s", dblNewline, newline)) + .append("USAGE:" + newline) + .append(" java -jar path/to/jmatrix.jar [options]" + dblNewline) + .append("OPTIONS:" + newline) .append(" ver, version, -V, --version" + newline) .append(" Print the current version of JMatrix." + dblNewline) .append(" copyright, -cr, --copyright" + newline) .append(" Print the copyright and license." + dblNewline) .append(" ?, help, -h, --help" + newline) - .append(" Print this help message."); + .append(" Print this help message." + dblNewline) + .append("ISSUE:" + newline) + .append(String.format( + " Want to report some issues? Let's help us improve JMatrix.%s" + + " ", + newline + )); System.out.println(sb.toString()); } From 2f257e6e7f73926e6d6946af8bbe783891f57420 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Fri, 5 Jan 2024 15:31:55 +0700 Subject: [PATCH 19/36] Install the JMBuilder as project builder utilizes Python * Removed old Python files and replaced with the new builder * Now the builder will not inside 'src/main/python', instead it moved to 'tools' directory, which more concise directory name. * Removed the 'requirements.txt' in the root directory, replaced with the JMBuilder's one (within its root directory) Repo: https://github.com/mitsuki31/JMBuilder.git Pages: https://mitsuki31.github.io/JMBuilder --- requirements.txt | 2 - src/main/python/fix_config.py | 615 ------------------ src/main/python/generate_list.py | 287 -------- src/main/python/utils.py | 457 ------------- tools/JMBuilder/LICENSE | 21 + tools/JMBuilder/README.md | 12 + tools/JMBuilder/jmbuilder/.config/setup.json | 6 + tools/JMBuilder/jmbuilder/__init__.py | 118 ++++ tools/JMBuilder/jmbuilder/__main__.py | 333 ++++++++++ tools/JMBuilder/jmbuilder/_globals.py | 335 ++++++++++ tools/JMBuilder/jmbuilder/core.py | 492 ++++++++++++++ .../JMBuilder/jmbuilder/exception/__init__.py | 33 + .../jmbuilder/exception/exception.py | 381 +++++++++++ tools/JMBuilder/jmbuilder/tests/__init__.py | 16 + tools/JMBuilder/jmbuilder/tests/test_utils.py | 134 ++++ tools/JMBuilder/jmbuilder/utils/__init__.py | 25 + tools/JMBuilder/jmbuilder/utils/logger.py | 195 ++++++ tools/JMBuilder/jmbuilder/utils/utils.py | 525 +++++++++++++++ tools/JMBuilder/requirements.txt | 6 + tools/JMBuilder/test-requirements.txt | 9 + 20 files changed, 2641 insertions(+), 1361 deletions(-) delete mode 100644 requirements.txt delete mode 100644 src/main/python/fix_config.py delete mode 100644 src/main/python/generate_list.py delete mode 100644 src/main/python/utils.py create mode 100644 tools/JMBuilder/LICENSE create mode 100644 tools/JMBuilder/README.md create mode 100644 tools/JMBuilder/jmbuilder/.config/setup.json create mode 100644 tools/JMBuilder/jmbuilder/__init__.py create mode 100644 tools/JMBuilder/jmbuilder/__main__.py create mode 100644 tools/JMBuilder/jmbuilder/_globals.py create mode 100644 tools/JMBuilder/jmbuilder/core.py create mode 100644 tools/JMBuilder/jmbuilder/exception/__init__.py create mode 100644 tools/JMBuilder/jmbuilder/exception/exception.py create mode 100644 tools/JMBuilder/jmbuilder/tests/__init__.py create mode 100644 tools/JMBuilder/jmbuilder/tests/test_utils.py create mode 100644 tools/JMBuilder/jmbuilder/utils/__init__.py create mode 100644 tools/JMBuilder/jmbuilder/utils/logger.py create mode 100644 tools/JMBuilder/jmbuilder/utils/utils.py create mode 100644 tools/JMBuilder/requirements.txt create mode 100644 tools/JMBuilder/test-requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 6327d76c..00000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -beautifulsoup4==4.11.1 -lxml==4.9.3 diff --git a/src/main/python/fix_config.py b/src/main/python/fix_config.py deleted file mode 100644 index 65329269..00000000 --- a/src/main/python/fix_config.py +++ /dev/null @@ -1,615 +0,0 @@ -""" -Program that fixes all JMatrix configurations. -""" -__author__ = 'Ryuu Mitsuki' -__all__ = ['FixConfig'] - -import os -import sys -import json -import re -from typing import Union, Optional -from utils import Utils, FileUtils - -try: - from bs4 import BeautifulSoup - from bs4.formatter import XMLFormatter -except ModuleNotFoundError as bs_not_found: - Utils.info_msg('Please install all the requirements first.' + os.linesep) - Utils.raise_error(bs_not_found, -1, file=__file__) - - -class FixConfig: - ''' - This class will fix the JMatrix configurations for specified file(s). - - File(s) to be fixed: - - MANIFEST.MF - - Makefile - - config.xml - ''' - __SOURCES_PATH: str = (os.sep).join(['src', 'main']) + os.sep - __TARGET_PATH: str = f'target{os.sep}' - __CLASSES_PATH: str = __TARGET_PATH + f'classes{os.sep}' - __CACHE_PATH: str = __TARGET_PATH + f'.cache{os.sep}' - __RESOURCES_PATH: str = __SOURCES_PATH + f'resources{os.sep}' - - __TARGET_FILES: tuple = ('config.xml', 'Makefile', 'MANIFEST.MF',) - __verbose: bool = False - - def __init__(self, verbose: bool = False) -> None: - """ - This constructor will construct new object of `FixConfig`. - - Parameters: - - verbose: bool (default = False) - Boolean value that specifies whether to print verbose output. - - Returns: - None - - Raises: - None - """ - self.__verbose = verbose - - - def run(self) -> None: - """ - Runs this program and fixes all JMatrix configurations data. - - Parameters: - None - - Returns: - None - - Raises: - None - """ - self.__get_pom_data(cache=True, out='config.json') - - # [config.xml] - self.__fix_configuration( - None, - cached=f'{self.__CACHE_PATH}config.json', - target='config.xml' - ) - - # [Makefile] - self.__fix_configuration( - None, - cached=f'{self.__CACHE_PATH}config.json', - target='makefile' - ) - - # [MANIFEST.MF] - self.__fix_configuration( - None, - cached=f'{self.__CACHE_PATH}config.json', - target='manifest.mf' - ) - - - def __create_cache(self, - data: Union[dict, str], indent: int = 4, out: str = None) -> None: - """ - Create the cache of `data` and store it to `target/.cache/` directory - with specified file name. - The function supports creating two types of cache (JSON and string). - For JSON cache, the `data` parameter should be a dictionary object. - - Parameters: - - data: Union[dict, str] - Data object that want to be cached. - The type is optional, can be 'dict' or 'str'. - Recommended use 'dict' object for creating JSON cache. - - - indent: int (default = 4) - An integer value that specifies number of spaces for - indentation in the JSON cache. - - - out: str (default = None) - String representing the name of the output file. - The output directory is `target/.cache/` directory. - - Returns: - None - - Raises: - - RuntimeError - If `out` parameter is empty. - - - PermissionError - If the directory path for output file is restricted - and cannot be accessed by program. - - """ - FileUtils.check_directory(self.__CACHE_PATH, verbose=self.__verbose) - - if self.__verbose: - Utils.pr_banner('CREATE CACHE') - Utils.info_msg(f'Creating cache for id:<{id(data)}> to "{out}"...') - - try: - if out is None: - msg = 'Please insert name for output file' - raise RuntimeError(msg) - except RuntimeError as run_err: - Utils.raise_error(run_err, -1, file=__file__) - - if isinstance(data, str): - try: - if self.__verbose: - Utils.info_msg(os.linesep + \ - f'Writing "{data.strip()}" -> \'{out}\'...') - - with open(self.__CACHE_PATH + out, 'w', encoding='utf-8') as cache: - cache.write(data) - except PermissionError as perm_err: - Utils.raise_error(perm_err, -1, file=__file__) - - if self.__verbose: - print() - Utils.info_msg(f'Cache created, saved in "{self.__CACHE_PATH}{out}".') - - elif isinstance(data, dict): - try: - with open(self.__CACHE_PATH + out, 'w', encoding='utf-8') as cache: - cache.write(json.dumps(data, indent=indent) + os.linesep) - - if self.__verbose: - print() - for key, val in data.items(): - Utils.info_msg(f'Writing ("{key}", "{val}") -> \'{out}\'...') - except PermissionError as perm_err: - Utils.raise_error(perm_err, -1, file=__file__) - - if self.__verbose: - print() - Utils.info_msg(f'Cache created, saved in "{self.__CACHE_PATH}{out}".') - Utils.pr_banner('(END) CREATE CACHE', newline=1) - - - def __get_cache(self, cache_path: str) -> dict: - """ - Method that gets cached configurations data from specified path. - - Parameters: - - cache_path: str - Path to cached configurations data - - Returns: - A dictionary containing all cached configurations data. - - Raises: - - FileNotFoundError - If the given cache path is does not exist. - - - IsADirectoryError - If the given cache path is exist but a directory. - """ - cache_data: dict = { } - - try: - if not os.path.exists(cache_path): - msg = 'Path to cached configuration data does not exist' - raise FileNotFoundError(msg) - if os.path.exists(cache_path) and not os.path.isfile(cache_path): - msg = 'Path to cached configuration data is a directory' - raise IsADirectoryError(msg) - - cache_data = json.loads(FileUtils.read_file( - cache_path, _list=False, - verbose=self.__verbose - )) - - except FileNotFoundError as nf_err: - Utils.raise_error(nf_err, 2, file=__file__) - except IsADirectoryError as dir_err: - Utils.raise_error(dir_err, 1, file=__file__) - - return cache_data - - - def __get_output_path(self, target: str) -> str: - """ - Gets the correct output path for targeted file. - - Parameters: - - target: str - The targeted file to gets it's correct output path. - - Returns: - A string representing output path for targeted file. - - Raises: - None - """ - path: str = None - - if target.lower() == self.__TARGET_FILES[0]: - path = self.__CLASSES_PATH + f'configuration{os.sep}' - if target.lower() == self.__TARGET_FILES[1].lower(): - path = '.' + os.sep - if target.lower() == self.__TARGET_FILES[2].lower(): - path = 'META-INF' + os.sep - - return path - - - def __get_version(self, list_data: list = None, **kwargs) -> Union[tuple, str]: - """ - Search for a version string in a list of data - using a regex pattern and return version information. - - Parameters: - - list_data: list (default = None) - A list of data to be searched. - - - **kwargs - Additional keyword arguments. - - + regex: str - A regex pattern to match against the list elements. - - + config_data: dict - Dictionary containing the configuration data and version. - - + fixed_only: bool (optional) - If True, returns only the fixed version string. - - Returns: - If a match is found, a tuple containing the index of the - matching element, the new version string, - and the original element content. - If 'fixed_only' is True, returns the fixed version string only. - - Raises: - None - """ - config_data: dict = kwargs.get('config_data', {}) - fixed_version: str = config_data.get('package.version.core', 'null') - if config_data.get('package.releaseType', 'null').lower() not in ('release', 'stable'): - fixed_version += '-' + config_data.get('package.releaseType', 'null') + \ - '.' + config_data.get('package.betaNum', 'null') - - if kwargs.get('fixed_only', False): - return fixed_version - - if not list_data or len(list_data) == 0: - Utils.raise_error( - ValueError('List data cannot be empty'), - 1, file=__file__ - ) - - regex: str = kwargs.get('regex') - new_version: str = None - index: int = -99 - - for i, __data in enumerate(list_data): - match = re.compile(regex).match(__data.lower().strip()) - if match: - index = i - new_version = ' '.join( - match.group(0).upper().split()[:2] - ) + ' ' + fixed_version - break - - return (list_data[index].strip(), new_version, index) - - - def __get_pom_data(self, cache: bool = True, out: str = None) -> Optional[dict]: - """ - This function is used to retrieve configuration data from 'pom.xml'. - - Parameters: - - cache: bool (default = True) - Boolean that specifies whether to create a cache file - for the retrieved data or not. - - out: str (default = None) - A string that specifies file name of output cache. - If None it will be overwrited with 'config.json' - - Returns: - If `cache` is False, the function returns a dictionary - containing the configuration data. Otherwise, returns None. - - Raises: - None - """ - def filter_data(bs_data: BeautifulSoup) -> dict: - def filter_group_id(data: dict) -> None: - for elm in bs_data.find_all('groupId'): - if elm.text.strip().split('.')[-1] not in data['author.name'].lower(): - continue - data['project.' + elm.name] = elm.text.strip() - if self.__verbose: - print(' ' * 4 + f'<{elm.name}>', elm.text.strip(), f'') - - # --------------------------------------------------- # - - def filter_artifact_id(data: dict) -> None: - for elm in bs_data.find_all('artifactId'): - if not elm.text.strip().split('-')[0] == data['package.name'].lower(): - continue - data['project.' + elm.name] = elm.text.strip() - if self.__verbose: - print(' ' * 4 + f'<{elm.name}>', elm.text.strip(), f'') - - # --------------------------------------------------- # - - data: dict = { } - - if self.__verbose: - Utils.info_msg('Filtering contents...') - print(os.linesep + '>> ' + '-' * 50 + ' <<') - print(os.linesep + '') - - for element in bs_data.find_all(): - for child in element.find_all(): - if child.name != 'properties': - continue - for child_1 in child.find_all(): - if child_1.name.startswith('project'): - continue - data[child_1.name] = child_1.text.strip() - if self.__verbose: - print(' ' * 4 + f'<{child_1.name}>', - child_1.text.strip(), f'' - ) - - filter_group_id(data) - filter_artifact_id(data) - - if self.__verbose: - print('' + os.linesep) - print('>> ' + '-' * 50 + ' <<' + os.linesep) - Utils.info_msg('All contents filtered.') - - return data - - # ------------------------------------------------------- # - - if out is None or out == '': - out = 'config.json' - - if self.__verbose: - Utils.pr_banner('GET CONFIG DATA', newline=1) - - FileUtils.check_file('pom.xml', verbose=self.__verbose) - bs_data: BeautifulSoup = Utils.convert_to_bs( - 'pom.xml', - verbose=self.__verbose - ) - - config_data: dict = filter_data(bs_data) - - if cache: - self.__create_cache(config_data, indent=4, out=out) - config_data = None - - if self.__verbose: - Utils.pr_banner('(END) GET CONFIG DATA', newline=1) - - return config_data - - - def __fix_configuration(self, - data: dict, cached: str = None, target: str = None) -> None: - """ - Fix the configurations of specified target file. - - Parameters: - - data: dict - The dictionary contains new configuration data. - - - cached: str (default = None) - File path to the cached new configuration data. - - - target: str (default = None) - The target file that want to fix it's configurations. - - Returns: - None - - Raises: - - RuntimeError - If `data` is empty and path to cached - configuration also empty. - - - FileNotFoundError - If file path to cached configuration data does not exist. - """ - target_path: str = None - target_data: Union[list, dict] = None - - try: - if not data and not cached: - raise RuntimeError( - 'Given config data is empty, please specify path to cached config' - ) - if target.lower() is None or \ - not target in [ - target_file.lower() - for target_file in self.__TARGET_FILES - ]: - raise RuntimeError('Please specify the target file') - - if target.lower() in [ - target_file.lower() - for target_file in self.__TARGET_FILES - ]: - # config.xml - if target.lower() == self.__TARGET_FILES[0]: - target_path = self.__RESOURCES_PATH + \ - f'configuration{os.sep}' + self.__TARGET_FILES[0] - - # Makefile - elif target.lower() == self.__TARGET_FILES[1].lower(): - target_path = self.__TARGET_FILES[1] - - # MANIFEST.MF - elif target.lower() == self.__TARGET_FILES[2].lower(): - target_path = f'META-INF{os.sep}' + self.__TARGET_FILES[2] - - except RuntimeError as run_err: - Utils.raise_error(run_err, -1, file=__file__) - - if self.__verbose: - Utils.pr_banner('FIX CONFIGURATION') - print(f'>>>>> FILE: "{target_path}"') - - if target.lower() != self.__TARGET_FILES[1].lower(): - FileUtils.check_directory( - self.__get_output_path(target), verbose=self.__verbose - ) - - data = self.__get_cache(cached) if not data else data - - # Check the target file - # [config.xml] - if target.lower() == self.__TARGET_FILES[0]: - bs_data = repr(Utils.convert_to_bs(target_path, verbose=self.__verbose)) - - target_data: dict = { } - target_value: list = re.compile(r"\$\{([\w.-]+)\}").findall(bs_data) - - for i, _ in enumerate(target_value): - target_data[target_value[i]] = data[target_value[i]] - - for key, val in target_data.items(): - bs_data = re.sub(fr'\${{{key}}}', val, bs_data) - - bs_data = BeautifulSoup(bs_data, 'xml').prettify( - formatter=XMLFormatter(indent=4) - ).split(os.linesep) - bs_data[0] += os.linesep - - fixed_data: list = [ ] - i: int = 0 - for idx, element in enumerate(bs_data): - if element.startswith((' None: - """ - Main program. - """ - opts_arg: tuple = ('-v', '--verbose', 'verbose',) - - # Checking the CLI arguments - try: - if len(sys.argv) > 2: - raise RuntimeError('Too many arguments, need (1) argument') - if len(sys.argv) == 2 and sys.argv[1] not in opts_arg: - raise ValueError(f'Unknown options for value: "{sys.argv[1]}"') - except RuntimeError as runtime_err: - Utils.raise_error(runtime_err, -1, file=__file__) - except ValueError as val_err: - Utils.raise_error(val_err, 1, file=__file__) - - - # Run the FixConfig program - if len(sys.argv) == 2 and sys.argv[1] in opts_arg: - FixConfig(verbose=True).run() - else: - FixConfig().run() - - - -## === DRIVER === ## -if __name__ == '__main__': - main() diff --git a/src/main/python/generate_list.py b/src/main/python/generate_list.py deleted file mode 100644 index 70da3772..00000000 --- a/src/main/python/generate_list.py +++ /dev/null @@ -1,287 +0,0 @@ -""" -Program that generates the list of source files or class files. -""" -__author__ = 'Ryuu Mitsuki' -__all__ = ['GenerateList'] - -import os -import sys -from utils import Utils, FileUtils - -class GenerateList: - """ - This class provides methods that generates list of source files and class files. - Currently only works on Linux/Unix system, Windows user need to install MinGW and Git Bash. - - Working directory: - : 'src/main/java/' - For searching all of source files (*.java). - : 'target/classes/' - For searching all of class files (*.class). - : 'target/generated-list/' - The output directory for generated list. - """ - __PATH: dict = { - ('target_path'): ('/').join(['src', 'main', 'java']), - ('class_path'): ('/').join(['target', 'classes']) + '/', - ('out_path'): ('/').join(['target', 'generated-list']) + '/', - ('source'): ('/').join(['target', 'generated-list', 'sourceFiles.lst']), - ('output'): ('/').join(['target', 'generated-list', 'outputFiles.lst']) - } - - __verbose: bool = False - __command: dict = { - 'unix': { - 'source': - f'find {__PATH["target_path"]} -type f -name "*.java" > {__PATH["source"]}', - - 'output': - f'find {__PATH["class_path"]} -type f -name "*.class" > {__PATH["output"]}' - }, - - 'windows': { - 'source': - f'dir "{__PATH["target_path"]}" /b /s *.java > {__PATH["source"]}', - - 'output': - f'dir "{__PATH["class_path"]}" /b /s *.class > {__PATH["output"]}' - } - } - - - def __init__(self, verbose: bool = False) -> None: - """ - This constructor will construct new object of `GenerateList`. - It does nothing, only checks and creates some working directories. - - Parameters: - - verbose: bool (default = False) - Boolean value that specifies whether to print verbose output. - - Returns: - None - - Raises: - None - """ - self.__verbose: bool = verbose - - # Checking 'src/main/java' directory - FileUtils.check_directory( - self.__PATH['target_path'], - verbose=self.__verbose - ) - - # Checking 'target/generated-list' directory - FileUtils.check_directory( - self.__PATH['out_path'], - verbose=self.__verbose - ) - - - def run(self, generate: str = 'source_list') -> None: - """ - Runs and generates the list by specifying the type of list - that want to be generated. - - Parameters: - - generate: str (default = 'source_list') - String value that specifies which one list - that want to be generated. - - Accepted values: - - 'source_list' or 'source' - - 'output_list' or 'output' - - Returns: - None - - Raises: - - ValueError - If the value of `generate` parameter is invalid. - """ - if generate in ('source', 'source_list'): - self.__generate_sources_list() - elif generate in ('output', 'output_list'): - self.__generate_output_list() - else: - try: - msg = f'Unknown list type: "{generate}"' - raise ValueError(msg) - except ValueError as val_err: - Utils.raise_error(val_err, -1, file=__file__) - - - def __generate_sources_list(self) -> None: - """ - This method generates a list of source files from `src/main/java` directory. - The generated list is sorted and written back to `sourceFiles.lst` file. - - Parameters: - None - - Returns: - None - - Raises: - None - """ - command = self.__command['unix']['source'] - - if self.__verbose: - Utils.pr_banner('GENERATING LIST') - Utils.info_msg('Generating list...') - - err_code: int = os.system(command) - - # Return if an error occured - if err_code != 0: - sys.exit(err_code) - - if self.__verbose: - Utils.info_msg( - f'List generated, saved in "{self.__PATH["source"]}".' - ) - Utils.pr_banner('SORT THE SOURCE LIST') - Utils.info_msg( - f'Sorting "{self.__PATH["source"].rsplit(os.sep, maxsplit=1)[-1]}"...' - ) - - source_lst: list = FileUtils.read_file( - self.__PATH['source'], verbose=self.__verbose - ) - - for idx, _ in enumerate(source_lst): - source_lst[idx] = source_lst[idx].strip() - source_lst.sort() - - if self.__verbose: - Utils.info_msg('All list sorted.') - - FileUtils.write_to_file( - self.__PATH['source'], contents=source_lst, verbose=self.__verbose - ) - - - def __generate_output_list(self) -> None: - """ - This method generates a list of class files from `target/classes/` directory. - The generated list is sorted and written back to `outputFiles.lst` file. - - Parameters: - None - - Returns: - None - - Raises: - None - """ - cmd = self.__command['unix']['output'] - - if self.__verbose: - Utils.pr_banner('GENERATING LIST') - Utils.info_msg('Generating list...') - - err_code: int = os.system(cmd) - - # Return if an error occured - if err_code != 0: - sys.exit(err_code) - - if self.__verbose: - Utils.info_msg( - f'List generated, saved in "{self.__PATH["output"]}".' - ) - Utils.pr_banner('SORT THE OUTPUT LIST') - Utils.info_msg( - f'Sorting "{self.__PATH["output"].rsplit(os.sep, maxsplit=1)[-1]}"...' - ) - - output_lst: list = FileUtils.read_file( - self.__PATH['output'], verbose=self.__verbose - ) - - new_output_lst: list = [ ] - - for output_file in output_lst: - for out in output_file.split(): - new_output_lst.append(os.path.join(*(out.split('/')[2:]))) - new_output_lst.sort() - - if self.__verbose: - Utils.info_msg('All list sorted.') - - FileUtils.write_to_file( - self.__PATH['output'], contents=new_output_lst, verbose=self.__verbose - ) - - - __all__ = ['run'] - - - -def main() -> None: - """ - Main program. - """ - def check_opt(opt: str, *args) -> bool: - result: bool = False - - if len(args) == 1 and args[0] == 1: - result = any(opt in option for option in opts_arg[args[0]]) - elif len(args) == 1 and args[0] == 0: - result = opt in opts_arg[0] - elif len(args) == 2 and args[0] == 1: - result = opt in opts_arg[args[0]][args[1]] - - return result - - # -------------------------------------------------------- # - - opts_arg: list = [ - ('-v', '--verbose', 'verbose'), - [ - ('src', 'source'), - ('cls', 'class') - ] - ] - - # Checking the CLI arguments - try: - if len(sys.argv) > 3: - raise RuntimeError('Too many arguments, need (1 or 2) arguments') - if len(sys.argv) == 2 and not check_opt(sys.argv[1], 1): - raise ValueError(f'Unknown options value: "{sys.argv[1]}"') - if len(sys.argv) == 3 and not (check_opt(sys.argv[1], 1) or check_opt(sys.argv[2], 0)): - raise ValueError(f'Unknown options value: "{sys.argv[1]}" + "{sys.argv[2]}"') - except ValueError as value_err: - Utils.raise_error(value_err, 1, file=__file__) - except RuntimeError as run_err: - Utils.raise_error(run_err, -1, file=__file__) - - # Run the GenerateList program - if len(sys.argv) == 2 and \ - (check_opt(sys.argv[1], 1) and check_opt(sys.argv[1], 1, 0)): - GenerateList().run(generate='source_list') - elif len(sys.argv) == 2 and \ - (check_opt(sys.argv[1], 1) and check_opt(sys.argv[1], 1, 1)): - GenerateList().run(generate='output_list') - elif len(sys.argv) == 3 and (check_opt(sys.argv[1], 1) and check_opt(sys.argv[2], 0)): - if check_opt(sys.argv[1], 1, 0): - GenerateList(verbose=True).run(generate='source_list') - elif check_opt(sys.argv[1], 1, 1): - GenerateList(verbose=True).run(generate='output_list') - else: - try: - raise ValueError(f'Unknown options value: "{sys.argv[1]}" "{sys.argv[2]}"') - except ValueError as value_err: - Utils.raise_error(value_err, 1, file=__file__) - - -## === DRIVER === ## -if __name__ == '__main__': - if len(sys.argv) < 2: - Utils.raise_error( - RuntimeError('Arguments cannot be empty'), - -1, file=__file__ - ) - - main() diff --git a/src/main/python/utils.py b/src/main/python/utils.py deleted file mode 100644 index c137f5ec..00000000 --- a/src/main/python/utils.py +++ /dev/null @@ -1,457 +0,0 @@ -""" -Utilities for JMatrix's Python programs -""" -__author__ = 'Ryuu Mitsuki' -__all__ = ['Utils', 'FileUtils'] - -import os -import sys -import json -from typing import Optional, Union -from traceback import extract_stack - -try: - from bs4 import BeautifulSoup -except ModuleNotFoundError as bs_not_found: - print('[jmatrix] Please install all the requirements first.') - raise bs_not_found - - -class Utils: - """ - Other utilities for all Python programs that supports `JMatrix`. - - All methods from this class is static. - """ - @staticmethod - def raise_error(ex: Exception, status: int = 1, file: str = None) -> None: - """ - This function is used to print the error message, traceback, - and exit the program with a given error code. - - Parameters: - - ex: Exception - The exception object to be printed. - - - status: int (default = 1) - The exit status code of the program. - - - file: str (default = None) - The file name where the error occurred. - - Returns: - None - - Raises: - None - """ - if file is not None and len(file.split(os.sep)) != 1: - file = file.split(os.sep)[-1] - - sys.stderr.write( - os.linesep + \ - fr'/!\ ERROR{os.linesep}{">"*9}{os.linesep}' - ) - - if file is not None: - sys.stderr.write(f'[jmatrix] An error occured in "{file}".{os.linesep}') - else: - sys.stderr.write(f'[jmatrix] An error occured.{os.linesep}') - - sys.stderr.write(f"{'>'*9} {ex} {'<'*9}{os.linesep*2}") - - tb_stack = extract_stack() - tb_stack.pop(-1) - - sys.stderr.write(fr'/!\ TRACEBACK{os.linesep}{">"*13}{os.linesep}') - for frame in tb_stack: - sys.stderr.write( - f'File "...{os.sep}' + \ - f'{(os.sep).join(frame.filename.split(os.sep)[-4:])}"{os.linesep}' - ) - sys.stderr.write( - '>' * 4 + ' ' * 6 + \ - f'at "{frame.name}", line {frame.lineno}' + \ - f'{os.linesep * 2 if tb_stack.index(frame) != len(tb_stack) - 1 else os.linesep}' - ) - - if tb_stack.index(frame) == len(tb_stack) - 1: - sys.stderr.write(f'{type(ex).__name__}: {ex}{os.linesep}') - - sys.stderr.write(f'{os.linesep}Exited with error code: {status}{os.linesep}') - - if status != 0: - sys.exit(status) - - - @staticmethod - def info_msg(message: str = None, prefix: str = 'jmatrix') -> None: - """ - Print the message to standard output (stdout) - with specified prefix. - - Parameters: - - message: str (default = None) - A string that want to be printed. - - - prefix: str (default = 'jmatrix') - A string that specifies prefix. - - Returns: - None - - Raises: - None - """ - print(f'[{prefix}] {message}') - - - @staticmethod - def convert_to_bs(filepath: str, _type: str = 'xml', verbose: bool = False) -> BeautifulSoup: - """ - Takes a file path as input and returns the contents of - the file in BeautifulSoup object form. - - Parameters: - - filepath: str - The file path of the file to be parsed. - - - _type: str (default = 'xml') - The type of parser to be used. - - - verbose: bool (default = False) - A boolean value that specifies whether - to print verbose output. - - Returns: - A `BeautifulSoup` object containing the contents of the file. - - Raises: - None - """ - contents: str = FileUtils.read_file(filepath, _list=False, verbose=verbose) - - if verbose: - Utils.info_msg('Contents converted to \'BeautifulSoup\' object.') - - return BeautifulSoup(contents, _type) - - - @staticmethod - def pr_banner(title: str, **kwargs) -> None: - """ - Create and print the banner to standard output (stdout). - - Parameters: - - title: str - String to specifies the banner title. - - Returns: - None - - Raises: - None - """ - print(os.linesep * kwargs.get('newline', 2) + \ - '-' * kwargs.get('lines', 80)) - print(f'>>> [ {title} ] <<< '.center(kwargs.get('center', 78))) - print('-' * kwargs.get('lines', 80)) - - - # List all of public methods, it can be imported all with wildcard '*' - __all__ = ['check_directory', 'check_file', 'read_file', 'write_to_file', 'pr_banner'] - - - -class FileUtils: - """ - File utilities for all Python programs that supports `JMatrix`. - - All methods from this class is static. - """ - @staticmethod - def check_directory(dirpath: str, verbose: bool = False) -> None: - """ - Check if a directory exists and create it if it does not exists. - - Parameters: - - dirpath: str - Directory path to be checked. - - - verbose: bool - Whether to print detailed messages while checking or creating the directory. - - Returns: - None - - Raises: - None - """ - if verbose: - Utils.pr_banner( - 'CHECK DIRECTORY', lines=40, center=39, newline=1 - ) - Utils.info_msg(f'Checking directory "{dirpath}"...') - if not os.path.exists(dirpath): - if verbose: - Utils.info_msg('Given directory path does not exist.') - Utils.info_msg(f'Creating new directory (\'{dirpath}\')...') - os.makedirs(dirpath) - if verbose: - Utils.info_msg(f'Successfully create "{dirpath}" directory.') - else: - if verbose: - Utils.info_msg(f'"{dirpath}" directory is already exist.') - if verbose: - Utils.pr_banner( - '(END) CHECK DIRECTORY', - lines=40, center=39, newline=0 - ) - print() - - - @staticmethod - def check_file(filepath: str, verbose: bool = False) -> None: - """ - Check if a file exists, if file does not exists it will raises an error. - - Parameters: - - filepath: str - The file path to be checked. - - - verbose: bool (default = False) - If True, print verbose output. - - Returns: - None - - Raises: - - FileNotFoundError - If the file does not exist or cannot be accessed. - """ - if verbose: - Utils.pr_banner( - 'CHECK FILE', lines=40, center=39, newline=1 - ) - Utils.info_msg(f'Check existence for file: "{filepath}"...') - - if os.path.exists(filepath): - if verbose: - Utils.info_msg(f'File "{filepath}" is exist.') - return - - Utils.info_msg(f'File "{filepath}" does not exist, error raised!' + os.linesep) - try: - msg = f'File "{filepath}" does not exist or cannot be accessed' - raise FileNotFoundError(msg) - except FileNotFoundError as not_found_err: - Utils.raise_error(not_found_err, 2, file=__file__) - - if verbose: - Utils.pr_banner( - '(END) CHECK FILE', - lines=40, center=39, newline=0 - ) - print() - - - @staticmethod - def read_file(filepath: str, _list: bool = True, verbose: bool = False) -> Optional[list]: - """ - Read all contents from specified file, - and specify the return to list or str. - - Parameters: - - filepath: str - The file path to read the contents. - - - _list: bool (default = True) - If True it will returns the contents with type of list, - otherwise it will returns the contents with type of str. - - - verbose: bool (default = False) - Whether to print detailed messages while - getting the file contents. - - Returns: - None - - Raises: - - ValueError - If the file path is empty. - - - FileNotFoundError - If file path to read it's contents does not exist. - - - PermissionError - If the directory path of file is restricted and cannot - be accessed by program. - """ - contents: Union[list, str] = None - - if verbose: - Utils.pr_banner( - 'GET FILE CONTENTS', lines=40, center=39, newline=1 - ) - Utils.info_msg(f'Retrieving all contents from "{filepath}"...') - - try: - if filepath is None: - msg = 'File path cannot be empty' - raise ValueError(msg) - if not os.path.exists(filepath): - msg = f'File "{filepath}" does not exist or cannot be accessed' - raise FileNotFoundError(msg) - - with open(filepath, 'r', encoding='utf-8') as file: - if _list: - contents = file.readlines() - else: - contents = file.read() - except ValueError as empty_path: - Utils.raise_error(empty_path, 1, file=__file__) - except FileNotFoundError as file_not_found: - Utils.raise_error(file_not_found, 2, file=__file__) - except PermissionError as perm_err: - Utils.raise_error(perm_err, -1, file=__file__) - - if verbose: - Utils.info_msg('Successfully retrieve all contents.') - Utils.pr_banner( - '(END) GET FILE CONTENTS', - lines=40, center=39, newline=0 - ) - print() - - return contents - - - @staticmethod - def write_to_file(filepath: str, - contents: Union[Union[list, dict], str] = None, - verbose: bool = False, **kwargs) -> None: - """ - Write the contents to specified file. - If file has a contents inside, this method won't overwrites it - instead creates with a new contents. - - Parameters: - - filepath: str - The file path to write the contents. - - - contents: Union[Union[list, dict], str] (default = None) - The contents to write to the file. - Supported type of contents: - - `list` (recommended) - - `str` - - `dict` - - - verbose: bool (default = False) - Whether to print detailed messages while - writing to the file. - - Returns: - None - - Raises: - - ValueError - If the file path or contents is empty. - - - TypeError - If the contents type is not valid or unsupported. - - - IsADirectoryError - If given file path is exists but a directory. - - - PermissionError - If the directory path is restricted and cannot be - accessed by program. - """ - def check_arguments(**kwargs) -> None: - contents = kwargs.get('contents', None) - filepath = kwargs.get('filepath', None) - - try: - if not filepath: - msg = 'File path cannot be empty' - raise ValueError(msg) - if not contents: - msg = 'Contents cannot be empty' - raise ValueError(msg) - if os.path.exists(filepath) and os.path.isdir(filepath): - msg = 'Given file path is a directory' - raise IsADirectoryError(msg) - if not isinstance(contents, (list, dict, str)): - msg = f'Unsupported contents type: "{type(contents)}"' - raise TypeError(msg) - - except (ValueError, TypeError) as val_type_err: - Utils.raise_error(val_type_err, 1, file=__file__) - except IsADirectoryError as dir_err: - Utils.raise_error(dir_err, 2, file=__file__) - - # -------------------------------------------------- # - - def pr_msg(message: str, *args) -> None: - if not verbose: - return - if len(args) == 0 or args is None: - Utils.info_msg(message) - elif len(args) != 0 and isinstance(args[0], dict): - for key, val in args[0].items(): - message.format(key=key, val=val) - Utils.info_msg(message) - - # -------------------------------------------------- # - - check_arguments( - filepath=filepath, contents=contents - ) - - if kwargs.get('newline', True): - newline = os.linesep - else: - newline = '' - - if verbose: - Utils.pr_banner( - 'WRITE FILE', lines=40, center=39, newline=1 - ) - Utils.info_msg( - f'Writing contents id:<{id(contents)}> to "{filepath}"...' - ) - - try: - with open(filepath, 'w', encoding='utf-8') as file: - if isinstance(contents, list): - for content in contents: - file.write(content + newline) - pr_msg( - f'Writing "{content.strip()}" -> \'{filepath}\'...' - ) - elif isinstance(contents, str): - pr_msg(f'Writing "{contents}" -> \'{filepath}\'...') - file.write(contents + newline) - elif isinstance(contents, dict): - file.write(json.dumps(contents, indent=4) + newline) - pr_msg( - 'Writing ("{key}", "{val}") ->' + f'\'{filepath}\'...', - contents - ) - - except PermissionError as perm_err: - Utils.raise_error(perm_err, -1, file=__file__) - - if verbose: - print() - Utils.info_msg( - f'Successfully write the contents to file: "{filepath}".' - ) - Utils.pr_banner( - '(END) WRITE FILE', lines=40, center=39, newline=0 - ) - print() - - - # List all of public methods, it can be imported all with wildcard '*' - __all__ = ['raise_error', 'info_msg', 'convert_to_bs'] diff --git a/tools/JMBuilder/LICENSE b/tools/JMBuilder/LICENSE new file mode 100644 index 00000000..32a072fb --- /dev/null +++ b/tools/JMBuilder/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023-2024 Ryuu Mitsuki + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tools/JMBuilder/README.md b/tools/JMBuilder/README.md new file mode 100644 index 00000000..ff12552b --- /dev/null +++ b/tools/JMBuilder/README.md @@ -0,0 +1,12 @@ +# JMBuilder + +**JMBuilder** or **JMatrix Builder**, is a builder that is designed to build and configure the +**JMatrix** library written in [Python](python) by [Ryuu Mitsuki](mitsuki31). + +For more details about **JMatrix**, you can check out its repository. + + + + +[python]: https://python.org +[mitsuki31]: https://github.com/mitsuki31 diff --git a/tools/JMBuilder/jmbuilder/.config/setup.json b/tools/JMBuilder/jmbuilder/.config/setup.json new file mode 100644 index 00000000..c1129608 --- /dev/null +++ b/tools/JMBuilder/jmbuilder/.config/setup.json @@ -0,0 +1,6 @@ +{ + "Program-Name": "JMBuilder", + "Version": [1, 0, 0], + "Author": "Ryuu Mitsuki", + "License": "MIT License" +} diff --git a/tools/JMBuilder/jmbuilder/__init__.py b/tools/JMBuilder/jmbuilder/__init__.py new file mode 100644 index 00000000..cf1650e2 --- /dev/null +++ b/tools/JMBuilder/jmbuilder/__init__.py @@ -0,0 +1,118 @@ +"""JMatrix Builder written in Python + +JMBuilder, provides some utilities and configurations to build the `JMatrix` +library, the module are written in Python. Created by Ryuu Mitsuki. +For more information about `JMatrix` you can refer to link below. + + https://github.com/mitsuki31/jmatrix.git + +Copyright (c) 2023-2024 Ryuu Mitsuki + + +Available Classes +----------------- +JMProperties + A custom properties parser class, implemented in `jmbuilder.utils` package. + Python does not provide an easy way to parsing files with extension of + `.properties`, this class is designed to parse properties file and + access their contents with ease without any third-party modules. + +JMSetupConfRetriever + A class that provides all configurations for setting up the `JMBuilder` module. + +JMException + This class is a base exception for `JMBuilder` module. + +JMParserError + This exception is raised when an error has occurred during parsing the + configuration (e.g., properties file). + +JMUnknownTypeError + This exception is raised when an unknown type error occurs during + the execution in this module. + +JMRepairer + A class for repairing manifest and properties files using information + from a POM file. + +PomParser + A class that provides an easy way to parse and retrieve useful + information from the provided POM file. + +Available Functions +------------------- +json_parser + Provide a simple way to parse the specified JSON file. + +remove_comments + Remove lines within the given list of strings that starting with the + specified delimiter, and returning a new list of strings with the lines + starting with the specified delimiter removed. + +remove_blanks + Remove blank or empty lines (including line with trailing whitespace) + and `None` within the given list of strings, and returning a new list + of strings with empty lines and `None` removed. + + +Available Constants +------------------- +BASEDIR + Provides a string that represents the path to base directory + of `JMBuilder` module. + +LOGSDIR + Provides a string that represents the path to logs directory + that being used by this package to log some information and errors. + +STDOUT + References the console standard output (`sys.stdout`). + +STDERR + References the console standard error (`sys.stderr`). + +TMPDIR + Provides a string that represents the path to temporary directory + that being used by this package to store some temporary file(s) or cache(s). + +""" + +# exception +from . import exception +from .exception import * + +# utils +from . import utils +from .utils import * + +# core +from . import core +from .core import * + +# _globals +from . import _globals +from ._globals import * +from ._globals import AUTHOR, VERSION, VERSION_INFO + + +__all__ = [] +__all__.extend(_globals.__all__) + +# We all know it is a bad practice to use wildcard imports, right? +# But people are still using it for specific reasons, and knew the risks. +# Import statement here are no longer being used to prevent the accumulation +# of names after using wildcard imports. Instead, it exports only a few related modules +# without directly exporting the global classes and functions within those modules. +__all__.extend(['exception', 'utils', 'core']) + +#__all__.extend(exception.__all__) +#__all__.extend(utils.__all__) + + +__author__ = AUTHOR +__version__ = VERSION +__version_info__ = VERSION_INFO + + +# Delete no longer used imported objects +del AUTHOR, VERSION, VERSION_INFO diff --git a/tools/JMBuilder/jmbuilder/__main__.py b/tools/JMBuilder/jmbuilder/__main__.py new file mode 100644 index 00000000..696aace7 --- /dev/null +++ b/tools/JMBuilder/jmbuilder/__main__.py @@ -0,0 +1,333 @@ +"""Main Module for JMBuilder + +Copyright (c) 2023-2024 Ryuu Mitsuki. + +""" + +import os as __os +import sys as __sys +import re as __re +from pathlib import Path as __Path +from typing import ( + Any, + Iterable, + Union, + Set, + List, + Dict, + Tuple, + TextIO +) + + +try: + from . import utils as __jmutils + from . import exception as __jmexc + from ._globals import AUTHOR, VERSION, VERSION_INFO, __jmsetup__ + from .core import PomParser as __core_PomParser, JMRepairer as __core_JMRepairer +except (ImportError, ModuleNotFoundError, ValueError): + # Add a new Python search path to the first index + __sys.path.insert(0, str(__Path(__sys.path[0]).parent)) + + from . import utils as __jmutils + from jmbuilder._globals import AUTHOR, VERSION, VERSION_INFO, __jmsetup__ + from jmbuilder.core import PomParser as __core_PomParser, JMRepairer as __core_JMRepairer +finally: + del __Path # This no longer being used + +CLEAN_ARGS: Tuple[str] = __jmutils.remove_duplicates(__sys.argv[1:]) + +def __print_version(_exit: bool = False, *, + only_ver: bool = False, + file: TextIO = __sys.stdout) -> None: + """ + Print the version info to specific opened file. + + Parameters + ---------- + exit : bool, optional + Whether to exit and terminate the Python after printed the version. + Defaults to False (disabled). + + only_ver: bool, optional + Whether to print the version only. By activating this option, + other information like program name, license, and copyright + will not be printed. Defaults to False. + + file : TextIO, optional + The file to print the version info. + Defaults to console standard output (`sys.stdout`). + + """ + + if not file: + raise ValueError(f"File must be a file object, got {type(file).__name__!r}") + + program_name: str = __jmsetup__.progname + version: str = f"v{'.'.join(map(str, __jmsetup__.version))}" + author: str = __jmsetup__.author + + # Check if only_ver is False (or not specified) + if not only_ver: + print( + program_name, version, f'- {__jmsetup__.license}', # Program name and version + __os.linesep + \ + f'Copyright (C) 2023-2024 by {author}.', # Copyright notice + file=file + ) + else: + print(version, file=file) + + if _exit: + __sys.exit(0) + + +def __argchck(targets: Union[str, Iterable], args: Union[List[str], Tuple[str]]) -> bool: + """ + Check whether specified argument are presented in `args`. + + Paramaters + ---------- + targets : str or iterable + An argument or a list of arguments (must iterable) to searched for. + + args : list or tuple of str + A list of arguments. + + Returns + ------- + bool : + Returns True if the specified argument are presented in `args`, + otherwise returns False. + + """ + if isinstance(targets, str): + return targets in args + + found: bool = False + for target in targets: + if str(target) in args: + found = True + + return found + + +def __find_arg(val: Union[str, __re.Pattern]) -> int: + """ + Find the index of specified argument from the command-line arguments. + + Parameters + ---------- + val : str or re.Pattern + A regular expression pattern used to search for the argument within + the command-line arguments. Accepts a string literal representing + the regular expression or a compiled regular expression. + + Returns + ------- + int : + The index of the specified argument in the command-line arguments. + Returns -1 if the argument cannot be found or if the command-line + arguments are empty. + + Notes + ----- + This function utilizes the global constant ``CLEAN_ARGS``, ensuring that + it searches for the desired argument within the command-line arguments + with all duplicate arguments omitted. + + """ + # Use the fixed arguments; global constant + if len(CLEAN_ARGS) == 0: + return -1 + + # Convert to regular expression + val = __re.compile(val) if isinstance(val, str) else val + + res: __re.Match = None + for arg in CLEAN_ARGS: + res = val.search(arg) + if res: + break + + return CLEAN_ARGS.index(res.group()) if res else -1 + + +def __print_help() -> None: + """Print the help message to the standard output.""" + + program_name: str = __jmsetup__.progname + version: str = f"v{'.'.join(map(str, __jmsetup__.version))}" + author: str = __jmsetup__.author + + header: str = f'{program_name} {version}' + + print(f"""\ +{header} +{''.join(['-' for _ in range(len(header))])} + +USAGE: + python -m {__package__} [OPTIONS] + +OPTIONS: + --fix-mf [out], + --fix-manifest [out] + Run the builder to correct the specified manifest file containing + Maven's variables. Utilizes information from the provided POM file. + The output will be written to the given output file, if provided; + otherwise, it will overwrite the input file. + + --fix-prop [out], + --fix-properties [out] + Run the builder to rectify the specified properties file with + Maven's variables. Incorporates information from the provided POM file. + The output will be written to the given output file, if provided; + otherwise, it will overwrite the input file. + + -V, --version, -version + Print the version and copyright information. All details will be printed + directly to the standard output, except for '-version', it goes + to the standard error. + + -VV, --only-ver, --only-version + Print the version number only. + + -h, --help + Print this help message. + +ISSUES: + Report some issues and help us improve this builder. + + +AUTHOR: + {author}\ +""") + + +#::# Main Driver #::# +def main() -> None: + """Main function for JMBuilder.""" + help_args: Tuple[str] = ('-h', '--help') + version_args: Tuple[str] = ('-V', '--version', '-version') + only_version_args: Tuple[str] = ('-VV', '--only-ver', '--only-version') + fix_mf_args: Tuple[str] = ('--fix-manifest', '--fix-mf') + fix_prop_args: Tuple[str] = ('--fix-properties', '--fix-prop') + all_known_args: Set[str] = { + *help_args, *version_args, *only_version_args, + *fix_mf_args, *fix_prop_args + } + + if len(CLEAN_ARGS) == 0: + print( + f'Nothing to run.{__os.linesep * 2}' + + f'USAGE: python -m {__package__} [-h | -V | -VV]{__os.linesep}' + + '\t\t[--fix-mf [out] | --fix-prop [out]]' + ) + __sys.exit(0) + + # Check for `-V` or `--version` in the arguments + # If found, print the version info then exit with exit code zero (success) + # + if __argchck(version_args[:-1], CLEAN_ARGS): + __print_version(True) + + # For `-version` argument, the output will be redirected + # to the standard error (`sys.stderr`) + # `-V`, `--version` -> sys.stdout + # `-version` -> sys.stderr + # + elif __argchck(version_args[-1], CLEAN_ARGS): + __print_version(True, file=__sys.stderr) + + # To print the version only, user can use several arguments. See 'only_version_args' + elif __argchck(only_version_args, CLEAN_ARGS): + __print_version(True, only_ver=True) + + # Print the help message + elif __argchck(help_args, CLEAN_ARGS): + __print_help() + + # Only executed if and only if both options not specified simultaneously + elif __argchck(fix_mf_args, CLEAN_ARGS) ^ __argchck(fix_prop_args, CLEAN_ARGS): + fix_args = fix_mf_args if __argchck(fix_mf_args, CLEAN_ARGS) else fix_prop_args + opt_idx = __find_arg('(' + '|'.join(fix_args) + ')') \ + if __argchck(fix_args, CLEAN_ARGS) else -1 + del fix_args + + # Keep the unformatted brackets, with this we can format and + # write the actual error message on later. + option_err_msg: str = \ + '{}' + f'.{__os.linesep * 2}' + \ + f'USAGE: python -m {__package__} {CLEAN_ARGS[opt_idx]} [out]' + + # This dictionary will holds the builder arguments + option_args: Dict[str, Any] = { + 'pom': None, # 1 + 'infile': None, # 2 + 'outfile': None # 3 + } + # Get the builder method name + method_name: Any = 'fix_manifest' \ + if CLEAN_ARGS[opt_idx] in fix_mf_args else 'fix_properties' + + if not (len(CLEAN_ARGS) - 1) >= (opt_idx + 1): + raise __jmexc.JMException( + option_err_msg.format('No POM file were specified')) + if not (len(CLEAN_ARGS) - 1) >= (opt_idx + 2): + raise __jmexc.JMException( + option_err_msg.format('No input file were specified')) + + # Get the argument right after the option argument, and treat it + # as a path to specific POM file, otherwise an error raised if not specified. + option_args['pom'] = __core_PomParser.parse(CLEAN_ARGS[opt_idx + 1]) + + # Get the input file (infile) argument, + # otherwise raise an error if not specified + option_args['infile'] = CLEAN_ARGS[opt_idx + 2] + + # Get the outfile argument, otherwise use the infile argument + # if not specified (i.e., overwrite the input file) + if (len(CLEAN_ARGS) - 1) >= (opt_idx + 3): + option_args['outfile'] = CLEAN_ARGS[opt_idx + 3] + else: + option_args['outfile'] = option_args.get('infile') + + repairer: __core_JMRepairer = __core_JMRepairer(option_args.pop('pom')) + # Call the method dynamically + args_vals: Tuple[str] = (*option_args.values(),) + getattr(repairer, method_name)(args_vals[0], outfile=args_vals[1]) + + # This for causing the error when both options are specified + elif __argchck(fix_mf_args, CLEAN_ARGS) and __argchck(fix_prop_args, CLEAN_ARGS): + raise __jmexc.JMException( + 'Program was unable to run both options simultaneously') + elif not __argchck(CLEAN_ARGS, all_known_args): + err: str = "Unknown argument detected: '{}'" + (__os.linesep * 2) + \ + 'For more details, type argument `-h` or `--help`.' + + arg_idx: int = next((idx for idx, arg in enumerate(CLEAN_ARGS) + if not __argchck(arg, all_known_args)), -1) + + raise __jmexc.JMException(err.format(CLEAN_ARGS[arg_idx])) + + +__author__ = AUTHOR +__version__ = VERSION +__version_info__ = VERSION_INFO + + +# Delete unused imported objects +del AUTHOR, VERSION, VERSION_INFO +del Iterable, Union, Any, TextIO, List, Tuple, Dict, Set + + +if __name__ == '__main__': + main() +else: + # Remove the `main` function when this module is being imported, + # but it's a bit silly if there someone imports this module. + # The reason is the function implementation are strongly depends on + # the command-line arguments (`sys.argv`). However, if this is imported + # users can still modify the `sys.argv` manually, right? + # But we restricted that, we want the function to run as main driver only. + del main diff --git a/tools/JMBuilder/jmbuilder/_globals.py b/tools/JMBuilder/jmbuilder/_globals.py new file mode 100644 index 00000000..d0faf7fb --- /dev/null +++ b/tools/JMBuilder/jmbuilder/_globals.py @@ -0,0 +1,335 @@ +"""Global Module for `JMBuilder` + +This module contains all global variables, constants, and classes +used by the `JMBuilder` module. It provides access to various path +variables and other configurations used throughout the module. + +Copyright (c) 2023-2024 Ryuu Mitsuki. + + +Available Classes +----------------- +JMSetupConfRetriever + This class provide an easy way to retrieve and access the setup + configurations for `JMBuilder` module. + +Available Constants +------------------- +AUTHOR : str + The author name (as well as the creator) of the `JMBuilder` module. + +BASEDIR : str + Provides the path to the base directory of the `JMBuilder` package. + + This is an alias for `_JMCustomPath().basedir`. + +CONFDIR : str + Provides the path to the directory that contains configuration + files for configuring the `JMBuilder` module. The path itself + is relative to `BASEDIR`. + + This is an alias for `_JMCustomPath().confdir`. + +LOGSDIR : str + Provides the path to the directory used for logging by the + `JMBuilder` module. The path itself is relative to `BASEDIR`. + + This is an alias for `_JMCustomPath().logsdir`. + +STDOUT : TextIO + An alias for `sys.stdout`, representing the standard output console. + +STDERR : TextIO + An alias for `sys.stderr`, representing the standard error console. + +TMPDIR : str or pathlib.Path + Provides the path to the temporary directory used by the + `JMBuilder` module. The path itself is relative to `BASEDIR`. + + This is an alias for `_JMCustomPath().tmpdir`. + +VERSION : str + A string representing the JMBuilder's version information. + +""" + +import os as _os +import sys as _sys +import json as _json +import collections as _collections +from pathlib import Path as _Path +from typing import ( + TextIO, + Type, + Union +) + +if '_global_imported' in globals(): + raise RuntimeError( + "Cannot import the '_globals' module more than once.") +_global_imported: bool = True + +class _JMCustomPath: + """ + Custom class to manage read-only path variables for `JMBuilder` module. + + This class provides read-only properties for common path variables + such as `basedir`, `tmpdir`, `logsdir` and more. + + Parameters + ---------- + _type : Type[str] or Type[pathlib.Path], optional + The class type used for casting the path variables. Defaults to + Python built-in string class (`str`). + + This parameter only supported the following types: + - `str` + - `pathlib.Path` + + Raises + ------ + TypeError + If `_type` is not a class of `str` neither `pathlib.Path`. + + Notes + ----- + The path variables are read-only properties, and attempts to modify + them will raise an `AttributeError`. + + Examples + -------- + # Use `pathlib.Path` to cast the path + >>> _JMCustomPath(Path).tmpdir + PosixPath('.../tmp') # if Windows, it will be `WindowsPath` + + # `str` type is the default value + >>> _JMCustomPath().basedir + '/path/to/base/directory' + + # User can also check the class type that currently + # used for casting + >>> _JMCustomPath(Path).type + + + # Attempt to modify the value will cause an error + >>> _JMCustomPath().basedir = '/path/to/another/directory' + Traceback (most recent call last): + ... + AttributeError: can't set attribute + + """ + + def __init__(self, _type: Union[Type[str], Type[_Path]] = str): + """Initialize self. See ``help(type(self))``, for accurate signature.""" + self.__type = _type + err = 'Invalid type of `_type`: %r. ' + \ + 'Expected "str" and "pathlib.Path"' + + if not isinstance(_type, type): + err = TypeError(err % type(_type).__name__) + raise err + + if isinstance(_type, type) and \ + _type.__name__ not in ('str', 'Path', 'pathlib.Path'): + err = TypeError(err % _type.__name__) + raise err + + self.__basedir: str = self.__type(str(_Path(__file__).resolve().parent)) + self.__tmpdir: str = self.__type(_os.path.join(self.__basedir, 'tmp')) + self.__logsdir: str = self.__type(_os.path.join(self.__basedir, 'logs')) + self.__confdir: str = self.__type(_os.path.join(self.__basedir, '.config')) + + def __repr__(self) -> str: + """ + Return the string represents the class name and the class type. + + Returns + ------- + str + A string representation of this class. + """ + return f'{self.__class__.__name__}(type: {self.__type.__name__!r})' + + @property + def basedir(self) -> Union[str, _Path]: + """ + The JMBuilder's base directory path based on parent directory of this file. + + Returns + ------- + str or pathlib.Path + The current working directory path. + """ + return self.__basedir + + @property + def tmpdir(self) -> Union[str, _Path]: + """ + The path to 'tmp' directory relative to `basedir`. + + Returns + ------- + str or pathlib.Path + The path to temporary directory. + """ + return self.__tmpdir + + @property + def logsdir(self) -> Union[str, _Path]: + """ + The path to 'logs' directory relative to `basedir`. + + Returns + ------- + str or pathlib.Path + The path to logs directory. + """ + return self.__logsdir + + @property + def confdir(self) -> Union[str, _Path]: + """ + The path to '.config' directory relative to `basedir`. + + Returns + ------- + str or pathlib.Path + The path to the specified directory that contains configuration + files for configuring `JMBuilder` module. + """ + return self.__confdir + + @property + def type(self) -> Union[Type[str], Type[_Path]]: + """ + Return the current class type for casting the path. + + Returns + ------- + Type[str] or Type[pathlib.Path] + The class type. + """ + return self.__type + + +class JMSetupConfRetriever: + """ + A class that retrieves and provides all setup configuration. + + Notes + ----- + This class only retrieves the setup configuration without any modification + methods to their values. + + """ + + def __init__(self): + """Initialize self.""" + + setupfile: str = _os.path.join(_JMCustomPath(str).confdir, 'setup.json') + # Get the properties + configs: dict = {} + with open(setupfile, 'r', encoding='utf-8') as setup_file: + configs = _json.load(setup_file) + + # Create an empty named tuple + frozen_ver = _collections.namedtuple( + 'FrozenJMVersion', + ['major', 'minor', 'patch'], + module=__package__ + ) + + # Change the named tuple's documentation + frozen_ver.__doc__ = f'{configs.get("Program-Name")}\'s ' + \ + 'version information as a frozen named tuple.' + + key_names: list = ['progname', 'version', 'author', 'license'] + self.__jmsetup_data: dict = dict(zip(key_names, configs.values())) + + # Convert the version info to FrozenJMVersion (a named tuple) + self.__jmsetup_data['version'] = frozen_ver(*self.__jmsetup_data['version']) + + @property + def progname(self) -> str: + """ + Get the program name from setup configuration. + + Returns + ------- + str : + A string representing the program name. + + """ + return self.__jmsetup_data['progname'] + + @property + def version(self) -> 'FrozenJMVersion': + """ + Get the module version from setup configuration. + + Returns + ------- + FrozenJMVersion : + A frozen named tuple representing the module version. + + """ + return self.__jmsetup_data['version'] + + @property + def author(self) -> str: + """ + Get the author name from setup configuration. + + Returns + ------- + str : + A string representing the author name. + + """ + return self.__jmsetup_data['author'] + + @property + def license(self) -> str: + """ + Get the license name from setup configuration. + + Returns + ------- + str : + A string representing the license name. + + """ + return self.__jmsetup_data['license'] + + +# Aliases +__jmsetup__ = JMSetupConfRetriever() +__jmpath__ = _JMCustomPath() + +BASEDIR: Union[str, _Path] = __jmpath__.basedir +TMPDIR: Union[str, _Path] = __jmpath__.tmpdir +LOGSDIR: Union[str, _Path] = __jmpath__.logsdir +CONFDIR: Union[str, _Path] = __jmpath__.confdir + +STDOUT: TextIO = _sys.stdout +STDERR: TextIO = _sys.stderr + +AUTHOR: str = __jmsetup__.author +VERSION: str = '.'.join(map(str, __jmsetup__.version)) +VERSION_INFO: str = __jmsetup__.version + +__author__ = AUTHOR +__version__ = VERSION +__version_info__ = VERSION_INFO + + +__all__ = [ + 'BASEDIR', 'CONFDIR', + 'LOGSDIR', 'TMPDIR', + 'STDOUT', 'STDERR', + 'JMSetupConfRetriever' +] + + +# Remove unnecessary variables +del Type, TextIO, Union diff --git a/tools/JMBuilder/jmbuilder/core.py b/tools/JMBuilder/jmbuilder/core.py new file mode 100644 index 00000000..910ef32a --- /dev/null +++ b/tools/JMBuilder/jmbuilder/core.py @@ -0,0 +1,492 @@ +"""JMBuilder's core module. + +Copyright (c) 2023-2024 Ryuu Mitsuki. +""" + +import os as _os +import sys as _sys +import re as _re +from datetime import datetime as _dt, timezone as _tz +from typing import Dict, List, Optional, Union, TextIO +from warnings import warn as __warn +import bs4 as _bs4 + +from . import utils as _jmutils +from . import exception as _jmexc + +try: + from ._globals import AUTHOR, VERSION, VERSION_INFO +except (ImportError, ModuleNotFoundError, ValueError): + from pathlib import Path + + # Add a new Python search path to the first index + _sys.path.insert(0, str(Path(_sys.path[0]).parent)) + del Path + + from jmbuilder._globals import AUTHOR, VERSION, VERSION_INFO + +CORE_ERR: _jmexc.JMException = _jmexc.JMException( + _os.linesep + ' CORE ERROR: An error occurred in core module.') + +__all__ = ['PomParser', 'JMRepairer'] + +class PomParser: + """ + A class that provides an easy way to parse and retrieve useful + information from the provided POM file. + + Parameters + ---------- + soup : BeautifulSoup + A `bs4.BeautifulSoup` object representing the parsed POM file. + + """ + + def __init__(self, soup: _bs4.BeautifulSoup) -> 'PomParser': + """Create a new instance of ``PomParser`` class.""" + if not isinstance(soup, _bs4.BeautifulSoup): + # Raise an error + raise TypeError(f'Invalid instance class: {soup.__class__}') \ + from CORE_ERR + + self.soup: _bs4.BeautifulSoup = soup + self.project_tag: _bs4.element.Tag = soup.find('project') + + @staticmethod + def parse(pom_file: str, encoding: str = 'UTF-8') -> 'PomParser': + """ + Parse the POM file (``pom.xml``) and return an instance of + this class. Remove comments and blank lines to keep the POM clean. + + Parameters + ---------- + pom_file : str + The path of the pom.xml file to be parsed. + + encoding : str, optional + The encoding used while parsing the pom.xml file. Defaults to UTF-8. + + Returns + ------- + PomParser : + An instance of this class. + + """ + + try: + # Read and convert the pom.xml file to BeautifulSoup object + soup: _bs4.BeautifulSoup = _bs4.BeautifulSoup( + ''.join(_jmutils.readfile(pom_file, encoding=encoding)), 'xml') + + # Find the comments using lambda, then extract them + for element in soup(text=lambda t: isinstance(t, _bs4.Comment)): + element.extract() + except Exception as exc: + raise exc from CORE_ERR + + # Return the instance of this class + return PomParser(soup) + + def printsoup(self, *, pretty: bool = True, file: TextIO = _sys.stdout) -> None: + """ + Print the ``BeautifulSoup`` object, optionally prettified, for debugging purposes. + + Parameters + ---------- + pretty : bool, optional + If True, the BeautifulSoup object will be prettified for better readability. + Defaults to True. + + file : TextIO, optional + A file-like object to which the output will be printed. + Defaults to ``sys.stdout``. + + Notes + ----- + This method is intended for debugging and allows you to print the + current state of the ``BeautifulSoup`` object. The output can be customized + with the `pretty` parameter to control prettification and the `file` + parameter to redirect the output to a specific file-like object. + + Example + ------- + # Print the soup to the console standard output + >>> soup_instance.printsoup() + + # Print and save the soup to the specified file + >>> with open('output.xml', 'w') as f: + ... soup_instance.printsoup(pretty=True, file=f) + + """ + print(self.soup.prettify() if pretty else str(self.soup).strip(), file=file) + + def get(self, key: Union[str, List[str]]) -> Optional[_bs4.element.Tag]: + """ + Find the element tag based on the provided key, which can be a string + (separated by dots) or a list of tag names. The result could be a None, + this means that element are undefined or the users has specified wrong + element tree path. + + Parameters + ---------- + key : str or a list of str + The key representing the element tree path. + + Returns + ------- + Tag or None : + A ``bs4.element.Tag`` object representing the desired element tag, + or ``None`` if the element tag is undefined or cannot be found. + + """ + + # Split the key if the key is a string + keys: List[str] = key.split('.') if isinstance(key, str) else key + + # Find the element according to the first key + result: _bs4.element.Tag = self.soup.find(keys[0]) + for k in keys[1:]: + # Break the loop if the result is None + if not result: + break + result = result.find(k) + + return result + + def get_name(self) -> Optional[str]: + """Return the project name.""" + # => project.name + name_element: _bs4.element.Tag = self.project_tag.find('name') + return name_element.text if name_element else name_element + + def get_version(self) -> Optional[str]: + """Return the project version.""" + # => project.version + version_element: _bs4.element.Tag = self.project_tag.find('version') + return version_element.text if version_element else version_element + + def get_id(self) -> Dict[str, Optional[str]]: + """Return a dictionary with 'groupId' and 'artifactId'.""" + id_element: List[Optional[_bs4.element.Tag]] = [ + self.project_tag.find('groupId'), # => project.groupId + self.project_tag.find('artifactId') # => project.artifactId + ] + + return { # Return a dictionary + 'groupId': id_element[0].text if id_element[0] else id_element[0], + 'artifactId': id_element[1].text if id_element[1] else id_element[1] + } + + def get_url(self) -> Optional[str]: + """Return the project URL.""" + # => project.url + url_element: _bs4.element.Tag = self.project_tag.find('url') + return url_element.text if url_element else url_element + + def get_inception_year(self) -> Optional[str]: + """Return the project inception year.""" + # => project.inceptionYear + inc_year_element: _bs4.element.Tag = self.project_tag.find('inceptionYear') + return inc_year_element.text if inc_year_element else inc_year_element + + def get_author(self) -> Dict[str, Optional[str]]: + """Return a dictionary with 'id', 'name', and 'url' of the project author.""" + key: str = 'project.developers.developer' + author_element: List[Optional[_bs4.element.Tag]] = [ + self.get(key + '.id'), # => project.developers[0].developer.id + self.get(key + '.name'), # => project.developers[0].developer.name + self.get(key + '.url') # => project.developers[0].developer.url + ] + + return { # Return a dictionary + 'id': author_element[0].text if author_element[0] else author_element[0], + 'name': author_element[1].text if author_element[1] else author_element[1], + 'url': author_element[2].text if author_element[2] else author_element[2], + } + + def get_license(self) -> Dict[str, str]: + """Return a dictionary with 'name', 'url', and 'distribution' of the project license.""" + key: str = 'project.licenses.license' + license_element: List[Optional[_bs4.element.Tag]] = [ + self.get(key + '.name'), # => project.licenses[0].license.name + self.get(key + '.url'), # => project.licenses[0].license.url + self.get(key + '.distribution') # => project.licenses[0].license.distribution + ] + + return { + 'name': license_element[0].text if license_element[0] else license_element[0], + 'url': license_element[1].text if license_element[1] else license_element[1], + 'distribution': license_element[2].text if license_element[2] else license_element[2], + } + + def get_property(self, key: str, dot: bool = True) -> Optional[str]: + """ + Return the value of the specified property key from the POM properties. + + Parameters + ---------- + key : str + The property key. + + dot : bool, optional + If True, split the key using dots. Defaults to True. + + Returns + ------- + str or None : + The property value if found, otherwise, returns None. + + Raises + ------ + ValueError : + If the provided key is an empty string or None. + + """ + # Raise an error if the provided key is an empty string or None + if not (key or len(key)): + raise ValueError('Key argument cannot be empty.') + + # Remove the 'properties' string tag + key = key.replace('properties.', '') \ + if key.startswith('properties.') else key + + # Only split the dots if 'dot' argument enabled + keys: List[str] = key.split('.') if dot else [key] + # Add the prefix of 'properties' element tag + if not dot or (dot and keys[0] != 'properties'): + keys.insert(0, 'properties') # Append to the first index + + # This way, we can prevent an error due to NoneType use + result: _bs4.element.Tag = self.get(keys) + return result.text if result else result + + +class JMRepairer: + """ + A class for repairing manifest and properties files using information + from a POM file. + + Parameters + ---------- + pom : str, PomParser, or _bs4.BeautifulSoup + The POM file, either as a path (str), a `PomParser` instance, + or a `BeautifulSoup` object. + + Raises + ------ + ValueError + If the 'pom' argument is empty. + + TypeError + If the type of 'pom' argument is unknown, neither of str, + a `PomParser` instance, nor a `BeautifulSoup` object. + + Attributes + ---------- + _val_pattern : re.Pattern + Regular expression pattern for extracting values from curly + braces in strings. + + _soup : PomParser + Instance of `PomParser` representing the parsed POM file. + + _pom_items : Dict[str, str (could possibly None)] + Dictionary containing key-value pairs extracted from the POM file. + + """ + + def __init__(self, pom: Union[str, PomParser, _bs4.BeautifulSoup]) -> 'JMRepairer': + """Create a new instance of this class.""" + if not pom: + raise ValueError("Argument 'pom' cannot be empty") \ + from CORE_ERR + + if not isinstance(pom, (str, PomParser, _bs4.BeautifulSoup)): + raise TypeError(f"Unknown type of 'pom' argument: {type(pom).__name__}") \ + from CORE_ERR + + self._val_pattern: _re.Pattern = _re.compile(r'\$\{([\w.-\[\]]+)\}') + self._soup: PomParser = None + + if isinstance(pom, str): + self._soup = PomParser.parse(pom) # Need to be parsed first + elif isinstance(pom, _bs4.BeautifulSoup): + self._soup = PomParser(pom) # Pass directly to the constructor + elif isinstance(pom, PomParser): + self._soup = pom # Already an instance of PomParser + + project_id: Dict[str, Optional[str]] = self._soup.get_id() + project_author: Dict[str, Optional[str]] = self._soup.get_author() + project_license: Dict[str, Optional[str]] = self._soup.get_license() + + self._pom_items: Dict[str, Optional[str]] = { + 'project.name': self._soup.get_name(), + 'project.version': self._soup.get_version(), + 'project.url': self._soup.get_url(), + 'project.groupId': project_id['groupId'], + 'project.artifactId': project_id['artifactId'], + 'project.inceptionYear': self._soup.get_inception_year(), + 'project.developers[0].name': project_author['name'], + 'project.developers[0].url': project_author['url'], + 'project.licenses[0].name': project_license['name'], + 'project.licenses[0].url': project_license['url'], + 'package.licenseFile': self._soup.get_property('package.licenseFile', dot=False), + 'package.mainClass': self._soup.get_property('package.mainClass', dot=False), + 'maven.build.timestamp': _dt.now(_tz.utc).strftime('%Y-%m-%dT%H:%M:%SZ') + } + + @classmethod + def __write_out(cls, contents: List[str], out: str) -> None: + """ + Write the given contents to the specified output file. + + Parameters + ---------- + contents : a list of str + List of strings to be written to the file. + + out : str + Path to the output file. + + Raises + ------ + Exception + If an error occurs while writing to the output file. + + """ + + parentdir: str = _os.path.dirname(out) + if not _os.path.exists(parentdir): + _os.mkdir(parentdir) + + try: + with open(out, 'w', encoding='UTF-8') as o_file: + for line in contents: + o_file.write(f'{line}{_os.linesep}') + except Exception as e: + raise e from CORE_ERR + + def fix_manifest(self, infile: str, outfile: str = None) -> None: + """ + Fix the given manifest file by replacing placeholders with values + from the POM file. + + Parameters + ---------- + infile : str + Path to the input manifest file. + + outfile : str, optional + Path to the output manifest file. If not specified, + the input file will be overwritten. + + Raises + ------ + ValueError + If the 'infile' argument is empty. + + FileNotFoundError + If the specified input file does not exist. + + """ + + if not infile: + raise ValueError("Argument 'infile' cannot be empty") \ + from CORE_ERR + + if not _os.path.exists(infile): + raise FileNotFoundError(f'Cannot read non-existing file: {infile!r}') \ + from CORE_ERR + + # When outfile argument not specified, then use infile + # for the name of output file, which means will overwrite the infile + outfile = infile if not outfile else outfile + + manifest: _jmutils.JMProperties = _jmutils.JMProperties(infile) + + # Fix the manifest + for key, val in manifest.items(): + new_val = self._val_pattern.match(val) + if not new_val: + continue + + new_val = new_val[1] + if key == 'ID': + manifest[key] = f"{self._pom_items['project.groupId']}:" + \ + f"{self._pom_items['project.artifactId']}" + elif new_val in self._pom_items: + manifest[key] = self._pom_items[new_val] + + self.__write_out( + [f'{key}: {val}' for key, val in manifest.items()] + [''], + out=outfile + ) + + def fix_properties(self, infile: str, outfile: str = None) -> None: + """ + Fix the given properties file by replacing placeholders with values + from the POM file. + + Parameters + ---------- + infile : str + Path to the input properties file. + + outfile : str, optional + Path to the output properties file. If not specified, + the input file will be overwritten. + + Raises + ------ + ValueError + If the 'infile' argument is empty. + + FileNotFoundError + If the specified input file does not exist. + + """ + + if not infile: + raise ValueError("Argument 'infile' cannot be empty") \ + from CORE_ERR + + if not _os.path.exists(infile): + raise FileNotFoundError(f'Cannot read non-existing file: {infile!r}') \ + from CORE_ERR + + # If the outfile argument were not specified, then use infile + # for the name of output file, which means will overwrite the infile + outfile = infile if not outfile else outfile + + # Parse the properties file + properties: _jmutils.JMProperties = _jmutils.JMProperties(infile) + + # Fix the properties file + for key, val in properties.items(): + new_val = self._val_pattern.match(val) + if not new_val: + continue + + new_val = new_val[1] + if new_val in self._pom_items: + properties[key] = self._pom_items[new_val] + + self.__write_out( + [f'{key} = {val}' for key, val in properties.items()], + out=outfile + ) + + +__author__ = AUTHOR +__version__ = VERSION +__version_info__ = VERSION_INFO + +# Delete unused variables +del AUTHOR, VERSION, VERSION_INFO +del Dict, List, Union, Optional, TextIO + +if __name__ == '__main__': + __warn( +'''You are attempting to run this module directly (i.e. as main module), \ +which is not permitted. It is designed to be imported, not as main module.''') + _sys.exit() diff --git a/tools/JMBuilder/jmbuilder/exception/__init__.py b/tools/JMBuilder/jmbuilder/exception/__init__.py new file mode 100644 index 00000000..a0b061d6 --- /dev/null +++ b/tools/JMBuilder/jmbuilder/exception/__init__.py @@ -0,0 +1,33 @@ +"""Exception Module for JMBuilder + +This module contains several custom exception for `JMBuilder`. + +Copyright (c) 2023-2024 Ryuu Mitsuki. + + +Available Classes +----------------- +JMException + This is base exception class for all custom exception classes in + this module and this class extends to `Exception` class. + +JMUnknownTypeError + This class extends to `JMException` and raised when an unknown type + error occurs during the execution of the package. +""" + +from . import exception +from .exception import * +from .._globals import AUTHOR, VERSION, VERSION_INFO + +__all__ = ['exception'] +__all__.extend(exception.__all__) + + + +__author__ = AUTHOR +__version__ = VERSION +__version_info__ = VERSION_INFO + +# Delete imported objects that are no longer being used +del AUTHOR, VERSION, VERSION_INFO diff --git a/tools/JMBuilder/jmbuilder/exception/exception.py b/tools/JMBuilder/jmbuilder/exception/exception.py new file mode 100644 index 00000000..e796b64c --- /dev/null +++ b/tools/JMBuilder/jmbuilder/exception/exception.py @@ -0,0 +1,381 @@ +"""Custom Exceptions Module for `JMBuilder` + +This module provides all custom exceptions for `JMBuilder`. + +Copyright (c) 2023-2024 Ryuu Mitsuki. + + +Available Classes +----------------- +JMException : + The base custom exception for `JMBuilder`. + +JMUnknownTypeError : + The custom exception that raised when an unknown type error + occurs during the execution of the software. + +JMParserError : + Raised when an error has occurred during parsing the configuration. + +""" + +import os as _os +import sys as _sys +import traceback as _tb + +from typing import Optional, Any + +from .._globals import AUTHOR, VERSION, VERSION_INFO + + +__all__ = ['JMException', 'JMUnknownTypeError', 'JMParserError'] + + +class JMException(Exception): + """ + Base custom exception for `JMBuilder`. This class also provides + traceback control (i.e., customizable). + + Parameters + ---------- + *args : list of positional arguments + This parameter accepts a variable number of arguments. It treats the + first argument as a format message and the subsequent arguments as + format arguments. If only one argument is specified, it will be treated + as a literal message. The formatting follows the conventions of C-style + string formatting. For example:: + + # Assuming we have an index variable with a value of 5 + >>> JMException('Invalid index: %d', index) + JMException('Invalid index: 5', custom_traceback=False) + + # Using multiple format arguments + >>> JMException('Invalid index: %d, at file: %s', 2, 'foo.py') + JMException('Invalid index: 2, at file foo.py', custom_traceback=False) + + **kwargs : keyword arguments + Arbitrary keyword arguments are accepted. Users can also unpack a + dictionary into this parameter. All keyword arguments will be passed to + the base exception class (i.e., the `Exception` class), except for the + following reserved keywords: + + - `tb` + - `trace` + - `traces` + + These reserved keywords allow users to control the exception's + traceback behavior. If more than one of these keywords is specified, + the `tb` keyword will take precedence. However, be cautious when + specifying multiple reserved keywords, as this can lead to unexpected + behavior due to other arguments being passed to the base exception + class. + + Properties + ---------- + message : str or None + The message of this exception. + + To specify the message of this exception, consider place the message + string at first argument. + + traces : traceback.StackSummary + The stack traces of this exception. If no traceback is provided during + the exception creation, it will be overrided by `traceback.extract_stack()`. + + To manually override the stack traces of this exception, consider use keyword + `tb`, `trace` or `traces`, with the value separated by equals sign (`=`). + For example:: + + # Use the `tb` keyword + >>> JMException('An error occured', tb=foo) + JMException('An error occured', custom_traceback=True) + + # Use the `trace` keyword + >>> JMException('An error occured', trace=foo) + JMException('An error occured', custom_traceback=True) + + # Use the `traces` keyword + >>> JMException('An error occured', traces=foo) + JMException('An error occured', custom_traceback=True) + + Raises + ------ + TypeError : + If the given arguments (other than the first argument) are different with + the format specifier at the first argument. For instance:: + + >>> JMBuilder('Syntax error at line: %d', '44') + TypeError: %d format: a real number is required, not str + + """ + + message: Optional[str] + traces: _tb.StackSummary + + def __init__(self, *args, **kwargs) -> None: + """Initialize self. For accurate signature, see ``help(type(self))``.""" + + baseerr: str = f'Error occured during initializing {type(self)}' + tb_key: str = None + + if len(args) > 0 and (args[0] and not isinstance(args[0], str)): + raise self.__class__(baseerr) from \ + TypeError( + f'Invalid type of `message`: "{type(args[0]).__name__}". ' + \ + 'Expected "str"' + ) + + self.__message = None + self.__traces = None + self.__use_custom_traceback = False + + if len(args) > 1: + try: + self.__message = args[0] % args[1:] + except TypeError as type_err: + raise self.__class__(baseerr) from type_err + + elif len(args) == 1: + self.__message = args[0] + + # Check the custom stack traces in keyword arguments (kwargs) + if 'tb' in kwargs: + tb_key = 'tb' + self.__use_custom_traceback = True + elif 'trace' in kwargs or 'traces' in kwargs: + tb_key = 'trace' if 'trace' in kwargs else 'traces' + self.__use_custom_traceback = True + else: + self.__traces = _tb.extract_stack() + + # Check the instance of 'tb_key', otherwise pass if None + if tb_key: + if not isinstance(kwargs[tb_key], _tb.StackSummary): + raise self.__class__(baseerr) from \ + TypeError( + f'Unknown type: "{type(kwargs[tb_key]).__name__}". ' + + 'Expected "traceback.StackSummary"' + ) + + # Retrieve the custom traceback + self.__traces = kwargs.get(tb_key) + del kwargs[tb_key] # delete after retrieved the stack traces + + super().__init__(self.__message, **kwargs) + + + def __repr__(self) -> str: + """ + Return ``repr(self)``. + + Returns + ------- + str : + The string representation of this exception, including the class name + and the message of this exception (if specified), and the + `with_traceback` with a stringized boolean value. + + Notes + ----- + The `with_traceback` will have ``True`` value if and only if the + stack traces are defined either given from initialization class + or defaults to `traceback.extract_stack()`, otherwise ``False`` + if there is no stack traces defined in this exception. + + """ + return f"{self.__class__.__name__}({self.__message!r}, " + \ + f"custom_traceback={self.__use_custom_traceback})" + + def __str__(self) -> str: + """ + Return the string representation of this exception's message. + + Returns + ------- + str : + The message of this exception. + + Notes + ----- + This method will never returns ``NoneType`` when the message are not specified, + instead it returns the empty string. + + """ + return f'{self.__message}' if self.__message is not None else '' + + def __eq__(self, other: Any) -> bool: + """ + Return ``self==value``. + + Parameters + ---------- + other : Any + The object to compare with this exception. + + Returns + ------- + bool + ``True`` if the given object are the same exception with the same message + or if the given object are string with the same message as this exception, + otherwise ``False``. + + """ + if isinstance(other, (self.__class__, str)): + return str(self) == str(other) + + return False + + + @property + def message(self) -> Optional[str]: + """ + Get the detail message of this exception. + + Returns + ------- + str or None + The message of this exception. If not specified, returns ``None``. + + """ + return self.__message + + @property + def traces(self) -> _tb.StackSummary: + """ + Get the stack traces of this exception. + + Returns + ------- + traceback.StackSummary + The stack traces of this exception. If not specified, returns + the stack traces from ``traceback.extract_stack()``. + + """ + if self.__traces and isinstance(self.__traces, _tb.StackSummary): + return self.__traces + + return _tb.extract_stack() + + + +class JMUnknownTypeError(JMException, TypeError): + """ + Custom exception for unknown type errors. + + This exception is raised when an unknown type error occurs during + the execution of the software. + + Parameters + ---------- + *args : list of arguments + This parameter accepts a variable number of arguments. + + **kwargs : keyword arguments + Additional keyword arguments to customize the exception. + + Notes + ----- + For more details about arguments, see `JMException` documentation. + + """ + + def __init__(self, *args, **kwargs) -> None: + """Initialize self. See ``help(type(self))`` for accurate signature.""" + + super().__init__(*args, **kwargs) + self.__message = super().message + self.__traces = super().traces + + @property + def message(self) -> Optional[str]: + """ + Get the detail message of this exception. + + Returns + ------- + str or None : + The message of this exception. If not specified, returns ``None``. + + """ + return self.__message + + @property + def traces(self) -> _tb.StackSummary: + """ + Get the stack traces of this exception. + + Returns + ------- + traceback.StackSummary : + The stack traces of this exception. If not specified, returns + the stack traces from `traceback.extract_stack()`. + + """ + if self.__traces and isinstance(self.__traces, _tb.StackSummary): + return self.__traces + + return _tb.extract_stack() + + + +class JMParserError(JMException): + """ + Raised when an error has occurred during parsing the configuration. + + Parameters + ---------- + *args : list of arguments + This parameter accepts a variable number of arguments. + + **kwargs : keyword arguments + Additional keyword arguments to customize the exception. + + Notes + ----- + For more details about arguments, see `JMException` documentation. + + """ + + def __init__(self, *args, **kwargs) -> None: + """Initialize self. See ``help(type(self))`` for accurate signature.""" + + super().__init__(*args, **kwargs) + self.__message = super().message + self.__traces = super().traces + + @property + def message(self) -> Optional[str]: + """ + Get the detail message of this exception. + + Returns + ------- + str or None : + The detail message of this exception. If not specified, returns ``None``. + + """ + return self.__message + + @property + def traces(self) -> _tb.StackSummary: + """ + Get the stack traces of this exception. + + Returns + ------- + traceback.StackSummary : + The stack traces of this exception. If not specified, returns + the stack traces from `traceback.extract_stack()`. + + """ + return self.__traces + + + +__author__ = AUTHOR +__version__ = VERSION +__version_info__ = VERSION_INFO + + +# Delete imported objects that are no longer being used +del AUTHOR, VERSION, VERSION_INFO, Any, Optional diff --git a/tools/JMBuilder/jmbuilder/tests/__init__.py b/tools/JMBuilder/jmbuilder/tests/__init__.py new file mode 100644 index 00000000..ea1d041a --- /dev/null +++ b/tools/JMBuilder/jmbuilder/tests/__init__.py @@ -0,0 +1,16 @@ +"""Test module. + +Copyright (c) 2023-2024 Ryuu Mitsuki. +""" + +from . import test_utils +from .._globals import AUTHOR, VERSION, VERSION_INFO + +__all__ = ['test_utils'] + +__author__ = AUTHOR +__version__ = VERSION +__version_info__ = VERSION_INFO + +# Remove imported objects that are no longer used +del AUTHOR, VERSION, VERSION_INFO diff --git a/tools/JMBuilder/jmbuilder/tests/test_utils.py b/tools/JMBuilder/jmbuilder/tests/test_utils.py new file mode 100644 index 00000000..a240bc25 --- /dev/null +++ b/tools/JMBuilder/jmbuilder/tests/test_utils.py @@ -0,0 +1,134 @@ +""" +Test suite for utilities members and properties parser, exclusively +for `jmbuilder.utils` submodule. + +Copyright (c) 2023-2024 Ryuu Mitsuki. + +""" + +import os +import json +import unittest + +from .._globals import AUTHOR, CONFDIR, VERSION, VERSION_INFO +from ..utils import utils as jmutils + + +class TestUtilities(unittest.TestCase): + """Test class for utilities functions.""" + + # The path reference to JSON config file + jsonfile: str = os.path.join(CONFDIR, 'setup.json') + + def test_json_parser(self) -> None: + """Test the `jmbuilder.utils.config.json_parser` function.""" + test_obj = jmutils.json_parser + + # Check the existence of config file + self.assertTrue(os.path.exists(self.jsonfile)) + + # Get the config data + jsondata: dict = test_obj(self.jsonfile) + self.assertIsNotNone(jsondata) + self.assertIsInstance(jsondata, dict) + + jsondata_manual: dict = {} + # Extract the JSON data manually and compare with the other one + with open(self.jsonfile, 'r', encoding='utf-8') as file: + jsondata_manual = json.load(file) + + self.assertDictEqual(jsondata, jsondata_manual) + + @unittest.skip(reason="The 'setupinit' are no longer available in 'jmbuilder.utils'") + def test_setupinit(self) -> None: + """ + Deprecated, due to the function of `setupinit` are no longer + available in `jmbuilder.utils` package (i.e., has been removed). + + Test the `jmbuilder.utils.config.setupinit` function. + """ + + + def test_remove_comments(self) -> None: + """Test the `jmbuilder.utils.config.remove_comments` function.""" + test_obj = jmutils.remove_comments + + contents: list = [ + 'Hello, world!', + '# This is a comment, you know.', + '! This is also a comment', + 'Foo And Bar' + ] + + expected_contents: tuple = ( + [ + 'Hello, world!', + '! This is also a comment', + 'Foo And Bar' + ], + [ + 'Hello, world!', + '# This is a comment, you know.', + 'Foo And Bar' + ], + [ + 'Hello, world!', + 'Foo And Bar' + ] + ) + + expected_delimiters: tuple = ('#', '!') + + for i, delimiter in enumerate(expected_delimiters): + self.assertListEqual( + test_obj(contents, delim=delimiter), + expected_contents[i] + ) + + self.assertListEqual(test_obj( + test_obj(contents, delim=expected_delimiters[0]), + delim=expected_delimiters[1] + ), expected_contents[2]) + + def test_remove_blanks(self) -> None: + """Test the `jmbuilder.utils.config.remove_blanks` function.""" + test_obj = jmutils.remove_blanks + + contents: list = [ + '', # blank line + 'Not blank line', + ' ', # line with trailing whitespace + None + ] + + expected_contents: tuple = ( + [ + 'Not blank line', + None + ], + [ + 'Not blank line' + ] + ) + + + len_exp_contents: int = len(expected_contents) + for i in range(len_exp_contents): + self.assertListEqual( + # Pass any numbers other than zero to `bool` will returning True value, + # including negative numbers + test_obj(contents, none=bool(i)), expected_contents[i] + ) + + +__author__ = AUTHOR +__version__ = VERSION +__version_info = VERSION_INFO + + +# Remove imported objects that are no longer used +del AUTHOR, VERSION, VERSION_INFO + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/JMBuilder/jmbuilder/utils/__init__.py b/tools/JMBuilder/jmbuilder/utils/__init__.py new file mode 100644 index 00000000..a2680b92 --- /dev/null +++ b/tools/JMBuilder/jmbuilder/utils/__init__.py @@ -0,0 +1,25 @@ +"""Utilities Module for JMBuilder + +This module provide utilities for `JMBuilder` package. + +Copyright (c) 2023-2024 Ryuu Mitsuki. +""" + +from . import logger, utils +from .logger import * +from .utils import * + +from .._globals import AUTHOR, VERSION, VERSION_INFO + +__all__ = ['logger', 'utils'] +__all__.extend(logger.__all__) +__all__.extend(utils.__all__) + + +__author__ = AUTHOR +__version__ = VERSION +__version_info__ = VERSION_INFO + + +# Delete imported objects that are no longer used +del AUTHOR, VERSION, VERSION_INFO diff --git a/tools/JMBuilder/jmbuilder/utils/logger.py b/tools/JMBuilder/jmbuilder/utils/logger.py new file mode 100644 index 00000000..fc1a6154 --- /dev/null +++ b/tools/JMBuilder/jmbuilder/utils/logger.py @@ -0,0 +1,195 @@ +"""Custom Logger Module for JMBuilder + +This module provides a custom logger utility that initializes and creates a new +`Logger` object for logging information or errors to the console or a log file. + +To use the custom logger in your project, you can import the `init_logger` function +from this module and create a new `Logger` object with desired settings. + +Copyright (c) 2023-2024 Ryuu Mitsuki. + + +Available Functions +------------------- +init_logger + Initialize and create new `Logger` object for logging any + information or errors to file, if specified, otherwise the output + will be written to console standard error. + +Available Constants +------------------- +BASIC_FORMAT : str + The basic (default) format for `logging.Formatter`. + This is alias for `logging.BASIC_FORMAT`. + +CUSTOM_FORMAT : str + The custom format for `logging.Formatter`. + +CRITICAL : int + The integer value representation of logging level 50. + This is alias for `logging.CRITICAL`. + +DEBUG : int + The integer value representation of logging level 10. + This is alias for `logging.DEBUG`. + +ERROR : int + The integer value representation of logging level 40. + This is alias for `logging.ERROR`. + +FATAL : int + The integer value representation of logging level 50. + This is alias for `logging.FATAL`. + +INFO : int + The integer value representation of logging level 20. + This is alias for `logging.INFO`. + +NOTSET : int + The integer value of representation of logging level 0. + This is alias for `logging.NOTSET`. + +WARN : int + The integer value of representation of logging level 30. + This is alias for `logging.WARN`. + +WARNING : int + The integer value of representation of logging level 30. + This is alias for `logging.WARNING`. + +Example +------- +# Import the module +>>> from jmbuilder import logger + +# Create a new logger object +>>> log = logger.init_logger(fmt=logger.CUSTOM_FORMAT, +... level=logger.INFO) +>>> log + +>>> type(log).__name__ +'RootLogger' + +# Log some information message +# The output will be printed to console standard error (stderr), +# because the `filename` are not defined on `init_logger`. +>>> log.info('This is an information message.') +This is an information message. + +# Log some exception +# To trace the error, pass any true value to `exc_info` argument. +>>> try: +... x = 3 / 0 +... except ZeroDivisionError as div_err: +... log.error('An error occurred.', exc_info=1) +An error occurred. +Traceback (most recent call last): + ... +ZeroDivisionError: division by zero +""" + +import os as _os +import sys as _sys +import logging as _log +from typing import Union + +from .._globals import AUTHOR, VERSION, VERSION_INFO, STDERR +from ..exception import JMUnknownTypeError as _JMTypeError + + +__all__ = ['init_logger'] +__author__ = AUTHOR +__version__ = VERSION +__version_info__ = VERSION_INFO + + +# References of formatter +BASIC_FORMAT = _log.BASIC_FORMAT +CUSTOM_FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + +# References of logging levels +NOTSET = _log.NOTSET # 0 +DEBUG = _log.DEBUG # 10 +INFO = _log.INFO # 20 +WARN = _log.WARN # 30 +WARNING = _log.WARNING # 30 +ERROR = _log.ERROR # 40 +CRITICAL = _log.CRITICAL # 50 +FATAL = _log.FATAL # 50 + + +def init_logger(filename: str = None, *, fmt: Union[str, _log.Formatter] = None, + level: int = DEBUG) -> _log.Logger: + """ + Initializes and creates a new `Logger` object. + + Parameters + ---------- + filename : str or None, optional + A string representing the name of the logger file. If specified, + logs will be written to the specified file, otherwise logs + will be printed to `stderr` (standard error). Default is ``None``. + + fmt : str or logging.Formatter, optional + A string representation of the log formatter or an object + of `logging.Formatter` class. If not specified, a customized + formatter will be used. See ``CUSTOM_FORMAT`` constant variable. + + level : int, optional + An integer value that specifies the logging level for the logger. + Default is ``logger.DEBUG`` (equal to 10). + + Returns + ------- + logging.Logger : + A new `Logger` object for logging any information or errors. + + Raises + ------ + JMUnknownTypeError : + If the 'fmt' are not instance of `str` or `logging.Formatter` class. + + """ + + handler: _log.Handler = None + + # Check whether the 'filename' are specified + if not filename: + handler = _log.StreamHandler(STDERR) + else: + if not isinstance(filename, str): + filename = str(filename) + + if isinstance(filename, str) and not filename.endswith('.log'): + filename += '.log' + + # Check whether the parent directory of 'filename' exist + if not _os.path.exists(_os.path.dirname(filename)): + # Create the directory if not exist + _os.mkdir(_os.path.dirname(filename)) + handler = _log.FileHandler(filename) + + # Check whether the 'fmt' as log formatter are specified + if not fmt: + handler.setFormatter(_log.Formatter(CUSTOM_FORMAT)) + elif fmt and isinstance(fmt, (str, _log.Formatter)): + if isinstance(fmt, str): + handler.setFormatter(_log.Formatter(fmt)) + else: + handler.setFormatter(fmt) + else: + raise _JMTypeError( + f'Invalid type of `fmt`: "{type(fmt).__name__}". ' + \ + 'Expected "str" and "logging.Formatter"') + + logger = _log.getLogger( + _os.path.basename(filename) + ) if filename else _log.getLogger('JMBuilder log') + logger.setLevel(level) # set the logger level, default is DEBUG + logger.addHandler(handler) # set the handler + + return logger + + +# Remove unnecessary variables +del AUTHOR, VERSION, VERSION_INFO, Union diff --git a/tools/JMBuilder/jmbuilder/utils/utils.py b/tools/JMBuilder/jmbuilder/utils/utils.py new file mode 100644 index 00000000..bb0663e2 --- /dev/null +++ b/tools/JMBuilder/jmbuilder/utils/utils.py @@ -0,0 +1,525 @@ +"""Utilities Module for JMBuilder + +This module provide utilities members, including JSON parser and properties +parser for `JMBuilder` module. + +Copyright (c) 2023-2024 Ryuu Mitsuki. + + +Available Classes +----------------- +JMProperties + A custom properties parser class, implemented in `jmbuilder.utils` package. + Python does not provide an easy way to parsing files with extension of + `.properties`, this class is designed to parse properties file and + access their contents with ease without any third-party modules. + +Available Functions +------------------- +json_parser + This utility function provides an easy way to parses and retrieves all + configurations from JSON configuration files. + + Examples:: + + >>> json_parser('path/to/configs_file.json') + {'foo': False, 'bar': True} + +remove_comments + This utility function can remove lines from a list of strings representing + the file contents that starting with a specified delimiter and returns + a new list with lines starting with a specified delimiter removed. + This function are designed for removing comments lines from file contents. + + Examples:: + + # Read file contents + >>> contents = [] + >>> with open('path/to/example.txt') as f: + ... contents = f.readlines() + ... + >>> contents + ['Hello', '# This is comment line', 'World'] + + >>> remove_comments(contents, delim='#') + ['Hello', 'World'] + +remove_blanks + This utility function can remove blank lines from the list of strings + representing the file contents, and optionally remove lines with value `None`. + Return a new list with blank lines removed. + + Examples:: + + >>> contents = ['', 'Foo', None, '', '1234'] + >>> remove_blanks(contents, none=False) # Ignore None + ['Foo', None, '1234'] + + >>> remove_blanks(contents, none=True) + ['Foo', '1234'] + +readfile + This utilty function will read the contents from the given file path and + return a list containing all contents from that file. + + If the given path does not refer to an existing regular file or the given path + is refer to a directory, it will raises an error. + + Examples:: + + # Read contents from file called 'myfile.txt' + >>> contents = readfile('myfile.txt') + +""" + +import os as _os +import io as _io +import sys as _sys +import json as _json +import locale as _locale +import collections as _collections +from pathlib import Path as _Path +from typing import ( + Dict, List, Optional, + Union, Type, TextIO, Sequence +) + +from .._globals import AUTHOR, VERSION, VERSION_INFO +from ..exception import ( + JMUnknownTypeError as _JMTypeError, + JMParserError as _JMParserError +) + + +__all__ = [ + 'json_parser', 'remove_comments', 'remove_blanks', 'JMProperties' +] + + +def json_parser(path: str) -> dict: + """ + Parse and retrieve all configurations from specified JSON + configuration file. + + Parameters + ---------- + path : str + The path to specify the configuration file to be parsed. + + Returns + ------- + dict : + A dictionary containing all parsed configurations from specified + configuration file. + + Raises + ------ + JMParserError : + If something went wrong during parsing the configuration file. + + ValueError : + If the given path is `None`. + + JMUnknownTypeError : + If the given path's type are not `str`. + + FileNotFoundError : + If the given path are refers to a non-existing file. + + IsADirectoryError : + If the given path are refers to a directory instead a configuration file. + + Notes + ----- + This function only supported configuration file with JSON type. + Given file paths will be validated before retrieving their configurations, + and check the extension file. Make sure the given path references + to file with the `.json` extension. + + """ + if not path: + raise ValueError( + f"Unexpected value: '{type(path).__name__}'. Expected 'str'" + ) from _JMParserError( + "Something went wrong while parsing the configuration file" + ) + + # Raise an error if the given path not `str` type + if not isinstance(path, str): + raise _JMTypeError( + f"Unknown type '{type(path).__name__}'. Expected 'str'" + ) from _JMParserError( + "Something went wrong while parsing the configuration file" + ) + + # Check existence + if not _os.path.exists(path): + raise FileNotFoundError( + f"No such file or directory: '{path}'" + ) from _JMParserError( + 'Configuration file was not found' + ) + + # Check whether a directory or regular file + if _os.path.isdir(path): + raise IsADirectoryError( + f"Is a directory: '{path}'" + ) from _JMParserError( + 'Directory found. No such configuration file' + ) + + # Check the extension file + if not path.endswith('.json'): + raise _JMParserError( + 'Unknown file type. No such JSON configuration file' + ) + + configs: dict = {} + + with open(path, 'r', encoding='utf-8') as cfg_file: + contents: str = cfg_file.read() + + # Check for null to prevent an error on JSONDecoder + if contents: + configs = _json.loads(contents) # return a dictionary + + return configs + + + +def remove_comments(contents: List[str], delim: str = '#') -> List[str]: + """ + Remove lines starting with a specified delimiter. + + This function removes lines from the input list of contents that start + with the specified delimiter. It returns a new contents with comments removed. + + Parameters + ---------- + contents : List[str] + A list of strings representing the contents of a file. + + delim : str, optional + The delimiter used to identify comment lines. Lines starting with + this delimiter will be removed. The default is '#'. + + Returns + ------- + List[str] : + A new contents with comment lines (specified by delimiter) removed. + + Raises + ------ + ValueError : + If the input list `contents` is empty. + + Notes + ----- + Multiple specified delimiters cannot be removed in a single call to + this function. Although the problem can be fixed by executing the + procedure as many times depending on the delimiters that need + to be removed. But still it is not a convenient way. + + Examples:: + + # Suppose we want to remove lines that starting with + # hashtags (#) and exclamation marks (!). + >>> remove_comments( + ... remove_comments(contents, delim='#'), delim='!') + + """ + if not contents or len(contents) == 0: + raise ValueError('File contents cannot be empty') + + # Use a list comprehension to filter out lines starting with the delimiter + return [line for line in contents if not line.startswith(delim)] + + + +def remove_blanks(contents: List[str], none: bool = True) -> List[str]: + """ + Remove empty lines from a list of strings. + + This function removes empty lines (lines with no content) and lines + containing only whitespace from the input list of strings. Optionally, + it can removes lines containing `None`. + + Parameters + ---------- + contents : List[str] + A list of strings representing the contents of a file. + + none : bool, optional + If True, lines containing `None` are also removed. + If False, only lines with no content are removed. The default is True. + + Returns + ------- + List[str] : + A new contents with empty lines removed. + + Raises + ------ + ValueError : + If the input list `contents` is empty. + + """ + if not contents or len(contents) == 0: + raise ValueError('File contents cannot be empty') + + return [ + line for line in contents + if (line is None and not none) or \ + (line is not None and line.strip() != '') + ] + + +def readfile(path: str, encoding: str = 'UTF-8') -> List[str]: + """ + Read all contents from the specified file. + + Parameters + ---------- + path : str + A string path refers to a regular file. + + encoding : str, optional + An encoding to be used during read operation. + Defaults to `UTF-8`. + + Returns + ------- + List[str] : + A list of string containing contents from the specified file. + + Raises + ------ + FileNotFoundError + If the given path does not refer to the existing file. + + PermissionError + If there is a permission restrictions during read operation. + + IsADirectoryError + If the given path is refer to a directory, not a regular file. + + """ + + contents: List[str] = [] + with open(path, 'r', encoding=encoding) as file: + contents = file.readlines() + + return contents + + +def remove_duplicates(seq: Sequence) -> Sequence: + """ + Remove duplicates from a sequence while preserving the original order. + + Parameters + ---------- + seq : list or iterable + The input sequence containing elements with potential duplicates. + + Returns + ------- + list + A new list containing unique elements from the input sequence, + while maintaining the original order. + + Notes + ----- + This function uses a set to keep track of seen elements and filters + out duplicates while preserving the order of the original sequence. + + Example + ------- + >>> foo = [1, 2, 3, 1, 2, 4, 5] + >>> remove_duplicates(foo) + [1, 2, 3, 4, 5] + + """ + seen = set() + return [x for x in seq if not (x in seen or seen.add(x))] + + +class JMProperties(_collections.UserDict): + """ + This class provides a convenient way to parse properties files + and access their contents. + + Parameters + ---------- + filename : str or TextIO + The filename or file object to read properties from. If a filename is + provided, it checks for the file's existence, opens the file stream, + and retrieves the properties. If a file object is provided, it directly + reads the properties from it. + + encoding : str, optional + The encoding to use when opening the file stream. If not specified, + it uses the encoding from `locale.getpreferredencoding()`. + Defaults to system's preferred encoding. + + Attributes + ---------- + data : Dict[str, str] + A dictionary containing all the parsed properties. + + filename : str + An absolute path to the specified property file. + + Raises + ------ + JMParserError : + If an error occurs while reading and parsing the properties file. + + FileNotFoundError : + If the specified file path does not exist. + + ValueError : + If the `filename` parameter is None. + + """ + def __init__(self, filename: Union[str, TextIO], *, + encoding: str = _locale.getpreferredencoding()) -> None: + """Initialize self.""" + + self.filename = filename + self.encoding = encoding + + def __blank_remover(contents: list) -> list: + """ + Remove trailing whitespace and newline character from the contents. + + Parameters + ---------- + contents : list + A list of strings representing contents of file. + + Returns + ------- + list : + A new list with trailing whitespace and newline characters removed. + + """ + return [line.strip() for line in contents] + + def __line_splitter(contents: list, delim: str) -> list: + """ + Split the given string using specified delimiters from the contents. + + Parameters + ---------- + contents : list + A list of strings representing contents of file. + + delim: str + The delimiter to be used for splitting keys and values. + + Returns + ------- + list : + A new list with each entries contains two strings that + representing a key and value respectively. + + Notes + ----- + The length of an entry in the returned list might be used to indicate + an unsuccessful result. If the length is equal to one, it indicates + that the function was unable to split the contents using the specified + delimiter (this could be due to the delimiter not being present + in the contents). + + For clarity, here's some examples:: + + >>> contents = ['foo: bar', 'pswd: helloWORLD'] + >>> new_contents = __line_splitter(contents, '$') + >>> len(new_contents[0]) # Check the length + 1 # Failed to split due to the delim not present in contents + + >>> new_contents = __line_splitter(contents, ':') + >>> len(new_contents[0]) + 2 # Succeed + + """ + return [line.split(delim, maxsplit=1) for line in contents] + + + if isinstance(filename, str): + self.filename = _os.path.abspath(filename) + + # If encoding is not specified, use the system's preferred encoding + encoding = encoding if encoding else _locale.getpreferredencoding() + + if not self.filename: + raise ValueError("The 'filename' parameter cannot be None") + + # Raise FileNotFoundError, if the given file are not exist + # First these code below will checks whether the given file is not None + # and its type is `str` + if self.filename and \ + isinstance(self.filename, str) and \ + not _os.path.exists(self.filename): + raise FileNotFoundError(f"File not found: '{filename}'") \ + from _JMParserError( + 'The specified path does not reference any property file ' + + 'or the file does not exist' + ) + + properties_data: Dict[str, str] = {} + contents: list = [] + + # Open and read the contents if the given file is of type `str` + if isinstance(filename, str): + with open(filename, 'r', encoding=encoding) as prop: + contents = prop.readlines() + elif isinstance(filename, _io.TextIOWrapper): + contents = filename.readlines() + + # Get the name of property file + self.filename = filename.name + + # Extract file contents, remove comments and empty strings + contents = __blank_remover(contents) + contents = remove_comments(contents, '#') + contents = remove_blanks(contents, none=True) + + # First, try to split the keys and values using equals sign (=) as a delimiter + data: List[str] = __line_splitter(contents, delim='=') + keys, values = None, None + + # Check if the first try has extracted the keys and values successfully + # If the length of each element is one, it indicates extraction failure, + # so we try splitting using a colon (:) as a delimiter + if data and len(data[0]) == 1: + # In this second try, use a colon (:) as a delimiter + # for keys and values + data = __line_splitter(contents, delim=':') + + try: + # Unpack keys and values into variables (errors can occur here) + keys, values = zip(*data) + except (ValueError, TypeError) as type_val_err: + raise type_val_err from _JMParserError( + 'Unable to unpack keys and values' + ) + + # Remove trailing whitespace in keys and values + keys = tuple(__blank_remover(keys)) + values = tuple(__blank_remover(values)) + + # Build the dictionary from extracted keys and values + properties_data = dict(zip(keys, values)) + + super().__init__(properties_data) + + +__author__ = AUTHOR +__version__ = VERSION +__version_info__ = VERSION_INFO + + +# Delete imported objects that are no longer used +del AUTHOR, VERSION, VERSION_INFO +del Dict, List, Optional, Union, Type, TextIO, Sequence diff --git a/tools/JMBuilder/requirements.txt b/tools/JMBuilder/requirements.txt new file mode 100644 index 00000000..2a82b616 --- /dev/null +++ b/tools/JMBuilder/requirements.txt @@ -0,0 +1,6 @@ +beautifulsoup4==4.11.1 +lxml==4.9.3 +sphinx==5.3.0 +numpydoc==1.5.0 +pydata-sphinx-theme==0.13.3 +sphinxcontrib-lastupdate==1.1 \ No newline at end of file diff --git a/tools/JMBuilder/test-requirements.txt b/tools/JMBuilder/test-requirements.txt new file mode 100644 index 00000000..b283d376 --- /dev/null +++ b/tools/JMBuilder/test-requirements.txt @@ -0,0 +1,9 @@ +pylint>=2.17.3 +platformdirs>=2.2.0 +isort>=4.2.5 +mccabe>=0.6 +tomlkit>=0.10.1 +dill>=0.3.6 +colorama>=0.4.5 +lazy-object-proxy>=1.4.0 +wrapt>=1.14 \ No newline at end of file From 9d7c48572f0d0dc9bee901bdc3be1460766cb177 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Sat, 6 Jan 2024 00:33:56 +0700 Subject: [PATCH 20/36] Update the PyLint config file Several changes and overrides configuration has been made which from .pylintrc of JMBuilder package --- .pylintrc | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/.pylintrc b/.pylintrc index 3d501483..2f5ae768 100644 --- a/.pylintrc +++ b/.pylintrc @@ -39,14 +39,17 @@ extension-pkg-whitelist= fail-on= # Specify a score threshold under which the program will exit with error. -fail-under=10 +fail-under=0.7 # Interpret the stdin as a python script, whose filename needs to be passed as # the module_or_package argument. #from-stdin= # Files or directories to be skipped. They should be base names, not paths. -ignore=CVS +ignore=CVS, + venv, + .venv, + docs # Add files or directories matching the regular expressions patterns to the # ignore-list. The regex matches against paths and can be in Posix or Windows @@ -72,7 +75,7 @@ ignored-modules= # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the # number of processors available to use, and will cap the count on Windows to # avoid hangs. -jobs=1 +jobs=0 # Control the amount of potential inferred values when inferring a single # object. This can help the performance when dealing with large functions or @@ -88,10 +91,10 @@ persistent=yes # Minimum Python version to use for version dependent checks. Will default to # the version used to run pylint. -py-version=3.11 +py-version=3.7 # Discover python modules and packages in the file system subtree. -recursive=no +recursive=yes # Add paths to the list of the source roots. Supports globbing patterns. The # source root is an absolute path or a path relative to the current working @@ -189,6 +192,7 @@ good-names=i, j, k, v, + e, ex, Run, _ @@ -289,16 +293,16 @@ ignored-parents= max-args=5 # Maximum number of attributes for a class (see R0902). -max-attributes=10 +max-attributes=7 # Maximum number of boolean expressions in an if statement (see R0916). max-bool-expr=5 # Maximum number of branch for function / method body. -max-branches=35 +max-branches=12 # Maximum number of locals for function / method body. -max-locals=25 +max-locals=15 # Maximum number of parents for a class (see R0901). max-parents=7 @@ -310,10 +314,10 @@ max-public-methods=20 max-returns=6 # Maximum number of statements in function / method body. -max-statements=80 +max-statements=50 # Minimum number of public methods for a class (see R0903). -min-public-methods=1 +min-public-methods=2 [EXCEPTIONS] @@ -362,7 +366,7 @@ allow-any-import-level= allow-reexport-from-package=no # Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=yes +allow-wildcard-with-all=no # Deprecated modules which should not be used, separated by a comma. deprecated-modules= @@ -459,7 +463,7 @@ notes-rgx= [REFACTORING] # Maximum number of nested blocks for function / method body -max-nested-blocks=6 +max-nested-blocks=5 # Complete name of functions that never returns. When checking for # inconsistent-return-statements if a never returning function is called then @@ -499,7 +503,7 @@ score=yes ignore-comments=yes # Docstrings are removed from the similarity computation -ignore-docstrings=no +ignore-docstrings=yes # Imports are removed from the similarity computation ignore-imports=yes From 12a8ffb1e9e25989112aa3633ba374eb4143a3e8 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Sat, 6 Jan 2024 17:41:40 +0700 Subject: [PATCH 21/36] feat: Add retriever module for POM information retrieval This module includes functions to calculate MD5 hashes, verify hashes, set up and retrieve information from a parsed POM file, and handle the main execution of the script to retrieve and print values based on user input. Syntax: $ python scripts/retriever.py Example: $ python scripts/retriever.py project.name JMatrix --- scripts/retriever.py | 199 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 scripts/retriever.py diff --git a/scripts/retriever.py b/scripts/retriever.py new file mode 100644 index 00000000..d007d839 --- /dev/null +++ b/scripts/retriever.py @@ -0,0 +1,199 @@ +""" +Retrieve and manipulate information from a parsed POM +(*Project Object Model*) file. + +This module includes functions to calculate MD5 hashes, verify hashes, +set up and retrieve information from a parsed POM file, and handle the main +execution of the script to retrieve and print values based on user input. + +Copyright (c) 2023-2024 Ryuu Mitsuki. + +Available Constants +------------------- +ROOT_DIR : os.PathLike + The absolute path to the root directory. + +TARGET_DIR : os.PathLike + The path to the target directory. + +OUTPUT_DIR : os.PathLike + The path to the output directory for retrieved information. + +Available Functions +------------------- +md5_get + Calculate the MD5 hash of the specified file. + +md5_verify + Verify the MD5 hash of a file against the original hash. + +setup + Set up and retrieve information from the parsed POM file. + +main + Main function to retrieve POM information and print values + based on user input. + +""" + + +import os +import sys +import json +import hashlib +from typing import Dict, Tuple, Optional + +ROOT_DIR: os.PathLike = os.path.abspath( + os.path.join(os.path.dirname(__file__), '..')) +TARGET_DIR: os.PathLike = os.path.join(ROOT_DIR, 'target') +OUTPUT_DIR: os.PathLike = os.path.join(TARGET_DIR, '_retriever') + + +def md5_get(file: os.PathLike, block_size: int = -1) -> str: + """ + Calculate the MD5 hash of the specified file. + + Parameters + ---------- + file : path-like string + The path to the file. + block_size : int, optional + The block size for reading the file, by default -1. + + Returns + ------- + str + The MD5 hash of the file. + """ + # Prevent an error when specifying lower than -1 + block_size = -1 if block_size < -1 else block_size + with open(file, 'rb') as file_obj: # Must use read binary mode + return hashlib.md5(file_obj.read(block_size)).hexdigest() + +def md5_verify(orig: str, file: os.PathLike) -> bool: + """ + Verify the MD5 hash of a file against the original hash. + + Parameters + ---------- + orig : str + A string representing the original MD5 hash. + file : path-like string + The path to the file. + + Returns + ------- + bool + ``True`` if the file's MD5 hash matches the original hash, + ``False`` otherwise. + """ + return md5_get(file) == orig + +def setup() -> str: + """ + Set up and retrieve information from the parsed POM file. + + Returns + ------- + str + The base name of the output file, which stored inside + directory ``OUTPUT_DIR``. + """ + # Insert the JMBuilder path to PYTHONPATH environment + sys.path.insert(0, os.path.join(ROOT_DIR, 'tools', 'JMBuilder')) + + # DO NOT TRY TO MOVE THIS IMPORTS TO GLOBAL!! Keep on this function only + # for speed and performance when commands in Makefile run this + # module multiple times. + try: + # Try to import the package + import jmbuilder + except (ValueError, ImportError, ModuleNotFoundError) as import_err: + print(f'{os.path.basename(__file__)}: {type(import_err).__name__}:', + import_err, file=sys.stderr) + print('>>> sys.path', file=sys.stderr) + _ = [print(path, file=sys.stderr) for path in sys.path] + sys.exit(1) + + curpom_file: os.PathLike = os.path.join(OUTPUT_DIR, '.CURRENT_POM') + + def write_checkpoint(md5_hash: str) -> None: + with open(curpom_file, 'w') as curpom_obj: + curpom_obj.write(md5_hash) + + # Retrieve several information (not all) from the parsed POM file, + # including the project name, version, group ID, etc. + pom_items: Dict[str, Optional[str]] = jmbuilder.JMRepairer( + os.path.join(ROOT_DIR, 'pom.xml'))._pom_items + del pom_items['maven.build.timestamp'] # No need for this + + # Create the temporary directory, ignoring existence + os.makedirs(OUTPUT_DIR, exist_ok=True) + + # Get the MD5 hash of POM file + pom_md5: str = md5_get(os.path.join(ROOT_DIR, 'pom.xml')) + write_checkpoint(pom_md5) # Write the checkpoint for later check + + # Create the file name from MD5 hash of POM file + output_file: os.PathLike = os.path.join( + OUTPUT_DIR, f'pom-{pom_md5}') + + with open(output_file, 'w') as out: + out.write(json.dumps(pom_items, sort_keys=True)) + + return os.path.basename(output_file) + +def main() -> None: + """ + Main function to retrieve POM information and return the values + based on user input. + + First it will checks the '.CURRENT_POM' file within 'target' + directory, if not exist it will try to setup and retrieve the + information from the POM file, if exist then read the MD5 hash + in that file to be used to find previously retrieved information + with JSON format in the same directory. + + Returns + ------- + str + The value of the given key which from the first argument. + """ + curpom_file: os.PathLike = os.path.join( + OUTPUT_DIR, '.CURRENT_POM') + pom_file: os.PathLike = os.path.join(ROOT_DIR, 'pom.xml') + + has_setup: bool = False + out_file: str = None + if not (os.path.isdir(OUTPUT_DIR) and os.path.exists(curpom_file)): + out_file = setup() + has_setup = True + + md5_hash: str = '' + if not has_setup: + with open(curpom_file, 'r') as curpom_obj: + md5_hash = curpom_obj.read() + if not md5_verify(md5_hash, pom_file): + setup() + else: + # Simply remove the prefix + md5_hash = out_file.removeprefix('pom-') + + json_file: os.PathLike = os.path.join(OUTPUT_DIR, f'pom-{md5_hash}') + if not os.path.exists(json_file): + json_file = setup() + + pom_items: Dict[str, Optional[str]] = {} + with open(json_file) as file_obj: + pom_items = json.load(file_obj) + + args: Tuple[str] = tuple(sys.argv[1:]) + if len(args) == 0: + return + + return pom_items.get(args[0], '') + +del Dict, Optional, Tuple + +if __name__ == '__main__': + print(main()) # This to make the value can be piped to Makefile From 645299d5b7ac91df24a0c1bdb64c1d4ad3d59ce3 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Sun, 7 Jan 2024 20:40:08 +0700 Subject: [PATCH 22/36] refactor: Update the internal Makefiles in make directory * The properties setup now utilizes the 'scripts/retriever.py' Python script to retrieve necessary project information * Replaced the `$(shell pwd)` code to get the current directory with CURDIR variable instead * Initialization of required files, e.g. MANIFEST.MF, has been added and its utilizes the JMBuilder to initiate and fix that files * Further improvements and refactors also has been made to make the Makefiles compatible with the new builder and works as expected --- make/Func.mk | 9 ++----- make/Main.mk | 58 +++++++++++++++++++++++++---------------- make/Prop.mk | 24 ++++++++++++++--- make/Setup.mk | 72 +++++++++++++++++++++------------------------------ 4 files changed, 88 insertions(+), 75 deletions(-) diff --git a/make/Func.mk b/make/Func.mk index 6ccfe123..b146d165 100644 --- a/make/Func.mk +++ b/make/Func.mk @@ -245,15 +245,10 @@ endif # __MAKE_FUNC_MK # __lines Function # -# This function prints a line with title in the center (if specified) to the console. -# With custom colors and prefixes applied. +# This function prints a line to the console. With custom colors and prefixes applied. # # Usage: -# $(call __lines,) -# -# Arguments: -# title (optional): -# The title to be printed. +# $(call __lines) # define __lines $(call __info,------------------------------------------------------------------------) diff --git a/make/Main.mk b/make/Main.mk index 154e5801..eaef4a3c 100644 --- a/make/Main.mk +++ b/make/Main.mk @@ -19,7 +19,9 @@ # Import: Setup.mk -include $(or $(MAKE_DIR),$(shell pwd)/make)/Setup.mk +### TODO: Remove this when all necessary variables has correctly +### imported from the Makefile within the project's root directory. +include $(or $(MAKE_DIR),$(CURDIR)/make)/Setup.mk ## :::::::::::::::: ## @@ -87,22 +89,19 @@ endef # __pr_include_src # The job name. # define __job_msg - $(info $(CLR_PREFIX) $(call __bold,-----[) $(call __clr,2,$(1)) $(call __bold,::) $(call __clr_br,4,$(PROGNAME)) $(call __bold,]-----)) + $(info $(CLR_PREFIX) $(call __bold,-----[) $(call __clr,2,$(1)) $(call __bold,::) $(call __clr_br,4,$(ARTIFACT_ID)) $(call __bold,]-----)) $(info $(CLR_PREFIX)) endef # __job_msg - ## :::::::::::::::: ## ## Builders ## ## :::::::::::::::: ## -compile: $(CLASSES_FILES) - @: - +compile: $(CLASSES_FILES) ; -package: compile | $(SOURCES) $(MANIFEST) $(RESOURCES) +package: compile | $(SOURCES) $(ORIG_MANIFEST) $(ORIG_SETUP_PROPERTIES) $(RESOURCES) $(call __job_msg,$@) $(call __pr_include_src) @@ -111,25 +110,41 @@ package: compile | $(SOURCES) $(MANIFEST) $(RESOURCES) $(eval JARFLAGS := $(subst {MANIFEST},$(MANIFEST),$(JARFLAGS)) $(FLAGS)) - $(call __info,Copying $(words $(shell find $(RESOURCES) -type f)) resources to '$(shell pwd)/$(CLASSES_DIR)'...) - @cp --recursive --preserve=all $(RESOURCES) $(CLASSES_DIR) \ + $(call __info,Copying $(words $(RESOURCES)) resources to '$(CURDIR)/$(CLASSES_DIR)'...) + @cp --recursive --preserve=all $(RESOURCES_ONLY_DIR) $(CLASSES_DIR) \ $(if $(__intern_VERBOSE),--verbose,) + $(call __info,) + + # Fix 'setup.properties' + $(call __info,Initiating '$(notdir $(SETUP_PROPERTIES))' file...) + PYTHONPATH="$(BUILDER_DIR):$PYTHONPATH" $(PY) -m jmbuilder --fix-properties \ + pom.xml $(ORIG_SETUP_PROPERTIES) $(SETUP_PROPERTIES) + # Overwrite the existing 'setup.properties' file in 'target/classes' directory + @cp --preserve=all $(SETUP_PROPERTIES) $(CLASSES_DIR)/$(subst $(wildcard $(RESOURCE_DIR)),,$(dir $(ORIG_SETUP_PROPERTIES))) + + # Fix 'MANIFEST.MF' + $(call __info,Initiating '$(notdir $(MANIFEST))' file...) + PYTHONPATH="$(BUILDER_DIR):$PYTHONPATH" $(PY) -m jmbuilder --fix-manifest \ + pom.xml $(ORIG_MANIFEST) $(MANIFEST) + # For MANIFEST file only, no need to copy to 'classes' directory + + $(call __info,$(call __clr,2,All initiated.)) $(call __info,) - $(call __info,Creating the JAR file...) - @$(JAR) $(subst {JAR},$(word 1,$(JAR_NAMES)),$(JARFLAGS)) $(MANIFEST) \ + $(call __info,Generating the JAR file...) + @$(JAR) $(subst {JAR},$(word 1,$(JAR_NAMES)),$(JARFLAGS)) \ $(wildcard LICENSE) -C $(CLASSES_DIR) . # Create the second JAR file containing the source files only, # if and only if the INCLUDE_SRC option is "true" (enabled) ifeq ($(__intern_INC_SRC),1) $(call __info,) - @echo "$(CLR_PREFIX) $(call __bold,-----[) $(call __clr,2,$@:sources) $(call __bold,::) $(call __clr_br,4,$(PROGNAME)) $(call __bold,]-----)" - $(call __info,Creating the JAR file (sources)...) - @$(JAR) $(subst {JAR},$(word 2,$(JAR_NAMES)),$(JARFLAGS)) $(MANIFEST) \ + @echo "$(CLR_PREFIX) $(call __bold,-----[) $(call __clr,2,$@:sources) $(call __bold,::) $(call __clr_br,4,$(ARTIFACT_ID)) $(call __bold,]-----)" + $(call __info,) + $(call __info,Generating the JAR file (sources)...) + @$(JAR) $(subst {JAR},$(word 2,$(JAR_NAMES)),$(JARFLAGS)) \ $(wildcard LICENSE) -C $(JAVA_DIR) . \ - -C $(addprefix $(CLASSES_DIR)/,$(notdir $(word 1,$(RESOURCES)))) . \ - -C $(addprefix $(CLASSES_DIR)/,$(notdir $(word 2,$(RESOURCES)))) . + -C $(addprefix $(CLASSES_DIR)/,$(notdir $(RESOURCES_ONLY_DIR))) . endif # __intern_INC_SRC $(call __lines) @@ -144,7 +159,7 @@ build-docs: $(eval JDOCFLAGS += $(FLAGS)) - $(call __info,Generating HTML documentations to '$(realpath $(JAVADOC_OUT))'...) + $(call __info,Generating HTML documentations to '$(abspath $(JAVADOC_OUT))'...) @$(JDOC) $(JDOCFLAGS) $(addprefix -J,$(MEMFLAGS)) $(call __lines) @@ -152,7 +167,6 @@ build-docs: .PHONY: compile package build-docs - ## :::::::::::::::: ## ## Cleaners ## ## :::::::::::::::: ## @@ -167,7 +181,7 @@ ifeq ($(call __is_exist,$(TARGET_DIR)),1) $(call __pr_verbose) $(call __info,) - $(call __info,Deleting '$(realpath $(TARGET_DIR))'...) + $(call __info,Deleting '$(abspath $(TARGET_DIR))'...) @-rm --recursive $(TARGET_DIR) $(if $(__intern_VERBOSE),--verbose,) endif @@ -182,7 +196,7 @@ ifeq ($(call __is_exist,$(CLASSES_DIR)),1) $(call __pr_verbose) $(call __info,) - $(call __info,Deleting '$(realpath $(CLASSES_DIR))'...) + $(call __info,Deleting '$(abspath $(CLASSES_DIR))'...) @-rm --recursive $(CLASSES_DIR) $(if $(__intern_VERBOSE),--verbose,) endif @@ -197,7 +211,7 @@ ifeq ($(call __is_exist,$(JAVADOC_OUT)),1) $(call __pr_verbose) $(call __info,) - $(call __info,Deleting '$(realpath $(JAVADOC_OUT))'...) + $(call __info,Deleting '$(abspath $(JAVADOC_OUT))'...) @-rm --recursive $(JAVADOC_OUT) $(if $(__intern_VERBOSE),--verbose,) endif @@ -218,7 +232,7 @@ $(CLASSES_FILES): $(SOURCES) $(call __pr_verbose) $(call __info,) - $(call __info,Compiling $(words $(SOURCES)) source files to '$(shell pwd)/$(CLASSES_DIR)'...) + $(call __info,Compiling $(words $(SOURCES)) source files to '$(CURDIR)/$(CLASSES_DIR)'...) @$(foreach src,$^,\ echo "$(CLR_PREFIX) $(call __bold,$(notdir $(src))) >>\ $(subst /,.,$(basename $(subst $(JAVA_DIR)/,,$(call __clr,4,$(src)))))$(NORMAL)";\ diff --git a/make/Prop.mk b/make/Prop.mk index 4ece0e54..fd8f6a76 100644 --- a/make/Prop.mk +++ b/make/Prop.mk @@ -1,3 +1,7 @@ +#### --------------- ======================================================= #### +#### :: Prop.mk :: Provides necessary project information and properties #### +#### --------------- ======================================================= #### + # Copyright (c) 2023 Ryuu Mitsuki # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,10 +21,22 @@ ifndef __MAKE_PROP_MK __MAKE_PROP_MK = 1 +RETRIEVER := scripts/retriever.py -PROGNAME := jmatrix -VERSION := 1.5.0-SNAPSHOT -AUTHOR := Ryuu Mitsuki -INCEPTION_YEAR := 2023 +### Retrieve required project information +ifeq ($(call __is_exist,$(RETRIEVER)),1) + # Project information + PROGNAME := $(shell $(PY) $(RETRIEVER) project.name) + VERSION := $(shell $(PY) $(RETRIEVER) project.version) + GROUP_ID := $(shell $(PY) $(RETRIEVER) project.groupId) + ARTIFACT_ID := $(shell $(PY) $(RETRIEVER) project.artifactId) + AUTHOR := $(shell $(PY) $(RETRIEVER) 'project.developers[0].name') + AUTHOR_URL := $(shell $(PY) $(RETRIEVER) 'project.developers[0].url') + INCEPTION_YEAR := $(shell $(PY) $(RETRIEVER) project.inceptionYear) + LICENSE := $(shell $(PY) $(RETRIEVER) 'project.licenses[0].name') + LICENSE_URL := $(shell $(PY) $(RETRIEVER) 'project.licenses[0].url') +else + $(call __raise_err,Fatal,No such file or directory: $(RETRIEVER)) +endif # __is_exist : $(RETRIEVER) endif # __MAKE_PROP_MK diff --git a/make/Setup.mk b/make/Setup.mk index f7558ce9..799606c4 100644 --- a/make/Setup.mk +++ b/make/Setup.mk @@ -22,28 +22,8 @@ ifndef __MAKE_SETUP_MK __MAKE_SETUP_MK = 1 - # Import: Func.mk -include $(or $(MAKE_DIR),$(shell pwd)/make)/Func.mk - -# Import: Prop.mk -include $(or $(MAKE_DIR),$(shell pwd)/make)/Prop.mk - - -# Prevent null or empty values for both version and program name -# If these constants undefined, warn user and then use their default values instead -# -ifndef PROGNAME - $(call __warn,PROGNAME constant are not defined correctly. Overriden by the default value) - PROGNAME := jmatrix -endif # PROGNAME - -ifndef VERSION - $(call __warn,VERSION constant are not defined correctly. Overriden by the default value) - VERSION := x.x.x-SNAPSHOT -endif # VERSION - - +include $(or $(MAKE_DIR),$(CURDIR)/make)/Func.mk ## :::::::::::::::: ## ## User Options ## @@ -56,12 +36,11 @@ LINT ?= false VERBOSE ?= false - ## :::::::::::::::: ## ## Properties ## ## :::::::::::::::: ## -# User could overrides these properties but with caution +# User can overrides these properties but with caution ENCODING ?= UTF-8 SOURCE_JDK ?= 11 @@ -70,7 +49,6 @@ MIN_MEMORY ?= 32m MAX_MEMORY ?= 64m - ## :::::::::::::::: ## ## Constants ## ## :::::::::::::::: ## @@ -81,7 +59,6 @@ ifdef JAVA_HOME JC := $(JAVA_HOME)/bin/javac JDOC := $(JAVA_HOME)/bin/javadoc JAR := $(JAVA_HOME)/bin/jar - else # Otherwise, use `type` command to search the Java commands, # raise an error if the command not found. @@ -99,6 +76,15 @@ else ) endif # JAVA_HOME +# Test the Python command and check its existence +PY ?= $(or $(shell command -v python 2> /dev/null),\ + $(call __raise_err,Fatal,Cannot find the `python' command.\ + Please ensure the Python is installed correctly)\ + ) + +# Import: Prop.mk +# Import this module only after the Python command checker +include $(or $(MAKE_DIR),$(PWD)/make)/Prop.mk ### Flags # Check if these constants has been defined to avoid redefine @@ -123,38 +109,41 @@ ifndef MEMFLAGS MEMFLAGS := -Xms$(MIN_MEMORY) -Xmx$(MAX_MEMORY) endif # MEMFLAGS - ### ::: Directories paths SOURCE_DIR := src/main TEST_DIR := src/test TARGET_DIR := target DOCS_DIR := docs -PYTHON_DIR := $(SOURCE_DIR)/python +BUILDER_DIR := tools/JMBuilder JAVA_DIR := $(SOURCE_DIR)/java RESOURCE_DIR := $(SOURCE_DIR)/resources CLASSES_DIR := $(TARGET_DIR)/classes JAVADOC_OUT := $(DOCS_DIR)/jmatrix-$(VERSION) +BUILDER_OUT := $(TARGET_DIR)/_builder ### ::: Files paths -MANIFEST := META-INF/MANIFEST.MF -MAKE_USAGE := $(addprefix $(DOCS_DIR)/make/,makefile-usage.txcc makefile-usage.txt) +ORIG_MANIFEST := META-INF/MANIFEST.MF +MANIFEST := $(BUILDER_OUT)/$(ORIG_MANIFEST) SOURCES_LIST := $(TARGET_DIR)/generated-list/sourceFiles.lst CLASSES_LIST := $(TARGET_DIR)/generated-list/outputFiles.lst ### ::: Others -PREFIX := [$(PROGNAME)] -CLR_PREFIX := [$(call __clr_br,6,jmatrix)] +PREFIX := [$(ARTIFACT_ID)] +CLR_PREFIX := [$(call __clr_br,6,$(ARTIFACT_ID))] # Check whether the current version is release version, zero if false, otherwise non-zero IS_RELEASE := $(if $(findstring 1,$(words $(subst -, ,$(VERSION)))),1,0) EXCLUDE_PKGS := com.mitsuki.jmatrix.util -JAR_NAMES := $(addprefix $(TARGET_DIR)/, \ - $(PROGNAME)-$(VERSION).jar \ - $(PROGNAME)-$(VERSION)-sources.jar \ +JAR_NAMES := $(addprefix $(TARGET_DIR)/, \ + $(ARTIFACT_ID)-$(VERSION).jar \ + $(ARTIFACT_ID)-$(VERSION)-sources.jar \ ) -# Retrieve all resources directories in "src/main/resources" -RESOURCES := $(wildcard $(RESOURCE_DIR)/*) +# Get all resources in "src/main/resources" +RESOURCES := $(wildcard $(RESOURCE_DIR)/**/*) +RESOURCES_ONLY_DIR := $(wildcard $(RESOURCE_DIR)/*) +ORIG_SETUP_PROPERTIES := $(filter %/setup.properties,$(RESOURCES)) +SETUP_PROPERTIES := $(BUILDER_OUT)/$(notdir $(ORIG_SETUP_PROPERTIES)) ### Private and internal constants @@ -173,20 +162,19 @@ ifndef __intern_INC_SRC __intern_INC_SRC := endif # __intern_INC_SRC +# Get current year using Python datetime module +__current_year := $(shell $(PY) -c "import datetime as date; print(date.datetime.now().year)") # Window title for HTML docs "JMatrix API" -__jdoc_WIN_TITLE := "$(subst jm,JM,$(PROGNAME)) API" - +__jdoc_WIN_TITLE := "$(PROGNAME) API" # Custom Javadoc tags __jdoc_CUSTOM_TAGS := -tag param -tag return -tag throws \ -tag warning:a:"Warning:" -tag author \ -tag license:pt:"License:" -tag see -# Custom bottom page; Must be closed with single quotes! -__jdoc_BOTTOM := 'Copyright (C) $(INCEPTION_YEAR) <a href="https://github.com/mitsuki31">$(AUTHOR)</a>. All rights reserved.' - - +# Custom bottom page; Must be closed with quotes! +__jdoc_BOTTOM := 'Copyright © $(INCEPTION_YEAR) - $(__current_year) <a href="$(AUTHOR_URL)">$(AUTHOR)</a>. All rights reserved.' # Call the `__get_sources` to initialize `SOURCES` variable From f5a768fccd90e92995b1007fd5718a4714edd5e0 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Sun, 7 Jan 2024 23:05:38 +0700 Subject: [PATCH 23/36] refactor: Add imports checker in Makefile to optimize performance Added imports checker when importing the Setup.mk, this way the module will be imported only when the user only when the user specifies targets, excluding 'help', or when specified nothing. This approach optimizes performance. In addition with this commit, refactored the 'help' rule, added 'Makefile' to .PHONY target, and also added license information on the header message. --- Makefile | 58 ++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/Makefile b/Makefile index b369fad5..3627332d 100644 --- a/Makefile +++ b/Makefile @@ -17,21 +17,32 @@ # limitations under the License. # - -MAKE_DIR := $(shell pwd)/make +MAKE_DIR := $(CURDIR)/make SETUP_MAKE := $(MAKE_DIR)/Setup.mk MAIN_MAKE := $(MAKE_DIR)/Main.mk +MAKE_USAGE := $(addprefix docs/make/,makefile-usage.txcc makefile-usage.txt) MKFLAGS := --no-print-directory --silent --file {FILE} CUSTOMGOALS := $(MAKECMDGOALS) - -# Imports -ifneq ($(wildcard $(MAKE_DIR)),) # Check the "make" directory -include $(SETUP_MAKE) -else -$(__raise_err,Fatal,Cannot import neccessary files from "$(MAKE_DIR)". No such a directory) -endif - +# Import: Func.mk +include $(MAKE_DIR)/Func.mk + +### ---------------- NOTE ---------------- ### +# Load 'Setup.mk' only when the user specifies targets, excluding 'help' +# or when nothing is specified (triggering the default 'help' rule). +# This approach optimizes performance, preventing unnecessary module imports, +# notably the time-consuming 'Setup.mk' with its extensive property +# initialization via the builder, especially crucial when users only seek +# to display the help message. +ifneq ($(words $(CUSTOMGOALS)),0) + ifneq ($(filter $(word 1,$(CUSTOMGOALS)),help),help) + ifneq ($(wildcard $(MAKE_DIR)),) # Check the "make" directory + include $(SETUP_MAKE) + else + $(__raise_err,Fatal,Cannot import neccessary files from "$(MAKE_DIR)". No such a directory) + endif # wildcard + endif # filter +endif # words # Enable the linter if '.lint' in command line arguments or # LINT variable are defined and the value is 'true'. @@ -117,8 +128,6 @@ else endif endif - - # Default target rule; If no target rule specified then display the help message help: ifneq ($(or $(RAW),$(NOCC)),true) @@ -127,7 +136,7 @@ ifneq ($(or $(RAW),$(NOCC)),true) # # And must specify `-r` or `--raw` flag to output the raw control-characters. # - @cat $(word 1,$(MAKE_USAGE)) + $(call __help,1) else # There is an exception for this. If user defined a variable called `RAW`, # from the command line then the raw or original version without raw control-characters @@ -136,12 +145,14 @@ else # The command will looks like this: # $ make [help] RAW=true | less # - @cat $(word 2,$(MAKE_USAGE)) + $(call __help,2) endif -.PHONY: help - +.PHONY: Makefile help +### TODO: Import all necessary variables from the Setup module +### so that increasing performance when run the Main module +### and no need to import the Setup module again in the Main module. # Exports export LINT VERBOSE INCLUDE_SRC FLAGS export JCFLAGS JARFLAGS JDOCFLAGS @@ -152,7 +163,7 @@ export MAKE_DIR # Suppress the warning # A variable used to signal whether the Make has been initialized and # currently on running stat. This variable also prevent some messages being printed # when user not specified any target rules or targetting only the `help` target. -__init__ = +__mk_init__ = # Accept any target rules (including undefined ones) # Then send all target rules to another Make file @@ -160,12 +171,13 @@ __init__ = %: # These messages will be printed when __init__ still an empty variable # i.e., does not have any value yet. - $(if $(__init__),, \ - $(eval __init__ = 1) \ - $(info $(CLR_PREFIX) $(call __clr,6,------------------------------------------------------------------------)) \ - $(info $(CLR_PREFIX) $(call __bold,Project): $(call __clr,4,$(PROGNAME)-$(VERSION))) \ - $(info $(CLR_PREFIX) $(call __bold,Author): $(call __clr,4,$(AUTHOR))) \ - $(info $(CLR_PREFIX) $(call __clr,6,------------------------------------------------------------------------)) \ + $(if $(__mk_init__),, \ + $(eval __mk_init__ = 1) \ + $(info $(CLR_PREFIX) $(call __clr,6,------------------------------------------------------------------------)) \ + $(info $(CLR_PREFIX) $(call __bold,Project): $(call __clr,4,$(ARTIFACT_ID)-$(VERSION) $(call __clr,3,($(GROUP_ID):$(ARTIFACT_ID))))) \ + $(info $(CLR_PREFIX) $(call __bold,Author): $(call __clr,4,$(AUTHOR) $(call __clr,3,($(notdir $(AUTHOR_URL)))))) \ + $(info $(CLR_PREFIX) $(call __bold,License): $(call __clr,4,$(LICENSE))) \ + $(info $(CLR_PREFIX) $(call __clr,6,------------------------------------------------------------------------)) \ ) # Skip this code below from the conditional check and allow the custom flags From add3ccfe48d468ad0375e6c94ee41f0b9e5c2888 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Mon, 8 Jan 2024 10:14:44 +0700 Subject: [PATCH 24/36] feat: Add some method for date utilities Introduced two method to convert ISO formatted date to local date: * dateISOToLocal(String, String) * dateISOToLocal(String) --- .../jmatrix/internal/JMatrixUtils.java | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/mitsuki/jmatrix/internal/JMatrixUtils.java b/src/main/java/com/mitsuki/jmatrix/internal/JMatrixUtils.java index cbe3c7fe..42346a11 100644 --- a/src/main/java/com/mitsuki/jmatrix/internal/JMatrixUtils.java +++ b/src/main/java/com/mitsuki/jmatrix/internal/JMatrixUtils.java @@ -30,12 +30,14 @@ import java.util.List; import java.util.Arrays; import java.util.Properties; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; /** * This class provides all neccessary utilities for <b>JMatrix</b> library. * * @author <a href="https://github.com/mitsuki31">Ryuu Mitsuki</a> - * @version 1.6, 12 December 2023 + * @version 1.7, 8 January 2024 * @since 1.0.0b.1 * @license <a href="https://www.apache.org/licenses/LICENSE-2.0"> * Apache License 2.0</a> @@ -333,6 +335,45 @@ public static boolean writeToFile(String filePath, String[ ] contents) { } + ///// --------------------- ///// + /// Date Utilities /// + ///// --------------------- ///// + + /** + * Converts the given date with format of ISO to local format with + * specific format pattern. + * + * @param date a {@code String} representing the ISO date. + * @param formatPattern the format pattern to be used. + * @return the local formatted date. + * + * @since 1.5.0 + * @see #dateISOToLocal(String) + */ + public static String dateISOToLocal(String date, String formatPattern) { + LocalDateTime dateTime = LocalDateTime.parse(date, DateTimeFormatter.ISO_DATE_TIME); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatPattern); + return dateTime.format(formatter); + } + + /** + * Converts the given date with format of ISO to local format with + * default format pattern. + * + * For customizing the format pattern, use the + * {@link #dateISOToLocal(String, String)} method instead. + * The default value of format pattern is {@code "yyyy-MM-dd HH:mm:ss"}. + * + * @param date a {@code String} representing the ISO date. + * @return the local formatted date. + * + * @since 1.5.0 + */ + public static String dateISOToLocal(String date) { + return dateISOToLocal(date, "yyyy-MM-dd HH:mm:ss"); + } + + ///// --------------------- ///// /// Error Options /// ///// --------------------- ///// From 9e733e51ca4be4a856d4c5fec3f71bc23b92294a Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Mon, 8 Jan 2024 17:02:38 +0700 Subject: [PATCH 25/36] refactor: Add the inception year property --- .../resources/configuration/setup.properties | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/resources/configuration/setup.properties b/src/main/resources/configuration/setup.properties index 8ac526e8..c7a1e033 100644 --- a/src/main/resources/configuration/setup.properties +++ b/src/main/resources/configuration/setup.properties @@ -4,11 +4,12 @@ # JMatrix is licensed under the Apache License 2.0 and later. # -JM-Name = ${project.name} -JM-Version = ${project.version} -JM-Url = ${project.url} -JM-GroupId = ${project.groupId} -JM-ArtifactId = ${project.artifactId} -JM-Author = ${project.developers[0].name} -JM-License = ${project.licenses[0].name} -JM-BuildTime = ${maven.build.timestamp} +JM-Name = ${project.name} +JM-Version = ${project.version} +JM-Url = ${project.url} +JM-GroupId = ${project.groupId} +JM-ArtifactId = ${project.artifactId} +JM-Author = ${project.developers[0].name} +JM-InceptionYear = ${project.inceptionYear} +JM-License = ${project.licenses[0].name} +JM-BuildTime = ${maven.build.timestamp} From 9a371444f6d2e90299c4188980c70d0e91ab632b Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Mon, 8 Jan 2024 18:07:46 +0700 Subject: [PATCH 26/36] refactor: Add some new information on message * Updated the copyright year * Added the built time on version information --- .../mitsuki/jmatrix/internal/MainClass.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java b/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java index 149e959c..ff4047ee 100644 --- a/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java +++ b/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java @@ -28,6 +28,7 @@ import java.util.Properties; import java.util.Set; import java.lang.Iterable; +import java.time.Year; /** @@ -39,7 +40,7 @@ * {@code Main} to {@code MainClass}. * * @author <a href="https://github.com/mitsuki31">Ryuu Mitsuki</a> - * @version 1.5, 12 December 2023 + * @version 1.6, 8 January 2024 * @since 1.0.0b.1 * @see com.mitsuki.jmatrix.Matrix * @license <a href="https://www.apache.org/licenses/LICENSE-2.0"> @@ -87,6 +88,9 @@ public static void main(String[] args) { final String license = setupProperties.getProperty("JM-License"); final String groupId = setupProperties.getProperty("JM-GroupId"); final String artifactId = setupProperties.getProperty("JM-ArtifactId"); + final String inceptionYear = setupProperties.getProperty("JM-InceptionYear"); + final String buildTime = JMatrixUtils.dateISOToLocal( + setupProperties.getProperty("JM-BuildTime"), "MMM dd, yyyy HH:mm:ss"); // Parse the command line arguments, removing all duplicate arguments List<String> parsedArgs = new ArgumentsParser<String>(args) @@ -105,16 +109,18 @@ public static void main(String[] args) { // Check for version arguments if (versionArgs.contains(firstArg)) { sb.append(String.format("%s v%s", name, version)) - .append(String.format(" <%s:%s>", groupId, artifactId)); + .append(String.format(" <%s:%s>", groupId, artifactId)) + .append(String.format("%sBuilt on %s UTC", newline, buildTime)); // Check for copyright arguments } else if (copyrightArgs.contains(firstArg)) { - sb.append(String.format("%s - Copyright (C) 2023 %s%s", - name, author, newline)) + int currentYear = Year.now().getValue(); + sb.append(String.format("%s - Copyright (C) %s - %d %s%s", + name, inceptionYear, currentYear, author, newline)) .append(String.format("Licensed under the %s", license)); // Check for help arguments } else if (helpArgs.contains(firstArg)) { printHelpMessage(); - } else if (firstArg.equals("--version-only")) { + } else if (firstArg != null && firstArg.equals("--version-only")) { sb.append(version); } @@ -180,7 +186,7 @@ private static void printHelpMessage() { .append(" ver, version, -V, --version" + newline) .append(" Print the current version of JMatrix." + dblNewline) .append(" copyright, -cr, --copyright" + newline) - .append(" Print the copyright and license." + dblNewline) + .append(" Print the copyright and license information." + dblNewline) .append(" ?, help, -h, --help" + newline) .append(" Print this help message." + dblNewline) .append("ISSUE:" + newline) From 10a4f224f42e2d50a2712f9b8a6c8a07a592a767 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Mon, 8 Jan 2024 22:41:56 +0700 Subject: [PATCH 27/36] refactor: Update and refine the help message --- .../mitsuki/jmatrix/internal/MainClass.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java b/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java index ff4047ee..f662de8d 100644 --- a/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java +++ b/src/main/java/com/mitsuki/jmatrix/internal/MainClass.java @@ -184,15 +184,25 @@ private static void printHelpMessage() { .append(" java -jar path/to/jmatrix.jar [options]" + dblNewline) .append("OPTIONS:" + newline) .append(" ver, version, -V, --version" + newline) - .append(" Print the current version of JMatrix." + dblNewline) + .append( + " Print the current version information of JMatrix." + newline + + " Use `--version-only` to print the number version only." + + dblNewline + ) .append(" copyright, -cr, --copyright" + newline) .append(" Print the copyright and license information." + dblNewline) .append(" ?, help, -h, --help" + newline) .append(" Print this help message." + dblNewline) - .append("ISSUE:" + newline) + .append("ISSUES:" + newline) .append(String.format( - " Want to report some issues? Let's help us improve JMatrix.%s" + - " <https://github.com/mitsuki31/jmatrix/issues/new/choose>", + " Having some bugs or having any issues with algorithm of matrix%s" + + " operations? Please report to the following link.%s" + + " https://github.com/mitsuki31/jmatrix/issues/new/choose%s", + newline, dblNewline, dblNewline + )) + .append(String.format( + " Or if you want to contribute as well, you can create your own%s" + + " pull request.", newline )); From 4d44f4ce97ff17462edea63d37cbc015c9b79395 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Tue, 9 Jan 2024 00:20:32 +0700 Subject: [PATCH 28/36] Refactor and update the info message * Updated and improved several info message for clarity * Refactored all custom flags to use ";" instead of "@:" to define empty rule * Now the 'clean' target will use all cleaners, this to make the output directory of generated HTML docs is also completely deleted during cleaning the project --- make/Main.mk | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/make/Main.mk b/make/Main.mk index eaef4a3c..12b9f274 100644 --- a/make/Main.mk +++ b/make/Main.mk @@ -110,7 +110,7 @@ package: compile | $(SOURCES) $(ORIG_MANIFEST) $(ORIG_SETUP_PROPERTIES) $(RESOUR $(eval JARFLAGS := $(subst {MANIFEST},$(MANIFEST),$(JARFLAGS)) $(FLAGS)) - $(call __info,Copying $(words $(RESOURCES)) resources to '$(CURDIR)/$(CLASSES_DIR)'...) + $(call __info,Copying $(words $(RESOURCES)) resources to '$(abspath $(CLASSES_DIR))'...) @cp --recursive --preserve=all $(RESOURCES_ONLY_DIR) $(CLASSES_DIR) \ $(if $(__intern_VERBOSE),--verbose,) $(call __info,) @@ -120,7 +120,7 @@ package: compile | $(SOURCES) $(ORIG_MANIFEST) $(ORIG_SETUP_PROPERTIES) $(RESOUR PYTHONPATH="$(BUILDER_DIR):$PYTHONPATH" $(PY) -m jmbuilder --fix-properties \ pom.xml $(ORIG_SETUP_PROPERTIES) $(SETUP_PROPERTIES) # Overwrite the existing 'setup.properties' file in 'target/classes' directory - @cp --preserve=all $(SETUP_PROPERTIES) $(CLASSES_DIR)/$(subst $(wildcard $(RESOURCE_DIR)),,$(dir $(ORIG_SETUP_PROPERTIES))) + @cp --preserve=all $(SETUP_PROPERTIES) $(CLASSES_DIR)/$(subst $(abspath $(RESOURCE_DIR)),,$(abspath $(dir $(ORIG_SETUP_PROPERTIES)))) # Fix 'MANIFEST.MF' $(call __info,Initiating '$(notdir $(MANIFEST))' file...) @@ -132,8 +132,11 @@ package: compile | $(SOURCES) $(ORIG_MANIFEST) $(ORIG_SETUP_PROPERTIES) $(RESOUR $(call __info,) $(call __info,Generating the JAR file...) - @$(JAR) $(subst {JAR},$(word 1,$(JAR_NAMES)),$(JARFLAGS)) \ + @$(JAR) $(subst {JAR},$(firstword $(JAR_NAMES)),$(JARFLAGS)) \ $(wildcard LICENSE) -C $(CLASSES_DIR) . + # Reusable variable for info message when generation JAR file(s) done successfully + $(eval ._done_msg = $(call __clr,2,Sucessfully generated.) Stored in '{JAR}') + $(call __info,$(subst {JAR},$(abspath $(firstword $(JAR_NAMES))),$(._done_msg))) # Create the second JAR file containing the source files only, # if and only if the INCLUDE_SRC option is "true" (enabled) @@ -142,10 +145,12 @@ ifeq ($(__intern_INC_SRC),1) @echo "$(CLR_PREFIX) $(call __bold,-----[) $(call __clr,2,$@:sources) $(call __bold,::) $(call __clr_br,4,$(ARTIFACT_ID)) $(call __bold,]-----)" $(call __info,) $(call __info,Generating the JAR file (sources)...) - @$(JAR) $(subst {JAR},$(word 2,$(JAR_NAMES)),$(JARFLAGS)) \ + @$(JAR) $(subst {JAR},$(lastword $(JAR_NAMES)),$(JARFLAGS)) \ $(wildcard LICENSE) -C $(JAVA_DIR) . \ -C $(addprefix $(CLASSES_DIR)/,$(notdir $(RESOURCES_ONLY_DIR))) . + $(call __info,$(subst {JAR},$(abspath $(lastword $(JAR_NAMES))),$(._done_msg))) endif # __intern_INC_SRC + $(eval undefine ._done_msg) $(call __lines) @@ -159,9 +164,10 @@ build-docs: $(eval JDOCFLAGS += $(FLAGS)) - $(call __info,Generating HTML documentations to '$(abspath $(JAVADOC_OUT))'...) + $(call __info,Generating HTML documentation to '$(abspath $(JAVADOC_OUT))'...) @$(JDOC) $(JDOCFLAGS) $(addprefix -J,$(MEMFLAGS)) + $(call __info,$(call __clr,2,Sucessfully generated.) Stored in '$(abspath $(JAVADOC_OUT))') $(call __lines) .PHONY: compile package build-docs @@ -172,7 +178,7 @@ build-docs: ## :::::::::::::::: ## -clean: +clean: cleanbin cleandocs $(call __job_msg,$@) # Clean the "target" (which is the output directory for compiled classes @@ -185,7 +191,7 @@ ifeq ($(call __is_exist,$(TARGET_DIR)),1) @-rm --recursive $(TARGET_DIR) $(if $(__intern_VERBOSE),--verbose,) endif - @echo "$(CLR_PREFIX) $(call __clr_br,2,ALL CLEAN)" + $(call __info,$(call __clr_br,2,ALL CLEAN)) $(call __lines) @@ -200,7 +206,7 @@ ifeq ($(call __is_exist,$(CLASSES_DIR)),1) @-rm --recursive $(CLASSES_DIR) $(if $(__intern_VERBOSE),--verbose,) endif - @echo "$(CLR_PREFIX) $(call __clr_br,2,ALL CLEAN)" + $(call __info,$(call __clr_br,2,Compiled classes cleaned.)) $(call __lines) @@ -215,7 +221,7 @@ ifeq ($(call __is_exist,$(JAVADOC_OUT)),1) @-rm --recursive $(JAVADOC_OUT) $(if $(__intern_VERBOSE),--verbose,) endif - @echo "$(CLR_PREFIX) $(call __clr_br,2,ALL CLEAN)" + $(call __info,$(call __clr_br,2,HTML documentation cleaned.)) $(call __lines) .PHONY: clean cleanbin cleandocs @@ -232,13 +238,14 @@ $(CLASSES_FILES): $(SOURCES) $(call __pr_verbose) $(call __info,) - $(call __info,Compiling $(words $(SOURCES)) source files to '$(CURDIR)/$(CLASSES_DIR)'...) + $(call __info,Compiling $(words $(SOURCES)) source files to '$(abspath $(CLASSES_DIR))'...) @$(foreach src,$^,\ echo "$(CLR_PREFIX) $(call __bold,$(notdir $(src))) >>\ $(subst /,.,$(basename $(subst $(JAVA_DIR)/,,$(call __clr,4,$(src)))))$(NORMAL)";\ ) @$(JC) -d $(CLASSES_DIR) $^ $(JCFLAGS) $(addprefix -J,$(MEMFLAGS)) + $(call __info,$(call __clr,2,All compiled.)) $(call __lines) @@ -271,10 +278,7 @@ $(CLASSES_FILES): $(SOURCES) # Alternative usage: # make <TARGET> LINT=true # -.lint: -# The `@:` does nothing, but to prevent the message from Make get printed -# Message: "Nothing to be done for '<TARGET>'" - @: +.lint: ; # Enables the verbose output @@ -285,10 +289,7 @@ $(CLASSES_FILES): $(SOURCES) # Alternative usage: # make <TARGET> VERBOSE=true # -.verbose: -# The `@:` does nothing, but to prevent the message from Make get printed -# Message: "Nothing to be done for '<TARGET>'" - @: +.verbose: ; # Includes the source files to be archived @@ -307,7 +308,4 @@ $(CLASSES_FILES): $(SOURCES) # Alternative usage: # make <TARGET> INCLUDE_SRC=true # -.include-src: -# The `@:` does nothing, but to prevent the message from Make get printed -# Message: "Nothing to be done for '<TARGET>'" - @: +.include-src: ; From f596e91faab92e364749792d563617a6bfa0b94a Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Tue, 9 Jan 2024 09:10:38 +0700 Subject: [PATCH 29/36] docs: Update and improve the docs of `dateISOToLocal` --- .../jmatrix/internal/JMatrixUtils.java | 59 +++++++++++++------ 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/mitsuki/jmatrix/internal/JMatrixUtils.java b/src/main/java/com/mitsuki/jmatrix/internal/JMatrixUtils.java index 42346a11..72ea3248 100644 --- a/src/main/java/com/mitsuki/jmatrix/internal/JMatrixUtils.java +++ b/src/main/java/com/mitsuki/jmatrix/internal/JMatrixUtils.java @@ -32,6 +32,7 @@ import java.util.Properties; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; /** * This class provides all neccessary utilities for <b>JMatrix</b> library. @@ -340,15 +341,29 @@ public static boolean writeToFile(String filePath, String[ ] contents) { ///// --------------------- ///// /** - * Converts the given date with format of ISO to local format with - * specific format pattern. - * - * @param date a {@code String} representing the ISO date. - * @param formatPattern the format pattern to be used. - * @return the local formatted date. - * - * @since 1.5.0 - * @see #dateISOToLocal(String) + * Converts a date and time string in ISO 8601 format to a localized string + * using the specified format pattern. + * + * <p><b>Note:</b></p> + * <p>This method is thread-safe, but does not handle null inputs gracefully. + * If {@code date} or {@code formatPattern} is {@code null}, a + * {@code NullPointerException} will be thrown. + * + * @param date The date and time string in ISO 8601 + * format (e.g., "2024-01-08T21:53:00Z"). + * @param formatPattern The format pattern to use for the + * localized date and time string + * (e.g., "dd/MM/yyyy HH:mm:ss"). + * @return The localized date and time string in + * the specified format. + * + * @throws DateTimeParseException If the input {@code date} string cannot be parsed + * using the ISO 8601 formatter. + * @throws NullPointerException If {@code null} are known on {@code date} or + * {@code formatPattern} argument. + * + * @since 1.5.0 + * @see #dateISOToLocal(String) */ public static String dateISOToLocal(String date, String formatPattern) { LocalDateTime dateTime = LocalDateTime.parse(date, DateTimeFormatter.ISO_DATE_TIME); @@ -357,17 +372,27 @@ public static String dateISOToLocal(String date, String formatPattern) { } /** - * Converts the given date with format of ISO to local format with - * default format pattern. + * Converts a date and time string in ISO 8601 format to a localized string + * using the {@code yyyy-MM-dd HH:mm:ss} format pattern. + * + * <p>For customizing the format pattern, use the + * {@link #dateISOToLocal(String, String)} method instead. The format pattern + * will use format {@code yyyy-MM-dd HH:mm:ss}. + * + * <p><b>Note:</b></p> + * <p>This method is thread-safe, but does not handle null inputs gracefully. + * If {@code date} is {@code null}, a {@code NullPointerException} will be thrown. * - * For customizing the format pattern, use the - * {@link #dateISOToLocal(String, String)} method instead. - * The default value of format pattern is {@code "yyyy-MM-dd HH:mm:ss"}. + * @param date The date and time string in ISO 8601 + * format (e.g., "2024-01-08T21:53:00Z"). + * @return The localized date and time string in + * the {@code yyyy-MM-dd HH:mm:ss} format. * - * @param date a {@code String} representing the ISO date. - * @return the local formatted date. + * @throws DateTimeParseException If the input {@code date} string cannot be parsed + * using the ISO 8601 formatter. + * @throws NullPointerException If {@code null} are known on {@code date} argument. * - * @since 1.5.0 + * @since 1.5.0 */ public static String dateISOToLocal(String date) { return dateISOToLocal(date, "yyyy-MM-dd HH:mm:ss"); From 464f09f66457ab5b7821f55e31b5ff67fe5511a5 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Wed, 10 Jan 2024 12:20:19 +0700 Subject: [PATCH 30/36] fix: Replace absolute to relative path for imports efficiency --- Makefile | 17 ++++++++++------- make/Main.mk | 2 +- make/Setup.mk | 4 ++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 3627332d..3147dc9a 100644 --- a/Makefile +++ b/Makefile @@ -17,13 +17,20 @@ # limitations under the License. # -MAKE_DIR := $(CURDIR)/make +# It is NOT RECOMMENDED to use the absolute path, Make are ineffectively +# handles directories and files with spaces, it will treats them as a list of values +# rather than a single path of directory. +MAKE_DIR := make SETUP_MAKE := $(MAKE_DIR)/Setup.mk MAIN_MAKE := $(MAKE_DIR)/Main.mk MAKE_USAGE := $(addprefix docs/make/,makefile-usage.txcc makefile-usage.txt) MKFLAGS := --no-print-directory --silent --file {FILE} CUSTOMGOALS := $(MAKECMDGOALS) +ifeq ($(wildcard $(MAKE_DIR)),) + $(error Cannot import internal modules from '$(abspath $(MAKE_DIR))') +endif # wildcard : $(MAKE_DIR) + # Import: Func.mk include $(MAKE_DIR)/Func.mk @@ -35,12 +42,8 @@ include $(MAKE_DIR)/Func.mk # initialization via the builder, especially crucial when users only seek # to display the help message. ifneq ($(words $(CUSTOMGOALS)),0) - ifneq ($(filter $(word 1,$(CUSTOMGOALS)),help),help) - ifneq ($(wildcard $(MAKE_DIR)),) # Check the "make" directory - include $(SETUP_MAKE) - else - $(__raise_err,Fatal,Cannot import neccessary files from "$(MAKE_DIR)". No such a directory) - endif # wildcard + ifneq ($(firstword $(CUSTOMGOALS)),help) + include $(SETUP_MAKE) endif # filter endif # words diff --git a/make/Main.mk b/make/Main.mk index 12b9f274..71c9b16e 100644 --- a/make/Main.mk +++ b/make/Main.mk @@ -21,7 +21,7 @@ # Import: Setup.mk ### TODO: Remove this when all necessary variables has correctly ### imported from the Makefile within the project's root directory. -include $(or $(MAKE_DIR),$(CURDIR)/make)/Setup.mk +include $(or $(MAKE_DIR),make)/Setup.mk ## :::::::::::::::: ## diff --git a/make/Setup.mk b/make/Setup.mk index 799606c4..a6a82c69 100644 --- a/make/Setup.mk +++ b/make/Setup.mk @@ -23,7 +23,7 @@ ifndef __MAKE_SETUP_MK __MAKE_SETUP_MK = 1 # Import: Func.mk -include $(or $(MAKE_DIR),$(CURDIR)/make)/Func.mk +include $(or $(MAKE_DIR),make)/Func.mk ## :::::::::::::::: ## ## User Options ## @@ -84,7 +84,7 @@ PY ?= $(or $(shell command -v python 2> /dev/null),\ # Import: Prop.mk # Import this module only after the Python command checker -include $(or $(MAKE_DIR),$(PWD)/make)/Prop.mk +include $(or $(MAKE_DIR),make)/Prop.mk ### Flags # Check if these constants has been defined to avoid redefine From 533f6fe0df6e7ecf2ff3c130dbede6bb9b8284d9 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Wed, 10 Jan 2024 20:26:19 +0700 Subject: [PATCH 31/36] refactor: Update the excluded packages list Replaced the "com.mitsuki.jmatrix.util" with "com.mitsuki.jmatrix.internal". And also changed the assignment operator for all internal options. --- Makefile | 23 ++++++++++++----------- make/Setup.mk | 14 +++++++------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index 3147dc9a..20c8c3fd 100644 --- a/Makefile +++ b/Makefile @@ -56,21 +56,21 @@ endif # words # It is equivalent with: # $ make <TARGET> LINT=true # -ifeq ($(filter $(CUSTOMGOALS),.lint),.lint) +ifeq ($(findstring .lint,$(CUSTOMGOALS)),.lint) CUSTOMGOALS := $(filter-out .lint,$(CUSTOMGOALS)) JCFLAGS += -Xlint:all -Xdoclint:all JDOCFLAGS += -Xdoclint:all LINT := true - __intern_LINT := 1 + __intern_LINT = 1 else # Enable the linter with all checks if LINT is true ifeq ($(LINT),true) JCFLAGS += -Xlint:all -Xdoclint:all JDOCFLAGS += -Xdoclint:all - __intern_LINT := 1 + __intern_LINT = 1 else JCFLAGS += -Xlint:deprecation,unchecked,cast -Xdoclint:html,syntax/protected JDOCFLAGS += -Xdoclint:html,syntax @@ -87,7 +87,7 @@ endif # It is equivalent with: # $ make <TARGET> VERBOSE=true # -ifeq ($(filter $(CUSTOMGOALS),.verbose),.verbose) +ifeq ($(findstring .verbose,$(CUSTOMGOALS)),.verbose) CUSTOMGOALS := $(filter-out .verbose,$(CUSTOMGOALS)) JCFLAGS += -verbose @@ -95,7 +95,7 @@ ifeq ($(filter $(CUSTOMGOALS),.verbose),.verbose) JDOCFLAGS += -verbose VERBOSE := true - __intern_VERBOSE := 1 + __intern_VERBOSE = 1 else # Enable the verbose if VERBOSE is true ifeq ($(VERBOSE),true) @@ -103,7 +103,7 @@ else JARFLAGS += --verbose JDOCFLAGS += -verbose - __intern_VERBOSE := 1 + __intern_VERBOSE = 1 else JDOCFLAGS += -quiet endif @@ -120,18 +120,19 @@ endif # It is equivalent with: # $ make <TARGET> INCLUDE_SRC=true # -ifeq ($(filter $(CUSTOMGOALS),.include-src),.include-src) - CUSTOMGOALS := $(filter-out .include-src,$(CUSTOMGOALS))) +ifeq ($(findstring .include-src,$(CUSTOMGOALS)),.include-src) + CUSTOMGOALS := $(filter-out .include-src,$(CUSTOMGOALS)) INCLUDE_SRC := true - __intern_INC_SRC := 1 + __intern_INC_SRC = 1 else ifeq ($(INCLUDE_SRC),true) - __intern_INC_SRC := 1 + __intern_INC_SRC = 1 endif endif -# Default target rule; If no target rule specified then display the help message +# Default target rule; If no target rule specified then display the help message. +.DEFAULT_GOAL: help help: ifneq ($(or $(RAW),$(NOCC)),true) # User could pipe this (`cat`) command to `less` command with: diff --git a/make/Setup.mk b/make/Setup.mk index a6a82c69..8d8c03df 100644 --- a/make/Setup.mk +++ b/make/Setup.mk @@ -133,7 +133,7 @@ CLR_PREFIX := [$(call __clr_br,6,$(ARTIFACT_ID))] # Check whether the current version is release version, zero if false, otherwise non-zero IS_RELEASE := $(if $(findstring 1,$(words $(subst -, ,$(VERSION)))),1,0) -EXCLUDE_PKGS := com.mitsuki.jmatrix.util +EXCLUDE_PKGS := com.mitsuki.jmatrix.internal JAR_NAMES := $(addprefix $(TARGET_DIR)/, \ $(ARTIFACT_ID)-$(VERSION).jar \ $(ARTIFACT_ID)-$(VERSION)-sources.jar \ @@ -149,18 +149,18 @@ SETUP_PROPERTIES := $(BUILDER_OUT)/$(notdir $(ORIG_SETUP_PROPERTIES)) # Lint option ifndef __intern_LINT - __intern_LINT := -endif # __intern_LINT + __intern_LINT = +endif # __intern_LINT # Verbose option ifndef __intern_VERBOSE - __intern_VERBOSE := -endif # __intern_VERBOSE + __intern_VERBOSE = +endif # __intern_VERBOSE # Include sources option ifndef __intern_INC_SRC - __intern_INC_SRC := -endif # __intern_INC_SRC + __intern_INC_SRC = +endif # __intern_INC_SRC # Get current year using Python datetime module __current_year := $(shell $(PY) -c "import datetime as date; print(date.datetime.now().year)") From 813fafb89827cb92b4a6d40f5e996cfd8de12c75 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Thu, 11 Jan 2024 12:07:37 +0700 Subject: [PATCH 32/36] refactor: Delete and add some properties We also updated the excluded packages to "com.mitsuki.jmatrix.internal". --- pom.xml | 31 +++++-------------------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/pom.xml b/pom.xml index 76a3009a..03af13f4 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,6 @@ <packaging>jar</packaging> <version>1.5.0-SNAPSHOT</version> - <name>JMatrix</name> <description>JMatrix is a Java library to build matrix with ease. Created by Ryuu Mitsuki.</description> <url>https://github.com/mitsuki31/jmatrix.git</url> @@ -24,7 +23,6 @@ </license> </licenses> - <!-- Developers & Maintainers --> <developers> <developer> @@ -34,7 +32,6 @@ </developer> </developers> - <!-- Issues & CI --> <issueManagement> <system>github</system> @@ -45,19 +42,14 @@ <url>https://github.com/mitsuki31/jmatrix/actions</url> </ciManagement> - <!-- Properties --> <properties> - <author.name>Ryuu Mitsuki</author.name> - <package.name>JMatrix</package.name> - <package.version.core>1.2.2</package.version.core> + <author.name>${project.developers[0].name}</author.name> <package.mainClass>com.mitsuki.jmatrix.internal.MainClass</package.mainClass> - <package.releaseType>stable</package.releaseType> - <package.betaNum>0</package.betaNum> <package.licenseFile>LICENSE</package.licenseFile> + <excludePackages>com.mitsuki.jmatrix.internal</excludePackages> - - <!-- Users could changes these properties if neccessary, but with caution --> + <!-- Users can changes these properties if neccessary, but with caution --> <jdkVersion>11</jdkVersion> <sourceJdk>${jdkVersion}</sourceJdk> <targetJdk>${jdkVersion}</targetJdk> @@ -70,7 +62,6 @@ <minmemory>32m</minmemory> <maxmemory>64m</maxmemory> - <!-- Paths --> <paths.manifestFile>${basedir}/META-INF/MANIFEST.MF</paths.manifestFile> @@ -82,7 +73,6 @@ <paths.testClassesDir>${project.build.testOutputDirectory}</paths.testClassesDir> <paths.docsOutputDir>${paths.docsDir}/jmatrix-${project.version}</paths.docsOutputDir> - <!-- Encoding --> <project.build.sourceEncoding>${_encoding}</project.build.sourceEncoding> <project.reporting.outputEncoding>${_encoding}</project.reporting.outputEncoding> @@ -91,7 +81,6 @@ <maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format> </properties> - <!-- Dependencies --> <dependencies> <dependency> @@ -102,11 +91,9 @@ </dependency> </dependencies> - <build> <!-- Plugins --> <plugins> - <!-- Maven Compiler --> <plugin> <groupId>org.apache.maven.plugins</groupId> @@ -202,16 +189,13 @@ <bottom> <![CDATA[ - Copyright (C) {inceptionYear} <a href="https://github.com/mitsuki31">${author.name}</a>. All rights reserved. + Copyright © {inceptionYear} - 2024 <a href="${project.developers[0].url}">${author.name}</a>. All rights reserved. ]]> </bottom> <!-- Dependencies --> <includeDependencySources>true</includeDependencySources> - <excludePackageNames> - <!-- Exclude com.mitsuki.jmatrix.util and its subpackages --> - com.mitsuki.jmatrix.util - </excludePackageNames> + <excludePackageNames>${excludePackages}</excludePackageNames> <!-- Custom Tags --> <tags> @@ -257,14 +241,12 @@ </executions> </plugin> - <!-- Maven Source --> <plugin> <artifactId>maven-source-plugin</artifactId> <version>2.4</version> </plugin> - <!-- Maven JAR --> <plugin> <groupId>org.apache.maven.plugins</groupId> @@ -284,7 +266,6 @@ </configuration> </plugin> - <!-- Maven Surefire --> <plugin> <groupId>org.apache.maven.plugins</groupId> @@ -294,7 +275,6 @@ </plugins> </build> - <reporting> <plugins> <plugin> @@ -318,7 +298,6 @@ </plugins> </reporting> - <profiles> <profile> <id>include-src</id> From 61456365c62fcc8ab45aab0abfbbb37492dc9868 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <117973493+mitsuki31@users.noreply.github.com> Date: Thu, 11 Jan 2024 12:08:57 +0700 Subject: [PATCH 33/36] ci: Reactivate and improve the Test workflow * Now both tests (Maven and Make) will runs on 3 platforms, they are Ubuntu, Windows, and MacOS. * Refactored and improved jobs setup * Updated several dependencies to the latest major version * Several changes and improvements --- .github/workflows/tests.yml | 216 +++++++++++++++++++----------------- 1 file changed, 113 insertions(+), 103 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2a71e80c..1afc8012 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,12 +1,12 @@ -name: Project Tester +name: Test on: # Run on push, pull request, and manual trigger push: # Only run when the specific files are changed paths: - - 'src/**/*.java' # Java files - - 'src/**/*.py' # Python files + - '**/*.java' # Java files + - '**/*.py' # Python files # Unlike push, the workflow always runs on pull requests pull_request: @@ -20,139 +20,149 @@ on: required: false type: boolean +# Environment variables definitions +env: + ## For Java installation + java-dist: temurin + + ## For Python installation + arch: x64 + + ## Other environments + debug: ${{ inputs.debug }} + deps: requirements.txt + jobs: # ::---:: Maven Test ::---:: # maven-test: - name: Maven Test / ${{ matrix.os }} + name: Maven Test / ${{ matrix.os }} / ${{ matrix.java-ver }} runs-on: ${{ matrix.os }}-latest env: - java-ver: 11 - java-dist: temurin - DEBUG: ${{ inputs.debug }} + # Maven's debug flag (`-X`) + mvnDebugFlag: ${{ env.debug == true && '-X' || '' }} strategy: + # Set to maximum number of processes to speed up jobs run + max-parallel: 6 matrix: - os: [Ubuntu, Windows] + os: [Ubuntu, Windows, macOS] + java-ver: [11, latest] # JDK 11 & latest steps: # Checkout repository - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Caching Maven deps - name: Cache Maven dependencies id: cache-maven - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + key: ${{ runner.os }}-jdk-${{ matrix.java-ver }}-${{ env.java-dist }}-maven-${{ hashFiles('**/pom.xml') }} restore-keys: | - ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - ${{ runner.os }}-maven- + ${{ runner.os }}-jdk-${{ matrix.java-ver }}-${{ env.java-dist }}-maven-${{ hashFiles('**/pom.xml') }} + ${{ runner.os }}-jdk-${{ matrix.java-ver }}-${{ env.java-dist }}-maven- # Setup Java - - name: Setup Java / ${{ matrix.os }} - uses: actions/setup-java@v3 + - name: Setup Java / ${{ matrix.os }} / ${{ matrix.java-ver }} + uses: actions/setup-java@v4 with: - java-version: ${{ env.java-ver }} + java-version: ${{ matrix.java-ver }} distribution: ${{ env.java-dist }} # Install deps - name: Install dependencies - if: ${{ steps.cache-maven.outputs.cache-hit != true && env.DEBUG != true }} - run: mvn clean install -DskipTests - - - name: Install dependencies (Debug) - if: ${{ steps.cache-maven.outputs.cache-hit != true && env.DEBUG == true }} - run: mvn clean install -DskipTests -X + if: ${{ steps.cache-maven.outputs.cache-hit != true }} + run: mvn clean install -DskipTests ${{ env.mvnDebugFlag }} + shell: bash # Packaging and testing - - name: Packaging and testing - if: ${{ env.DEBUG != true }} - run: mvn test package -P include-src + - name: Packaging the project + run: mvn package -P include-src ${{ env.mvnDebugFlag }} + shell: bash - - name: Package source (Debug) - if: ${{ env.DEBUG == true }} - run: mvn test package -P include-src -X + # Build the docs + - name: Build the HTML docs + run: mvn site -P lint ${{ env.mvnDebugFlag }} + shell: bash # Clean up - name: Clean up the project - run: mvn clean + run: | + mvn clean ${{ env.mvnDebugFlag }} + [ -d docs/jmatrix-* ] && rm --recursive --force docs/jmatrix-* + shell: bash # ::---:: Make Test ::---:: # -# make-test: -# name: Make Test -# runs-on: ubuntu-latest -# continue-on-error: true -# -# strategy: -# matrix: -# py-ver: ['3.7', '3.x'] -# -# env: -# arch: x64 -# DEPS_FILE: 'requirements.txt' -# DEBUG: ${{ inputs.debug }} -# -# steps: -# # Checkout -# - name: Checkout repository -# uses: actions/checkout@v3 -# -# # Setup Python -# - name: Setup Python ${{ matrix.py-ver }} -# id: setup-py -# uses: actions/setup-python@v3 -# with: -# python-version: ${{ matrix.py-ver }} -# architecture: ${{ env.arch }} -# cache: 'pip' -# cache-dependency-path: '**/${{ env.DEPS_FILE }}' -# -# # Install deps -# - name: Install dependencies -# if: ${{ steps.setup-py.outputs.cache-hit != true }} -# run: | -# if [ $DEBUG = 'true' ]; then -# python -m pip install -r $DEPS_FILE --debug -# else -# python -m pip install -r $DEPS_FILE -# fi -# -# # Sadly, Make cannot tests the project thoroughly due to unavailability -# # of necessary packages (e.g "org.junit"), so here it just tests -# # the project on compiling, packaging, and generating docs. -# -# # Compile -# - name: Compile the project -# run: | -# [ -d target/classes ] && make clean -# make compile VERBOSE=$DEBUG LINT=true -# -# # Package -# - name: Packaging the project -# run: | -# make package VERBOSE=$DEBUG -# -# - name: Packaging the project (with source) -# run: | -# make package INCLUDE-SRC=true VERBOSE=$DEBUG -# -# # Build docs -# - name: Build the docs -# run: | -# # Build docs -# # For more information on debugging, we prefer to change it -# # to "all" mode. -# if [ $DEBUG = 'true' ]; then -# make build-docs VERBOSE=all -# else -# make build-docs -# fi -# -# # Clean up -# - name: Clean up the project -# run: | -# [ -d target ] && echo "Clean the project" && make clean + make-test: + name: Make Test / ${{ matrix.os }} / ${{ matrix.py-ver }} + runs-on: ${{ matrix.os }}-latest + + env: + MAKE: ${{ env.debug == true && 'make -d' || 'make' }} + + strategy: + # Set to maximum number of processes to speed up jobs run + max-parallel: 6 + matrix: + os: [Ubuntu, Windows, macOS] + py-ver: [3.7, 3.x] # Python 3.7 & latest + + steps: + # Checkout + - name: Checkout repository + uses: actions/checkout@v4 + + # Setup Python + - name: Setup Python / ${{ matrix.os }} / ${{ matrix.py-ver }} + id: setup-py + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.py-ver }} + architecture: ${{ env.arch }} + cache: pip + cache-dependency-path: '**/${{ env.deps }}' + + # Install deps + - name: Install dependencies + id: init + if: ${{ steps.setup-py.outputs.cache-hit != true }} + run: | + if [ $debug = 'true' ]; then + python -m pip install -r $(git ls-files **/$deps) --debug + else + python -m pip install -r $(git ls-files **/$deps) + fi + shell: bash + + # Sadly, Make cannot tests the project thoroughly due to unavailability + # of necessary packages (e.g "org.junit"), so here it just tests + # the project on compiling, packaging, and generating docs. + + # Compile + - name: Compile the project + needs: init + run: | + [ -d target ] && make clean > /dev/null + $MAKE compile LINT=true VERBOSE=$debug + shell: bash + + # Package + - name: Packaging the project + run: $MAKE package INCLUDE_SRC=true VERBOSE=$debug + shell: bash + + # Build docs + - name: Build the HTML docs + # For more information on debugging, it will implictly set the + # verbose mode to 'all' when the VERBOSE is true. + run: $MAKE build-docs LINT=true VERBOSE=$debug + shell: bash + + # Clean up + - name: Clean up the project + run: $MAKE clean VERBOSE=$debug + shell: bash From 42f40a99b54619dd7adaab89a63ae4894c764a15 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <dhefam31@gmail.com> Date: Wed, 31 Jan 2024 22:50:07 +0700 Subject: [PATCH 34/36] Fix CI tests error and invalid workflow --- .github/workflows/tests.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1afc8012..916ada9a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,7 +40,7 @@ jobs: env: # Maven's debug flag (`-X`) - mvnDebugFlag: ${{ env.debug == true && '-X' || '' }} + mvnDebugFlag: ${{ inputs.debug == true && '-X' || '' }} strategy: # Set to maximum number of processes to speed up jobs run @@ -102,7 +102,7 @@ jobs: runs-on: ${{ matrix.os }}-latest env: - MAKE: ${{ env.debug == true && 'make -d' || 'make' }} + MAKE: ${{ inputs.debug == true && 'make -d' || 'make' }} strategy: # Set to maximum number of processes to speed up jobs run @@ -128,7 +128,6 @@ jobs: # Install deps - name: Install dependencies - id: init if: ${{ steps.setup-py.outputs.cache-hit != true }} run: | if [ $debug = 'true' ]; then @@ -144,7 +143,6 @@ jobs: # Compile - name: Compile the project - needs: init run: | [ -d target ] && make clean > /dev/null $MAKE compile LINT=true VERBOSE=$debug From e5e93e5025fb58561c7d3acccd4f50436fa184a7 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <dhefam31@gmail.com> Date: Wed, 31 Jan 2024 23:27:04 +0700 Subject: [PATCH 35/36] Fix unrecognized Java semVer and improve deps caching --- .github/workflows/tests.yml | 42 +++++++++++-------------------------- 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 916ada9a..680b16d3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -47,7 +47,7 @@ jobs: max-parallel: 6 matrix: os: [Ubuntu, Windows, macOS] - java-ver: [11, latest] # JDK 11 & latest + java-ver: [11, 17, 20] # JDK 11, 17 & 20 steps: # Checkout repository @@ -71,28 +71,17 @@ jobs: with: java-version: ${{ matrix.java-ver }} distribution: ${{ env.java-dist }} + cache: 'maven' + cache-dependency-path: '**/pom.xml' - # Install deps - - name: Install dependencies - if: ${{ steps.cache-maven.outputs.cache-hit != true }} - run: mvn clean install -DskipTests ${{ env.mvnDebugFlag }} - shell: bash - - # Packaging and testing - - name: Packaging the project - run: mvn package -P include-src ${{ env.mvnDebugFlag }} - shell: bash - - # Build the docs - - name: Build the HTML docs - run: mvn site -P lint ${{ env.mvnDebugFlag }} + # Build the project + - name: Build with Maven + run: mvn -B package -P include-src -P lint ${{ env.mvnDebugFlag }} shell: bash # Clean up - name: Clean up the project - run: | - mvn clean ${{ env.mvnDebugFlag }} - [ -d docs/jmatrix-* ] && rm --recursive --force docs/jmatrix-* + run: mvn clean ${{ env.mvnDebugFlag }} shell: bash @@ -108,8 +97,8 @@ jobs: # Set to maximum number of processes to speed up jobs run max-parallel: 6 matrix: - os: [Ubuntu, Windows, macOS] - py-ver: [3.7, 3.x] # Python 3.7 & latest + os: [Ubuntu, Windows] + py-ver: [3.7, 3.x] # Python 3.7 & latest of version 3 steps: # Checkout @@ -127,7 +116,7 @@ jobs: cache-dependency-path: '**/${{ env.deps }}' # Install deps - - name: Install dependencies + - name: Install Python dependencies if: ${{ steps.setup-py.outputs.cache-hit != true }} run: | if [ $debug = 'true' ]; then @@ -148,18 +137,11 @@ jobs: $MAKE compile LINT=true VERBOSE=$debug shell: bash - # Package - - name: Packaging the project + # Build + - name: Build with Make run: $MAKE package INCLUDE_SRC=true VERBOSE=$debug shell: bash - # Build docs - - name: Build the HTML docs - # For more information on debugging, it will implictly set the - # verbose mode to 'all' when the VERBOSE is true. - run: $MAKE build-docs LINT=true VERBOSE=$debug - shell: bash - # Clean up - name: Clean up the project run: $MAKE clean VERBOSE=$debug From 2a2f7ad50c8c1faeac6ea845b6d0f6a49ffd6257 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki <dhefam31@gmail.com> Date: Wed, 31 Jan 2024 23:48:26 +0700 Subject: [PATCH 36/36] Make tests only for *NIX systems The implemented code inside Makefiles are intended to run only in UNIX systems, as we got some errors on Windows system. For across platform, you can take the Maven to build the project. --- .github/workflows/tests.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 680b16d3..8136b9f8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -87,8 +87,8 @@ jobs: # ::---:: Make Test ::---:: # make-test: - name: Make Test / ${{ matrix.os }} / ${{ matrix.py-ver }} - runs-on: ${{ matrix.os }}-latest + name: Make Test / Ubuntu / ${{ matrix.py-ver }} + runs-on: ubuntu-latest env: MAKE: ${{ inputs.debug == true && 'make -d' || 'make' }} @@ -97,7 +97,6 @@ jobs: # Set to maximum number of processes to speed up jobs run max-parallel: 6 matrix: - os: [Ubuntu, Windows] py-ver: [3.7, 3.x] # Python 3.7 & latest of version 3 steps: @@ -106,7 +105,7 @@ jobs: uses: actions/checkout@v4 # Setup Python - - name: Setup Python / ${{ matrix.os }} / ${{ matrix.py-ver }} + - name: Setup Python / Ubuntu / ${{ matrix.py-ver }} id: setup-py uses: actions/setup-python@v4 with: