diff --git a/.github/workflows/build-and-analyze.yml b/.github/workflows/build-and-analyze.yml new file mode 100644 index 0000000..cb942a9 --- /dev/null +++ b/.github/workflows/build-and-analyze.yml @@ -0,0 +1,37 @@ +name: Java CI with Maven and SonarCloud + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + types: [opened, synchronize, reopened] + +jobs: + build-and-analyze: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Necessary for a comprehensive SonarCloud analysis + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'zulu' + + - name: Cache Maven and SonarCloud packages + uses: actions/cache@v3 + with: + path: | + ~/.m2 + ~/.sonar/cache + key: ${{ runner.os }}-m2-sonar-${{ hashFiles('**/pom.xml') }} + + - name: Build and analyze with Maven + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: mvn -B verify --file pom.xml org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -PcoverageReport diff --git a/.github/workflows/sonatype-publish.yml b/.github/workflows/sonatype-publish.yml new file mode 100644 index 0000000..4f4319c --- /dev/null +++ b/.github/workflows/sonatype-publish.yml @@ -0,0 +1,37 @@ +name: Maven Library Publish + +on: + release: + types: [ created ] + +jobs: + publish: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK 17 for deploy to Sonatype + uses: actions/setup-java@v3 + with: + distribution: 'adopt' + java-version: 17 + server-id: central + server-username: MAVEN_USERNAME + server-password: MAVEN_CENTRAL_TOKEN + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: MAVEN_GPG_PASSPHRASE + + - name: Build with Maven + run: mvn -B package --file pom.xml + + - name: Prepare Maven environnement with Java 17 for deployment to Sonatype + run: export MAVEN_OPTS="--add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED" + + - name: Publish to Apache Maven Central + run: mvn deploy -PsonatypeDeploy + env: + MAVEN_USERNAME: ${{ secrets.NEXUS_USERNAME }} + MAVEN_CENTRAL_TOKEN: ${{ secrets.NEXUS_PASSWORD }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} diff --git a/.gitignore b/.gitignore index 86f68ec..777547b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,222 @@ +# Created by https://www.toptal.com/developers/gitignore/api/java,maven,eclipse,intellij+all,visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=java,maven,eclipse,intellij+all,visualstudiocode + +### Eclipse ### +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +# Uncomment this line if you wish to ignore the project description file. +# Typically, this file would be tracked if it contains build/dependency configurations: +#.project + +### Eclipse Patch ### +# Spring Boot Tooling +.sts4-cache/ + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +#!.idea/codeStyles +#!.idea/runConfigurations + +### Java ### +# Compiled class file *.class +# Log file +*.log + +# BlueJ files +*.ctxt + # Mobile Tools for Java (J2ME) .mtj.tmp/ # Package Files # *.jar *.war +*.nar *.ear +*.zip +*.tar.gz +*.rar # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* +replay_pid* -*.iml -*.idea -*.DS_Store +### Maven ### +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar -**/target/ -/target/ -*/target/* -.classpath +# Eclipse m2e generated files +# Eclipse Core .project -.settings +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/java,maven,eclipse,intellij+all,visualstudiocode diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 47ee859..0000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: java - -jdk: - - oraclejdk8 - - oraclejdk7 - - - openjdk7 - - openjdk6 diff --git a/README.md b/README.md index b8540d6..242e0ff 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,7 @@ IpSubnetTree ============ -[![Build Status](http://img.shields.io/travis/x25/ip-subnet-tree/master.svg?style=flat-square)](https://travis-ci.org/x25/ip-subnet-tree) -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.x25/ip-subnet-tree/badge.svg?style=flat-square)](https://maven-badges.herokuapp.com/maven-central/com.github.x25/ip-subnet-tree/) - -An implementation of IP radix trie for CIDR Lookups. Current version supports only IPv4 addresses. +An implementation of an IP radix trie for CIDR lookups. The current version supports only IPv4 addresses. ## Usage: @@ -27,60 +24,63 @@ assertEquals("WAP Tele2", tree.find("77.219.59.9")); assertEquals("Unknown", tree.find("10.0.0.1")); ``` -A realistic example using the Spring Framework and a csv file in [ngx_http_geo_module](http://nginx.org/en/docs/http/ngx_http_geo_module.html) format: +A realistic example using the Spring Framework and a CSV file in the format of [ngx_http_geo_module](http://nginx.org/en/docs/http/ngx_http_geo_module.html): ```java -import com.github.x25.tree.IpSubnetTree; -import org.springframework.beans.factory.annotation.Autowired; +import io.github.teamlead.net.tree.IpSubnetTree; +import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; - import javax.annotation.PostConstruct; -import java.io.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; @Service public class GeoIpService { - private final IpSubnetTree tree = new IpSubnetTree(); + private final IpSubnetTree tree = new IpSubnetTree<>(); + private final ResourceLoader resourceLoader; + private final String dbPath; - @Autowired - private ResourceLoader resourceLoader; + public GeoIpService(ResourceLoader resourceLoader, + @Value("${geoip.db.path:classpath:path/to/db.csv}") String dbPath) { + this.resourceLoader = resourceLoader; + this.dbPath = dbPath; + } @PostConstruct - public void postConstruct() throws IOException { - - // Nginx geo module config format, e.g.: - // 127.0.0.1 foo; - // 192.168.1.0/24 bar; - // 10.1.0.0/16 baz; - Resource resource = resourceLoader.getResource("classpath:" + "path/to/db.csv"); + public void initGeoIpDatabase() throws IOException { + Resource resource = resourceLoader.getResource(dbPath); + loadDatabase(resource); + } + private void loadDatabase(Resource resource) throws IOException { if (!resource.isReadable()) { - throw new RuntimeException("Cannot read file"); + throw new IOException("Cannot read resource from " + resource.getDescription()); } - BufferedReader br = new BufferedReader(new InputStreamReader(resource.getInputStream(), "UTF-8")); - String line; - - while ((line = br.readLine()) != null) { - - if (StringUtils.isEmpty(line)) { - continue; - } - - String[] columns = line.split("\t", -1); - - if (columns.length != 2) { - throw new RuntimeException("Invalid line: " + line); - } + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) { + reader.lines() + .filter(line -> !StringUtils.isEmpty(line)) + .forEach(this::processLine); + } + } - tree.insert(columns[0].trim(), columns[1].replaceFirst(";$", "").trim()); + private void processLine(String line) { + String[] columns = line.split("\t", -1); + if (columns.length != 2) { + throw new IllegalArgumentException("Invalid line format: " + line); } - br.close(); + String ipRange = columns[0].trim(); + String geoCode = columns[1].replaceFirst(";$", "").trim(); + tree.insert(ipRange, geoCode); } public String getGeoCodeByIp(String ip) { @@ -97,16 +97,16 @@ Apache Maven dependency: ```xml - com.github.x25 + io.github.teamlead ip-subnet-tree - 1.0.2 + 1.1.0 ``` Gradle/Grails dependency: ``` -compile 'com.github.x25:ip-subnet-tree:1.0.2' +compile 'io.github.teamlead:ip-subnet-tree:1.1.0' ``` ## Tests diff --git a/pom.xml b/pom.xml index 094aaa2..509cc22 100644 --- a/pom.xml +++ b/pom.xml @@ -3,20 +3,26 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 - com.github.x25 + io.github.teamlead ip-subnet-tree - 1.0.2 + 1.1.0 jar - 3.8.1 + 5.10.2 UTF-8 - 1.6 + 1.7 + 1.7 + 1.8 + 1.8 + teamlead + https://sonarcloud.io + teamlead_ip-subnet-tree - ${project.groupId}:${project.artifactId} + IpSubnetTree An implementation of IP radix trie. - https://github.com/x25/ip-subnet-tree + https://github.com/teamlead/ip-subnet-tree @@ -25,80 +31,77 @@ + + + teamlead + info@teamlead.pro + https://teamlead.pro/ + pro.teamlead + https://github.com/teamlead + + + - scm:git:git@github.com:x25/ip-subnet-tree.git - scm:git:git@github.com:x25/ip-subnet-tree.git - git@github.com:x25/ip-subnet-tree.git + scm:git:git@github.com:teamlead/ip-subnet-tree.git + + + scm:git:git@github.com:teamlead/ip-subnet-tree.git + + git@github.com:teamlead/ip-subnet-tree.git + + github + https://github.com/teamlead/ip-subnet-tree/issues + + - junit - junit + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-params ${junit.version} test - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - ${project.build.sourceEncoding} - ${jdk.version} - ${jdk.version} - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 - true - - ossrh - https://oss.sonatype.org/ - true - - - - maven-deploy-plugin - 2.8.2 - - - default-deploy - deploy - - deploy - - - - - - - - release + sonatypeDeploy + + org.sonatype.central + central-publishing-maven-plugin + 0.3.0 + true + + central + true + true + published + + org.apache.maven.plugins maven-source-plugin - 3.0.0 + 3.3.0 attach-sources - jar + jar-no-fork @@ -106,13 +109,10 @@ org.apache.maven.plugins maven-javadoc-plugin - 2.10.3 - - ${project.build.sourceEncoding} - + 3.6.3 - attach-javadoc + attach-javadocs jar @@ -122,7 +122,7 @@ org.apache.maven.plugins maven-gpg-plugin - 1.5 + 3.1.0 sign-artifacts @@ -130,6 +130,47 @@ sign + + + --pinentry-mode + loopback + + + + + + + + + + coverageReport + + + + maven-surefire-plugin + 3.2.5 + + + org.jacoco + jacoco-maven-plugin + 0.8.7 + + + prepare-agent + + prepare-agent + + + + report + + report + + + + XML + + @@ -138,4 +179,21 @@ + + + + maven-compiler-plugin + 3.12.1 + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.testSource} + ${maven.compiler.testTarget} + + + + + + + diff --git a/src/main/java/com/github/x25/net/Utils.java b/src/main/java/com/github/x25/net/Utils.java deleted file mode 100644 index 7b6d37a..0000000 --- a/src/main/java/com/github/x25/net/Utils.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.github.x25.net; - -public class Utils { - - /** - * @param ipStr A quad-dotted notation string, e.g. "192.168.5.25" - * @return byte[] - */ - public static byte[] ipAddrToBytes(String ipStr) { - byte[] result = new byte[4]; - long b = 0L; - int q = 0; - - if (ipStr.length() <= 0 || ipStr.length() > 15) { - return null; - } - - for (int i = 0; i < ipStr.length(); ++i) { - - if (ipStr.charAt(i) == 46) { - if (b < 0L || b > 255L || q == 3) { - return null; - } - - result[q++] = (byte) ((int) (b & 255L)); - b = 0L; - } else { - int d = Character.digit(ipStr.charAt(i), 10); - if (d < 0) { - return null; - } - - b *= 10L; - b += (long) d; - } - } - - if (b >= 0L && b < 1L << (4 - q) * 8) { - switch (q) { - case 0: - result[0] = (byte) ((int) (b >> 24 & 255L)); - case 1: - result[1] = (byte) ((int) (b >> 16 & 255L)); - case 2: - result[2] = (byte) ((int) (b >> 8 & 255L)); - case 3: - result[3] = (byte) ((int) (b & 255L)); - default: - return result; - } - } else { - return null; - } - } - - /** - * @param ipStr A quad-dotted notation string, e.g. "192.168.5.25" - * @return long - */ - public static int ipAddrToInt(String ipStr) { - byte[] bytes = ipAddrToBytes(ipStr); - - if (bytes == null) { - throw new IllegalArgumentException("Could not parse [" + ipStr + "]"); - } - - int ret = 0; - for (int i = 0; i < 4 && i < bytes.length; i++) { - ret <<= 8; - ret |= (int) bytes[i] & 0xFF; - } - return ret; - } - - /** - * @param ip A quad-dotted notation string, e.g. "192.168.5.25" - * @return String - */ - public static String intToIpAddr(int ip) { - - return "" + String.valueOf((ip) >>> 24) + - "." + - String.valueOf((ip & 0x00FFFFFF) >>> 16) + - "." + - String.valueOf((ip & 0x0000FFFF) >>> 8) + - "." + - String.valueOf(ip & 0x000000FF); - } -} diff --git a/src/main/java/com/github/x25/net/tree/RadixInt32Tree.java b/src/main/java/com/github/x25/net/tree/RadixInt32Tree.java deleted file mode 100644 index 3c2eb71..0000000 --- a/src/main/java/com/github/x25/net/tree/RadixInt32Tree.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.github.x25.net.tree; - -public class RadixInt32Tree { - - private RadixTreeNode root; - - public RadixInt32Tree() { - root = new RadixTreeNode(); - } - - public void insert(int key, int mask, V value) { - RadixTreeNode node, next; - long bit = 0x80000000L; - - node = root; - next = root; - - while ((bit & mask) != 0) { - - next = ((key & bit) != 0) ? node.getRight() : node.getLeft(); - - if (next == null) { - break; - } - - bit >>= 1; - node = next; - } - - if (next != null) { - node.setValue(value); - return; - } - - while ((bit & mask) != 0) { - next = new RadixTreeNode(); - - next.setRight(null); - next.setLeft(null); - next.setValue(null); - - if ((key & bit) != 0) { - node.setRight(next); - - } else { - node.setLeft(next); - } - - bit >>= 1; - node = next; - } - - node.setValue(value); - } - - public V find(int key) { - long bit = 0x80000000L; - V value; - RadixTreeNode node; - - value = null; - node = root; - - while (node != null) { - if (node.getValue() != null) { - value = node.getValue(); - } - - if ((key & bit) != 0) { - node = node.getRight(); - - } else { - node = node.getLeft(); - } - - bit >>= 1; - } - - return value; - } -} diff --git a/src/main/java/com/github/x25/net/tree/RadixTreeNode.java b/src/main/java/com/github/x25/net/tree/RadixTreeNode.java deleted file mode 100644 index 85b7dc0..0000000 --- a/src/main/java/com/github/x25/net/tree/RadixTreeNode.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.x25.net.tree; - -public class RadixTreeNode { - - private RadixTreeNode right; - private RadixTreeNode left; - private V value; - - public V getValue() { - return value; - } - - public void setValue(V value) { - this.value = value; - } - - public RadixTreeNode getRight() { - return right; - } - - public void setRight(RadixTreeNode right) { - this.right = right; - } - - public RadixTreeNode getLeft() { - return left; - } - - public void setLeft(RadixTreeNode left) { - this.left = left; - } -} diff --git a/src/main/java/io/github/teamlead/net/Utils.java b/src/main/java/io/github/teamlead/net/Utils.java new file mode 100644 index 0000000..2ded850 --- /dev/null +++ b/src/main/java/io/github/teamlead/net/Utils.java @@ -0,0 +1,112 @@ +package io.github.teamlead.net; + +/** + * A utility class providing static methods for converting IP addresses between different formats. + * This class includes methods for converting IP addresses from quad-dotted string representation + * to byte array and integer formats, and vice versa. It is designed with private constructor + * to prevent instantiation, as it is intended to be used in a static context. + */ +public class Utils { + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private Utils() { + + } + + /** + * Converts an IP address from its quad-dotted string representation to a byte array. + * Each element in the byte array represents one part of the IP address. + * + * @param ipStr A quad-dotted notation string, e.g., "192.168.5.25" + * @return A byte array representing the IP address, where each byte corresponds to a part of the IP address. + */ + public static byte[] ipAddrToBytes(String ipStr) { + byte[] result = new byte[4]; + long b = 0L; + int q = 0; + + if (ipStr.length() <= 0 || ipStr.length() > 15) { + return null; + } + + for (int i = 0; i < ipStr.length(); ++i) { + + if (ipStr.charAt(i) == 46) { + if (b < 0L || b > 255L || q == 3) { + return null; + } + + result[q++] = (byte) ((int) (b & 255L)); + b = 0L; + } else { + int d = Character.digit(ipStr.charAt(i), 10); + if (d < 0) { + return null; + } + + b *= 10L; + b += d; + } + } + + if (b >= 0L && b < 1L << (4 - q) * 8) { + switch (q) { + case 0: + result[0] = (byte) ((int) (b >> 24 & 255L)); + case 1: + result[1] = (byte) ((int) (b >> 16 & 255L)); + case 2: + result[2] = (byte) ((int) (b >> 8 & 255L)); + case 3: + result[3] = (byte) ((int) (b & 255L)); + default: + return result; + } + } else { + return null; + } + } + + /** + * Converts an IP address from its quad-dotted string representation to an integer. + * This method encapsulates the IP address into a single integer value, where each byte of the integer + * represents one part of the IP address. + * + * @param ipStr A quad-dotted notation string, e.g., "192.168.5.25" + * @return An integer representing the IP address. + */ + public static int ipAddrToInt(String ipStr) { + byte[] bytes = ipAddrToBytes(ipStr); + + if (bytes == null) { + throw new IllegalArgumentException("Could not parse [" + ipStr + "]"); + } + + int ret = 0; + for (int i = 0; i < 4 && i < bytes.length; i++) { + ret <<= 8; + ret |= (int) bytes[i] & 0xFF; + } + return ret; + } + + /** + * Converts an IP address from its integer representation back to a quad-dotted string format. + * This method decodes the integer into its constituent parts to form the IP address string. + * + * @param ip An integer representing the IP address. + * @return A quad-dotted notation string representing the IP address, e.g., "192.168.5.25". + */ + public static String intToIpAddr(int ip) { + + return ((ip) >>> 24) + + "." + + ((ip & 0x00FFFFFF) >>> 16) + + "." + + ((ip & 0x0000FFFF) >>> 8) + + "." + + (ip & 0x000000FF); + } +} diff --git a/src/main/java/com/github/x25/net/tree/IpSubnetTree.java b/src/main/java/io/github/teamlead/net/tree/IpSubnetTree.java similarity index 57% rename from src/main/java/com/github/x25/net/tree/IpSubnetTree.java rename to src/main/java/io/github/teamlead/net/tree/IpSubnetTree.java index 90953f4..2b4baa0 100644 --- a/src/main/java/com/github/x25/net/tree/IpSubnetTree.java +++ b/src/main/java/io/github/teamlead/net/tree/IpSubnetTree.java @@ -1,10 +1,20 @@ -package com.github.x25.net.tree; +package io.github.teamlead.net.tree; -import com.github.x25.net.Utils; +import io.github.teamlead.net.Utils; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Represents a data structure for efficient IP subnet storage and lookup. This class + * utilizes a radix tree (trie) for storing values associated with IP subnets. It allows + * for quick retrieval of values based on IP address inputs, supporting both CIDR notation + * and range insertion for subnet definitions. It is designed with concurrency in mind, + * employing a {@link java.util.concurrent.locks.ReadWriteLock} to manage concurrent access. + * + * @param the type of value that the IP subnets will be associated with in this tree + */ public class IpSubnetTree { private RadixInt32Tree tree; @@ -20,14 +30,28 @@ private void releaseWriteLock() { readWriteLock.writeLock().unlock(); } + /** + * Constructs a new IPSubnetTree. Initializes the internal data structures required + * for IP subnet storage and retrieval. + */ public IpSubnetTree() { tree = new RadixInt32Tree(); } + /** + * Returns the default value to be used when no specific value is found for an IP address lookup. + * + * @return the default value + */ public V getDefaultValue() { return defaultValue; } + /** + * Sets the default value to be returned when no specific value is found for an IP address lookup. + * + * @param value the default value to set + */ public void setDefaultValue(V value) { defaultValue = value; } @@ -58,8 +82,10 @@ private static long calcMaxBlock(long ip) { } /** - * @param cidrNotation A CIDR-notation string, e.g. "192.168.0.0/16" - * @param value A value + * Inserts a value associated with a subnet defined in CIDR notation into the IP subnet tree. + * + * @param cidrNotation A CIDR-notation string, e.g., "192.168.0.0/16", representing the IP subnet + * @param value The value to associate with the specified IP subnet */ public void insert(String cidrNotation, V value) { if (value == null) @@ -83,9 +109,11 @@ public void insert(String cidrNotation, V value) { /** - * @param dotDecimalRangeStart A dot-decimal notation range start string, e.g. "192.168.0.0" - * @param dotDecimalRangeEnd A dot-decimal notation range end string, e.g. "192.168.255.255" - * @param value A value + * Inserts a value associated with an IP range specified in dot-decimal notation. + * + * @param dotDecimalRangeStart A dot-decimal notation string representing the start of the IP range, e.g., "192.168.0.0" + * @param dotDecimalRangeEnd A dot-decimal notation string representing the end of the IP range, e.g., "192.168.255.255" + * @param value The value to associate with the specified IP range */ public void insert(String dotDecimalRangeStart, String dotDecimalRangeEnd, V value) { if (value == null) @@ -97,7 +125,7 @@ public void insert(String dotDecimalRangeStart, String dotDecimalRangeEnd, V val while (end >= start) { long maxBlock = calcMaxBlock(start); long maxDiff = (int) (32 - Math.floor(Math.log(end - start + 1) / Math.log(2))); - maxBlock = maxBlock > maxDiff ? maxBlock : maxDiff; + maxBlock = Math.max(maxBlock, maxDiff); acquireWriteLock(); tree.insert((int) start, getMaskByBlock(maxBlock), value); releaseWriteLock(); @@ -106,8 +134,10 @@ public void insert(String dotDecimalRangeStart, String dotDecimalRangeEnd, V val } /** - * @param dotDecimalNotation A quad-dotted notation string, e.g. "192.168.5.25" - * @return A Value + * Finds and returns the value associated with a specific IP address. + * + * @param dotDecimalNotation A quad-dotted notation string, e.g., "192.168.5.25", representing the IP address + * @return The value associated with the specified IP address or the default value if no association exists */ public V find(String dotDecimalNotation) { V value = tree.find(Utils.ipAddrToInt(dotDecimalNotation)); diff --git a/src/main/java/io/github/teamlead/net/tree/RadixInt32Tree.java b/src/main/java/io/github/teamlead/net/tree/RadixInt32Tree.java new file mode 100644 index 0000000..f959288 --- /dev/null +++ b/src/main/java/io/github/teamlead/net/tree/RadixInt32Tree.java @@ -0,0 +1,110 @@ +package io.github.teamlead.net.tree; + +/** + * A radix tree implementation specifically designed for storing and retrieving values based on 32-bit integer keys. + * This implementation is optimized for IP routing table lookups, where the keys are often IP addresses represented + * as integers and the masks denote the subnet sizes. The tree allows for efficient storage and retrieval by + * compacting the common prefix of the keys, reducing the overall memory footprint and lookup time. + * + * @param the type of the values that the tree nodes will hold + */ +public class RadixInt32Tree { + + private RadixTreeNode root; + + /** + * Constructs a new, empty RadixInt32Tree. Initializes the root of the tree to a new node, + * effectively setting up the tree for subsequent insertions and lookups. + */ + public RadixInt32Tree() { + root = new RadixTreeNode(); + } + + /** + * Inserts a value into the tree with a specified key and mask. The key is a 32-bit integer + * representing the data to be stored, and the mask helps determine how the key is stored + * within the tree's structure, affecting the node placement based on the key's significant bits. + * + * @param key the 32-bit integer key associated with the value to be inserted + * @param mask the mask indicating the significant bits of the key for insertion + * @param value the value to associate with the given key in the tree + */ + public void insert(int key, int mask, V value) { + RadixTreeNode node, next; + long bit = 0x80000000L; + + node = root; + next = root; + + while ((bit & mask) != 0) { + + next = ((key & bit) != 0) ? node.getRight() : node.getLeft(); + + if (next == null) { + break; + } + + bit >>= 1; + node = next; + } + + if (next != null) { + node.setValue(value); + return; + } + + while ((bit & mask) != 0) { + next = new RadixTreeNode(); + + next.setRight(null); + next.setLeft(null); + next.setValue(null); + + if ((key & bit) != 0) { + node.setRight(next); + + } else { + node.setLeft(next); + } + + bit >>= 1; + node = next; + } + + node.setValue(value); + } + + /** + * Finds and returns the value associated with a given key. The search is performed based on + * the exact match of the provided key against those stored in the tree. If no matching key is + * found, this method returns {@code null}. + * + * @param key the 32-bit integer key for which to search in the tree + * @return the value associated with the specified key, or {@code null} if no such key exists in the tree + */ + public V find(int key) { + long bit = 0x80000000L; + V value; + RadixTreeNode node; + + value = null; + node = root; + + while (node != null) { + if (node.getValue() != null) { + value = node.getValue(); + } + + if ((key & bit) != 0) { + node = node.getRight(); + + } else { + node = node.getLeft(); + } + + bit >>= 1; + } + + return value; + } +} diff --git a/src/main/java/io/github/teamlead/net/tree/RadixTreeNode.java b/src/main/java/io/github/teamlead/net/tree/RadixTreeNode.java new file mode 100644 index 0000000..163fefe --- /dev/null +++ b/src/main/java/io/github/teamlead/net/tree/RadixTreeNode.java @@ -0,0 +1,70 @@ +package io.github.teamlead.net.tree; + +/** + * Represents a single node within a RadixInt32Tree, designed to hold values associated with 32-bit integer keys. + * Each node potentially has two children, referred to as the left and right child nodes, representing the binary + * choices at each step in the tree based on the bits of the key. This class also encapsulates the value associated + * with a node, which can represent the endpoint of a key or intermediate data in the radix tree structure. + * + * @param the type of the value that the node holds + */ +public class RadixTreeNode { + + private RadixTreeNode right; + private RadixTreeNode left; + private V value; + + /** + * Retrieves the value associated with this node. + * + * @return the value held by this node, which may be {@code null} if the node does not hold a value + */ + public V getValue() { + return value; + } + + /** + * Sets or updates the value associated with this node. + * + * @param value the new value to be associated with this node + */ + public void setValue(V value) { + this.value = value; + } + + /** + * Retrieves the right child of this node. + * + * @return the right child node, which may be {@code null} if no right child exists + */ + public RadixTreeNode getRight() { + return right; + } + + /** + * Sets or updates the right child of this node. + * + * @param right the node to be set as the right child of this node + */ + public void setRight(RadixTreeNode right) { + this.right = right; + } + + /** + * Retrieves the left child of this node. + * + * @return the left child node, which may be {@code null} if no left child exists + */ + public RadixTreeNode getLeft() { + return left; + } + + /** + * Sets or updates the left child of this node. + * + * @param left the node to be set as the left child of this node + */ + public void setLeft(RadixTreeNode left) { + this.left = left; + } +} diff --git a/src/test/java/com/github/x25/IpSubnetTreeTest.java b/src/test/java/io/github/teamlead/net/IpSubnetTreeTest.java similarity index 90% rename from src/test/java/com/github/x25/IpSubnetTreeTest.java rename to src/test/java/io/github/teamlead/net/IpSubnetTreeTest.java index 504b1b2..f6e236b 100644 --- a/src/test/java/com/github/x25/IpSubnetTreeTest.java +++ b/src/test/java/io/github/teamlead/net/IpSubnetTreeTest.java @@ -1,11 +1,13 @@ -package com.github.x25; +package io.github.teamlead.net; -import com.github.x25.net.tree.IpSubnetTree; -import com.github.x25.net.Utils; -import junit.framework.TestCase; +import io.github.teamlead.net.tree.IpSubnetTree; +import org.junit.jupiter.api.Test; -public class IpSubnetTreeTest extends TestCase { +import static org.junit.jupiter.api.Assertions.*; +public class IpSubnetTreeTest { + + @Test public void testExample() { IpSubnetTree tree = new IpSubnetTree(); @@ -21,6 +23,7 @@ public void testExample() assertEquals("Unknown", tree.find("10.0.0.1")); } + @Test public void testBasic() { IpSubnetTree tree = new IpSubnetTree(); tree.setDefaultValue("X"); @@ -41,6 +44,7 @@ public void testBasic() { assertEquals("X", tree.find("128.0.0.1")); } + @Test public void testNoCidr() { IpSubnetTree tree = new IpSubnetTree(); tree.setDefaultValue("X"); @@ -51,6 +55,7 @@ public void testNoCidr() { assertEquals("X", tree.find("129.0.0.6")); } + @Test public void testCidr() { IpSubnetTree tree = new IpSubnetTree(); for (int i = 0; i < 255; i++) { @@ -58,10 +63,11 @@ public void testCidr() { } for (int i = 0; i < 255; i++) { assertEquals(i, (int) tree.find(i + ".127.0.255")); - assertEquals(null, tree.find(i + ".128.0.255")); + assertNull(tree.find(i + ".128.0.255")); } } + @Test public void testRange() { IpSubnetTree tree = new IpSubnetTree(); for (int i = 0; i < 255; i++) { @@ -69,10 +75,11 @@ public void testRange() { } for (int i = 0; i < 255; i++) { assertEquals(i, (int) tree.find(i + ".127.0.255")); - assertEquals(null, tree.find(i + ".128.0.255")); + assertNull(tree.find(i + ".128.0.255")); } } + @Test public void testOverride() { IpSubnetTree test = new IpSubnetTree(); @@ -83,6 +90,7 @@ public void testOverride() { assertEquals("bar", test.find("1.2.3.4")); } + @Test public void testDotAll() { IpSubnetTree foo = new IpSubnetTree(); @@ -98,16 +106,18 @@ public void testDotAll() { assertEquals("bar", bar.find("255.255.255.255")); } + @Test public void testIllegalArgument() { IpSubnetTree foo = new IpSubnetTree(); try { foo.insert("abc", true); - assertTrue(false); + fail(); } catch (IllegalArgumentException e) { assertTrue(true); } } + @Test public void testUtils() { assertEquals("255.128.127.1", Utils.intToIpAddr(Utils.ipAddrToInt("255.128.127.1"))); assertEquals("255.255.255.255", Utils.intToIpAddr(Utils.ipAddrToInt("255.255.255.255"))); @@ -115,6 +125,7 @@ public void testUtils() { assertEquals("0.0.0.0", Utils.intToIpAddr(Utils.ipAddrToInt("0.0.0.0"))); } + @Test public void testTrim() { String foo = " dasd;";