All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ip2location.IP2Location Maven / Gradle / Ivy

Go to download

IP2Location Java Component enables applications to get info from IP address such as the visitor’s country, region, city, latitude, longitude, ZIP code, ISP name, domain name, time zone, connection speed, IDD code, area code, weather station code, weather station name, MCC, MNC, mobile brand name, elevation, usage type, address type, IAB category, district, autonomous system number (ASN) and autonomous system (AS).

The newest version!
package com.ip2location;

import java.net.InetAddress;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.UnknownHostException;
import java.io.*;
import java.util.*;
import java.util.regex.*;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.lang.StringBuffer; // JDK 1.4 does not support StringBuilder so can't use that
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;

/**
 * This class performs the lookup of IP2Location data from an IP address by reading a BIN file.
 * 

* Example usage scenarios: *

    *
  • Redirect based on country
  • *
  • Digital rights management
  • *
  • Web log stats and analysis
  • *
  • Auto-selection of country on forms
  • *
  • Filter access from countries you do not do business with
  • *
  • Geo-targeting for increased sales and click-through
  • *
  • And much, much more!
  • *
*

* Copyright (c) 2002-2024 IP2Location.com *

* * @author IP2Location.com * @version 8.12.0 */ public class IP2Location { private static final Pattern pattern = Pattern.compile("^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"); // IPv4 private static final Pattern pattern2 = Pattern.compile("^([0-9A-F]{1,4}:){6}(0[0-9]+\\.|.*?\\.0[0-9]+).*$", Pattern.CASE_INSENSITIVE); private static final Pattern pattern3 = Pattern.compile("^[0-9]+$"); private static final Pattern pattern4 = Pattern.compile("^(.*:)(([0-9]+\\.){3}[0-9]+)$"); private static final Pattern pattern5 = Pattern.compile("^.*((:[0-9A-F]{1,4}){2})$"); private static final Pattern pattern6 = Pattern.compile("^[0:]+((:[0-9A-F]{1,4}){1,2})$", Pattern.CASE_INSENSITIVE); private static final BigInteger MAX_IPV4_RANGE = new BigInteger("4294967295"); private static final BigInteger MAX_IPV6_RANGE = new BigInteger("340282366920938463463374607431768211455"); private static final BigInteger FROM_6TO4 = new BigInteger("42545680458834377588178886921629466624"); private static final BigInteger TO_6TO4 = new BigInteger("42550872755692912415807417417958686719"); private static final BigInteger FROM_TEREDO = new BigInteger("42540488161975842760550356425300246528"); private static final BigInteger TO_TEREDO = new BigInteger("42540488241204005274814694018844196863"); private static final BigInteger LAST_32BITS = new BigInteger("4294967295"); private static final int[] COUNTRY_POSITION = {0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; private static final int[] REGION_POSITION = {0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}; private static final int[] CITY_POSITION = {0, 0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}; private static final int[] ISP_POSITION = {0, 0, 3, 0, 5, 0, 7, 5, 7, 0, 8, 0, 9, 0, 9, 0, 9, 0, 9, 7, 9, 0, 9, 7, 9, 9, 9}; private static final int[] LATITUDE_POSITION = {0, 0, 0, 0, 0, 5, 5, 0, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}; private static final int[] LONGITUDE_POSITION = {0, 0, 0, 0, 0, 6, 6, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}; private static final int[] DOMAIN_POSITION = {0, 0, 0, 0, 0, 0, 0, 6, 8, 0, 9, 0, 10, 0, 10, 0, 10, 0, 10, 8, 10, 0, 10, 8, 10, 10, 10}; private static final int[] ZIPCODE_POSITION = {0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 7, 7, 0, 7, 7, 7, 0, 7, 0, 7, 7, 7, 0, 7, 7, 7}; private static final int[] TIMEZONE_POSITION = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 7, 8, 8, 8, 7, 8, 0, 8, 8, 8, 0, 8, 8, 8}; private static final int[] NETSPEED_POSITION = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 11, 0, 11, 8, 11, 0, 11, 0, 11, 0, 11, 11, 11}; private static final int[] IDDCODE_POSITION = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 12, 0, 12, 0, 12, 9, 12, 0, 12, 12, 12}; private static final int[] AREACODE_POSITION = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 13, 0, 13, 0, 13, 10, 13, 0, 13, 13, 13}; private static final int[] WEATHERSTATIONCODE_POSITION = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 14, 0, 14, 0, 14, 0, 14, 14, 14}; private static final int[] WEATHERSTATIONNAME_POSITION = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 15, 0, 15, 0, 15, 0, 15, 15, 15}; private static final int[] MCC_POSITION = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 16, 0, 16, 9, 16, 16, 16}; private static final int[] MNC_POSITION = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 17, 0, 17, 10, 17, 17, 17}; private static final int[] MOBILEBRAND_POSITION = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 18, 0, 18, 11, 18, 18, 18}; private static final int[] ELEVATION_POSITION = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 19, 0, 19, 19, 19}; private static final int[] USAGETYPE_POSITION = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 20, 20, 20}; private static final int[] ADDRESSTYPE_POSITION = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 21}; private static final int[] CATEGORY_POSITION = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 22}; private static final int[] DISTRICT_POSITION = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23}; private static final int[] ASN_POSITION = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24}; private static final int[] AS_POSITION = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25}; static final DecimalFormat GEO_COORDINATE_FORMAT; static { DecimalFormatSymbols symbols = new DecimalFormatSymbols(); symbols.setDecimalSeparator('.'); GEO_COORDINATE_FORMAT = new DecimalFormat("###.######", symbols); } private MetaData _MetaData = null; private MappedByteBuffer _IPv4Buffer = null; private MappedByteBuffer _IPv6Buffer = null; private MappedByteBuffer _MapDataBuffer = null; private final int[][] _IndexArrayIPv4 = new int[65536][2]; private final int[][] _IndexArrayIPv6 = new int[65536][2]; private long _MapDataOffset = 0; private int _IPv4ColumnSize = 0; private int _IPv6ColumnSize = 0; /** * To use memory mapped file for faster queries, set to true. */ public boolean UseMemoryMappedFile = false; /** * Sets the path for the BIN database (IPv4 BIN or IPv4+IPv6 BIN). */ public String IPDatabasePath = ""; /** * Sets the path for the license key file. */ public String IPLicensePath = ""; private FileLike.Supplier binFile; private int COUNTRY_POSITION_OFFSET; private int REGION_POSITION_OFFSET; private int CITY_POSITION_OFFSET; private int ISP_POSITION_OFFSET; private int DOMAIN_POSITION_OFFSET; private int ZIPCODE_POSITION_OFFSET; private int LATITUDE_POSITION_OFFSET; private int LONGITUDE_POSITION_OFFSET; private int TIMEZONE_POSITION_OFFSET; private int NETSPEED_POSITION_OFFSET; private int IDDCODE_POSITION_OFFSET; private int AREACODE_POSITION_OFFSET; private int WEATHERSTATIONCODE_POSITION_OFFSET; private int WEATHERSTATIONNAME_POSITION_OFFSET; private int MCC_POSITION_OFFSET; private int MNC_POSITION_OFFSET; private int MOBILEBRAND_POSITION_OFFSET; private int ELEVATION_POSITION_OFFSET; private int USAGETYPE_POSITION_OFFSET; private int ADDRESSTYPE_POSITION_OFFSET; private int CATEGORY_POSITION_OFFSET; private int DISTRICT_POSITION_OFFSET; private int ASN_POSITION_OFFSET; private int AS_POSITION_OFFSET; private boolean COUNTRY_ENABLED; private boolean REGION_ENABLED; private boolean CITY_ENABLED; private boolean ISP_ENABLED; private boolean LATITUDE_ENABLED; private boolean LONGITUDE_ENABLED; private boolean DOMAIN_ENABLED; private boolean ZIPCODE_ENABLED; private boolean TIMEZONE_ENABLED; private boolean NETSPEED_ENABLED; private boolean IDDCODE_ENABLED; private boolean AREACODE_ENABLED; private boolean WEATHERSTATIONCODE_ENABLED; private boolean WEATHERSTATIONNAME_ENABLED; private boolean MCC_ENABLED; private boolean MNC_ENABLED; private boolean MOBILEBRAND_ENABLED; private boolean ELEVATION_ENABLED; private boolean USAGETYPE_ENABLED; private boolean ADDRESSTYPE_ENABLED; private boolean CATEGORY_ENABLED; private boolean DISTRICT_ENABLED; private boolean ASN_ENABLED; private boolean AS_ENABLED; public IP2Location() { } interface FileLike { interface Supplier { FileLike open() throws IOException; boolean isValid(); } int read(byte[] buffer) throws IOException; int read(byte b[], int off, int len) throws IOException; void seek(long pos) throws IOException; void close() throws IOException; } /** * This function returns the package version. * * @return Package version */ public String GetPackageVersion() { if (_MetaData == null) { return ""; } if (_MetaData.getDBType() == 0) { return ""; } else { return String.valueOf(_MetaData.getDBType()); } } /** * This function returns the IP database version. * * @return IP database version */ public String GetDatabaseVersion() { if (_MetaData == null) { return ""; } if (_MetaData.getDBYear() == 0) { return ""; } else { return "20" + _MetaData.getDBYear() + "." + _MetaData.getDBMonth() + "." + _MetaData.getDBDay(); } } /** * This function can be used to pre-load the BIN file. * * @param DBPath The full path to the IP2Location BIN database file * @throws IOException If an input or output exception occurred */ public void Open(String DBPath) throws IOException { IPDatabasePath = DBPath; binFile = new FileLike.Supplier() { public FileLike open() throws IOException { return new FileLike() { private final RandomAccessFile aFile = new RandomAccessFile(DBPath, "r"); public int read(byte[] buffer) throws IOException { return aFile.read(buffer); } public int read(byte[] b, int off, int len) throws IOException { return aFile.read(b, off, len); } public void seek(long pos) throws IOException { aFile.seek(pos); } public void close() throws IOException { aFile.close(); } }; } public boolean isValid() { return DBPath.length() > 0; } }; LoadBIN(); } /** * This function can be used to initialize the component with params and pre-load the BIN file. * * @param DBPath The full path to the IP2Location BIN database file * @param UseMMF Set to true to load the BIN database file into memory mapped file * @throws IOException If an input or output exception occurred */ public void Open(String DBPath, boolean UseMMF) throws IOException { UseMemoryMappedFile = UseMMF; Open(DBPath); } public void Open(byte[] db) throws IOException { binFile = new FileLike.Supplier() { public FileLike open() { return new FileLike() { private final ByteArrayInputStream stream = new ByteArrayInputStream(db); public int read(byte[] buffer) throws IOException { return stream.read(buffer); } public int read(byte[] b, int off, int len) { return stream.read(b, off, len); } public void seek(long pos) { stream.reset(); stream.skip(pos); } public void close() throws IOException { stream.close(); } }; } public boolean isValid() { return db.length > 0; } }; LoadBIN(); } /** * This function destroys the mapped bytes. */ public void Close() { _MetaData = null; DestroyMappedBytes(); } private void DestroyMappedBytes() { _IPv4Buffer = null; _IPv6Buffer = null; _MapDataBuffer = null; } private void CreateMappedBytes() throws IOException { try (RandomAccessFile aFile = new RandomAccessFile(IPDatabasePath, "r")) { final FileChannel inChannel = aFile.getChannel(); CreateMappedBytes(inChannel); } } private void CreateMappedBytes(FileChannel inChannel) throws IOException { if (_IPv4Buffer == null) { final long _IPv4Bytes = (long) _IPv4ColumnSize * (long) _MetaData.getDBCount(); long _IPv4Offset = _MetaData.getBaseAddr() - 1; _IPv4Buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, _IPv4Offset, _IPv4Bytes); _IPv4Buffer.order(ByteOrder.LITTLE_ENDIAN); _MapDataOffset = _IPv4Offset + _IPv4Bytes; } if (!_MetaData.getOldBIN() && _IPv6Buffer == null) { final long _IPv6Bytes = (long) _IPv6ColumnSize * (long) _MetaData.getDBCountIPv6(); long _IPv6Offset = _MetaData.getBaseAddrIPv6() - 1; _IPv6Buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, _IPv6Offset, _IPv6Bytes); _IPv6Buffer.order(ByteOrder.LITTLE_ENDIAN); _MapDataOffset = _IPv6Offset + _IPv6Bytes; } if (_MapDataBuffer == null) { _MapDataBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, _MapDataOffset, inChannel.size() - _MapDataOffset); _MapDataBuffer.order(ByteOrder.LITTLE_ENDIAN); } } private boolean LoadBIN() throws IOException { boolean loadOK = false; FileLike aFile = null; try { if (binFile.isValid()) { aFile = binFile.open(); byte[] _HeaderData = new byte[64]; aFile.read(_HeaderData); ByteBuffer _HeaderBuffer = ByteBuffer.wrap(_HeaderData); _HeaderBuffer.order(ByteOrder.LITTLE_ENDIAN); _MetaData = new MetaData(); _MetaData.setDBType(_HeaderBuffer.get(0)); _MetaData.setDBColumn(_HeaderBuffer.get(1)); _MetaData.setDBYear(_HeaderBuffer.get(2)); _MetaData.setDBMonth(_HeaderBuffer.get(3)); _MetaData.setDBDay(_HeaderBuffer.get(4)); _MetaData.setDBCount(_HeaderBuffer.getInt(5)); // 4 bytes _MetaData.setBaseAddr(_HeaderBuffer.getInt(9)); // 4 bytes _MetaData.setDBCountIPv6(_HeaderBuffer.getInt(13)); // 4 bytes _MetaData.setBaseAddrIPv6(_HeaderBuffer.getInt(17)); // 4 bytes _MetaData.setIndexBaseAddr(_HeaderBuffer.getInt(21)); //4 bytes _MetaData.setIndexBaseAddrIPv6(_HeaderBuffer.getInt(25)); //4 bytes _MetaData.setProductCode(_HeaderBuffer.get(29)); // below 2 fields just read for now, not being used yet _MetaData.setProductType(_HeaderBuffer.get(30)); _MetaData.setFileSize(_HeaderBuffer.getInt(31)); //4 bytes // check if is correct BIN (should be 1 for IP2Location BIN file), also checking for zipped file (PK being the first 2 chars) if ((_MetaData.getProductCode() != 1 && _MetaData.getDBYear() >= 21) || (_MetaData.getDBType() == 80 && _MetaData.getDBColumn() == 75)) // only BINs from Jan 2021 onwards have this byte set { throw new IOException("Incorrect IP2Location BIN file format. Please make sure that you are using the latest IP2Location BIN file."); } if (_MetaData.getIndexBaseAddr() > 0) { _MetaData.setIndexed(true); } if (_MetaData.getDBCountIPv6() == 0) { // old style IPv4-only BIN file _MetaData.setOldBIN(true); } else { if (_MetaData.getIndexBaseAddrIPv6() > 0) { _MetaData.setIndexedIPv6(true); } } final int dbcoll = _MetaData.getDBColumn(); _IPv4ColumnSize = dbcoll << 2; // 4 bytes each column _IPv6ColumnSize = 16 + ((dbcoll - 1) << 2); // 4 bytes each column, except IPFrom column which is 16 bytes final int dbtype = _MetaData.getDBType(); COUNTRY_POSITION_OFFSET = (COUNTRY_POSITION[dbtype] != 0) ? (COUNTRY_POSITION[dbtype] - 2) << 2 : 0; REGION_POSITION_OFFSET = (REGION_POSITION[dbtype] != 0) ? (REGION_POSITION[dbtype] - 2) << 2 : 0; CITY_POSITION_OFFSET = (CITY_POSITION[dbtype] != 0) ? (CITY_POSITION[dbtype] - 2) << 2 : 0; ISP_POSITION_OFFSET = (ISP_POSITION[dbtype] != 0) ? (ISP_POSITION[dbtype] - 2) << 2 : 0; DOMAIN_POSITION_OFFSET = (DOMAIN_POSITION[dbtype] != 0) ? (DOMAIN_POSITION[dbtype] - 2) << 2 : 0; ZIPCODE_POSITION_OFFSET = (ZIPCODE_POSITION[dbtype] != 0) ? (ZIPCODE_POSITION[dbtype] - 2) << 2 : 0; LATITUDE_POSITION_OFFSET = (LATITUDE_POSITION[dbtype] != 0) ? (LATITUDE_POSITION[dbtype] - 2) << 2 : 0; LONGITUDE_POSITION_OFFSET = (LONGITUDE_POSITION[dbtype] != 0) ? (LONGITUDE_POSITION[dbtype] - 2) << 2 : 0; TIMEZONE_POSITION_OFFSET = (TIMEZONE_POSITION[dbtype] != 0) ? (TIMEZONE_POSITION[dbtype] - 2) << 2 : 0; NETSPEED_POSITION_OFFSET = (NETSPEED_POSITION[dbtype] != 0) ? (NETSPEED_POSITION[dbtype] - 2) << 2 : 0; IDDCODE_POSITION_OFFSET = (IDDCODE_POSITION[dbtype] != 0) ? (IDDCODE_POSITION[dbtype] - 2) << 2 : 0; AREACODE_POSITION_OFFSET = (AREACODE_POSITION[dbtype] != 0) ? (AREACODE_POSITION[dbtype] - 2) << 2 : 0; WEATHERSTATIONCODE_POSITION_OFFSET = (WEATHERSTATIONCODE_POSITION[dbtype] != 0) ? (WEATHERSTATIONCODE_POSITION[dbtype] - 2) << 2 : 0; WEATHERSTATIONNAME_POSITION_OFFSET = (WEATHERSTATIONNAME_POSITION[dbtype] != 0) ? (WEATHERSTATIONNAME_POSITION[dbtype] - 2) << 2 : 0; MCC_POSITION_OFFSET = (MCC_POSITION[dbtype] != 0) ? (MCC_POSITION[dbtype] - 2) << 2 : 0; MNC_POSITION_OFFSET = (MNC_POSITION[dbtype] != 0) ? (MNC_POSITION[dbtype] - 2) << 2 : 0; MOBILEBRAND_POSITION_OFFSET = (MOBILEBRAND_POSITION[dbtype] != 0) ? (MOBILEBRAND_POSITION[dbtype] - 2) << 2 : 0; ELEVATION_POSITION_OFFSET = (ELEVATION_POSITION[dbtype] != 0) ? (ELEVATION_POSITION[dbtype] - 2) << 2 : 0; USAGETYPE_POSITION_OFFSET = (USAGETYPE_POSITION[dbtype] != 0) ? (USAGETYPE_POSITION[dbtype] - 2) << 2 : 0; ADDRESSTYPE_POSITION_OFFSET = (ADDRESSTYPE_POSITION[dbtype] != 0) ? (ADDRESSTYPE_POSITION[dbtype] - 2) << 2 : 0; CATEGORY_POSITION_OFFSET = (CATEGORY_POSITION[dbtype] != 0) ? (CATEGORY_POSITION[dbtype] - 2) << 2 : 0; DISTRICT_POSITION_OFFSET = (DISTRICT_POSITION[dbtype] != 0) ? (DISTRICT_POSITION[dbtype] - 2) << 2 : 0; ASN_POSITION_OFFSET = (ASN_POSITION[dbtype] != 0) ? (ASN_POSITION[dbtype] - 2) << 2 : 0; AS_POSITION_OFFSET = (AS_POSITION[dbtype] != 0) ? (AS_POSITION[dbtype] - 2) << 2 : 0; COUNTRY_ENABLED = (COUNTRY_POSITION[dbtype] != 0); REGION_ENABLED = (REGION_POSITION[dbtype] != 0); CITY_ENABLED = (CITY_POSITION[dbtype] != 0); ISP_ENABLED = (ISP_POSITION[dbtype] != 0); LATITUDE_ENABLED = (LATITUDE_POSITION[dbtype] != 0); LONGITUDE_ENABLED = (LONGITUDE_POSITION[dbtype] != 0); DOMAIN_ENABLED = (DOMAIN_POSITION[dbtype] != 0); ZIPCODE_ENABLED = (ZIPCODE_POSITION[dbtype] != 0); TIMEZONE_ENABLED = (TIMEZONE_POSITION[dbtype] != 0); NETSPEED_ENABLED = (NETSPEED_POSITION[dbtype] != 0); IDDCODE_ENABLED = (IDDCODE_POSITION[dbtype] != 0); AREACODE_ENABLED = (AREACODE_POSITION[dbtype] != 0); WEATHERSTATIONCODE_ENABLED = (WEATHERSTATIONCODE_POSITION[dbtype] != 0); WEATHERSTATIONNAME_ENABLED = (WEATHERSTATIONNAME_POSITION[dbtype] != 0); MCC_ENABLED = (MCC_POSITION[dbtype] != 0); MNC_ENABLED = (MNC_POSITION[dbtype] != 0); MOBILEBRAND_ENABLED = (MOBILEBRAND_POSITION[dbtype] != 0); ELEVATION_ENABLED = (ELEVATION_POSITION[dbtype] != 0); USAGETYPE_ENABLED = (USAGETYPE_POSITION[dbtype] != 0); ADDRESSTYPE_ENABLED = (ADDRESSTYPE_POSITION[dbtype] != 0); CATEGORY_ENABLED = (CATEGORY_POSITION[dbtype] != 0); DISTRICT_ENABLED = (DISTRICT_POSITION[dbtype] != 0); ASN_ENABLED = (ASN_POSITION[dbtype] != 0); AS_ENABLED = (AS_POSITION[dbtype] != 0); if (_MetaData.getIndexed()) { int readLen = _IndexArrayIPv4.length; if (_MetaData.getIndexedIPv6()) { readLen += _IndexArrayIPv6.length; } byte[] _IndexData = new byte[readLen * 8]; // 4 bytes for both from row and to row aFile.seek(_MetaData.getIndexBaseAddr() - 1); aFile.read(_IndexData); ByteBuffer _IndexBuffer = ByteBuffer.wrap(_IndexData); _IndexBuffer.order(ByteOrder.LITTLE_ENDIAN); int pointer = 0; // read IPv4 index for (int x = 0; x < _IndexArrayIPv4.length; x++) { _IndexArrayIPv4[x][0] = _IndexBuffer.getInt(pointer); // 4 bytes for from row _IndexArrayIPv4[x][1] = _IndexBuffer.getInt(pointer + 4); // 4 bytes for to row pointer += 8; } if (_MetaData.getIndexedIPv6()) { // read IPv6 index for (int x = 0; x < _IndexArrayIPv6.length; x++) { _IndexArrayIPv6[x][0] = _IndexBuffer.getInt(pointer); // 4 bytes for from row _IndexArrayIPv6[x][1] = _IndexBuffer.getInt(pointer + 4); // 4 bytes for to row pointer += 8; } } } if (UseMemoryMappedFile) { CreateMappedBytes(); } else { DestroyMappedBytes(); } loadOK = true; } } finally { if (aFile != null) { aFile.close(); } } return loadOK; } /** * @deprecated */ protected void finalize() throws Throwable { super.finalize(); } /** * This function to query IP2Location data. * * @param IPAddress IP Address you wish to query * @return IP2Location data * @throws IOException If an input or output exception occurred */ public IPResult IPQuery(String IPAddress) throws IOException { IPAddress = IPAddress.trim(); IPResult record = new IPResult(IPAddress); FileLike filehandle = null; ByteBuffer mybuffer = null; ByteBuffer mydatabuffer = null; byte[] row; byte[] fullrow = null; try { if (IPAddress == null || IPAddress.length() == 0) { record.status = "EMPTY_IP_ADDRESS"; return record; } BigInteger ipno; int indexaddr; int actualiptype; int myiptype; int mybaseaddr = 0; int mycolumnsize; int mybufcapacity = 0; BigInteger MAX_IP_RANGE; long rowoffset; long rowoffset2; BigInteger[] bi; boolean overcapacity = false; String[] retarr; try { bi = ip2No(IPAddress); myiptype = bi[0].intValue(); ipno = bi[1]; actualiptype = bi[2].intValue(); if (actualiptype == 6) { // means didn't match IPv4 regex retarr = expandIPV6(IPAddress, myiptype); record.ip_address = retarr[0]; // return after expand IPv6 format myiptype = Integer.parseInt(retarr[1]); // special cases } } catch (UnknownHostException e) { record.status = "INVALID_IP_ADDRESS"; return record; } long low = 0; long high; long mid; long position; BigInteger ipfrom; BigInteger ipto; int firstcol = 4; // IP From is 4 bytes // Read BIN if haven't done so if (_MetaData == null) { if (!LoadBIN()) { // problems reading BIN record.status = "MISSING_FILE"; return record; } } if (UseMemoryMappedFile) { if ((_IPv4Buffer == null) || (!_MetaData.getOldBIN() && _IPv6Buffer == null) || (_MapDataBuffer == null)) { CreateMappedBytes(); } } else { DestroyMappedBytes(); filehandle = binFile.open(); } if (myiptype == 4) { // IPv4 MAX_IP_RANGE = MAX_IPV4_RANGE; high = _MetaData.getDBCount(); if (UseMemoryMappedFile) { mybuffer = _IPv4Buffer.duplicate(); // this enables this thread to maintain its own position in a multi-threaded environment mybuffer.order(ByteOrder.LITTLE_ENDIAN); mybufcapacity = mybuffer.capacity(); } else { mybaseaddr = _MetaData.getBaseAddr(); } mycolumnsize = _IPv4ColumnSize; if (_MetaData.getIndexed()) { indexaddr = ipno.shiftRight(16).intValue(); low = _IndexArrayIPv4[indexaddr][0]; high = _IndexArrayIPv4[indexaddr][1]; } } else { // IPv6 firstcol = 16; // IPv6 is 16 bytes if (_MetaData.getOldBIN()) { record.status = "IPV6_NOT_SUPPORTED"; return record; } MAX_IP_RANGE = MAX_IPV6_RANGE; high = _MetaData.getDBCountIPv6(); if (UseMemoryMappedFile) { mybuffer = _IPv6Buffer.duplicate(); // this enables this thread to maintain its own position in a multi-threaded environment mybuffer.order(ByteOrder.LITTLE_ENDIAN); mybufcapacity = mybuffer.capacity(); } else { mybaseaddr = _MetaData.getBaseAddrIPv6(); } mycolumnsize = _IPv6ColumnSize; if (_MetaData.getIndexedIPv6()) { indexaddr = ipno.shiftRight(112).intValue(); low = _IndexArrayIPv6[indexaddr][0]; high = _IndexArrayIPv6[indexaddr][1]; } } if (ipno.compareTo(MAX_IP_RANGE) == 0) ipno = ipno.subtract(BigInteger.ONE); while (low <= high) { mid = (low + high) / 2; rowoffset = mybaseaddr + (mid * mycolumnsize); rowoffset2 = rowoffset + mycolumnsize; if (UseMemoryMappedFile) { // only reading the IP From fields overcapacity = (rowoffset2 >= mybufcapacity); ipfrom = read32or128(rowoffset, myiptype, mybuffer, filehandle); ipto = (overcapacity) ? BigInteger.ZERO : read32or128(rowoffset2, myiptype, mybuffer, filehandle); } else { // reading IP From + whole row + next IP From fullrow = readRow(rowoffset, mycolumnsize + firstcol, mybuffer, filehandle); ipfrom = read32or128Row(fullrow, 0, firstcol); ipto = (overcapacity) ? BigInteger.ZERO : read32or128Row(fullrow, mycolumnsize, firstcol); } if (ipno.compareTo(ipfrom) >= 0 && ipno.compareTo(ipto) < 0) { int rowlen = mycolumnsize - firstcol; if (UseMemoryMappedFile) { row = readRow(rowoffset + firstcol, rowlen, mybuffer, filehandle); mydatabuffer = _MapDataBuffer.duplicate(); // this is to enable reading of a range of bytes in multi-threaded environment mydatabuffer.order(ByteOrder.LITTLE_ENDIAN); } else { row = new byte[rowlen]; System.arraycopy(fullrow, firstcol, row, (int) 0, rowlen); // extract the actual row data } if (COUNTRY_ENABLED) { position = read32Row(row, COUNTRY_POSITION_OFFSET).longValue(); record.country_short = readStr(position, mydatabuffer, filehandle); position += 3; record.country_long = readStr(position, mydatabuffer, filehandle); } else { record.country_short = IPResult.NOT_SUPPORTED; record.country_long = IPResult.NOT_SUPPORTED; } if (REGION_ENABLED) { position = read32Row(row, REGION_POSITION_OFFSET).longValue(); record.region = readStr(position, mydatabuffer, filehandle); } else { record.region = IPResult.NOT_SUPPORTED; } if (CITY_ENABLED) { position = read32Row(row, CITY_POSITION_OFFSET).longValue(); record.city = readStr(position, mydatabuffer, filehandle); } else { record.city = IPResult.NOT_SUPPORTED; } if (ISP_ENABLED) { position = read32Row(row, ISP_POSITION_OFFSET).longValue(); record.isp = readStr(position, mydatabuffer, filehandle); } else { record.isp = IPResult.NOT_SUPPORTED; } if (LATITUDE_ENABLED) { record.latitude = Float.parseFloat(setDecimalPlaces(readFloatRow(row, LATITUDE_POSITION_OFFSET))); } else { record.latitude = 0.0F; } if (LONGITUDE_ENABLED) { record.longitude = Float.parseFloat(setDecimalPlaces(readFloatRow(row, LONGITUDE_POSITION_OFFSET))); } else { record.longitude = 0.0F; } if (DOMAIN_ENABLED) { position = read32Row(row, DOMAIN_POSITION_OFFSET).longValue(); record.domain = readStr(position, mydatabuffer, filehandle); } else { record.domain = IPResult.NOT_SUPPORTED; } if (ZIPCODE_ENABLED) { position = read32Row(row, ZIPCODE_POSITION_OFFSET).longValue(); record.zipcode = readStr(position, mydatabuffer, filehandle); } else { record.zipcode = IPResult.NOT_SUPPORTED; } if (TIMEZONE_ENABLED) { position = read32Row(row, TIMEZONE_POSITION_OFFSET).longValue(); record.timezone = readStr(position, mydatabuffer, filehandle); } else { record.timezone = IPResult.NOT_SUPPORTED; } if (NETSPEED_ENABLED) { position = read32Row(row, NETSPEED_POSITION_OFFSET).longValue(); record.netspeed = readStr(position, mydatabuffer, filehandle); } else { record.netspeed = IPResult.NOT_SUPPORTED; } if (IDDCODE_ENABLED) { position = read32Row(row, IDDCODE_POSITION_OFFSET).longValue(); record.iddcode = readStr(position, mydatabuffer, filehandle); } else { record.iddcode = IPResult.NOT_SUPPORTED; } if (AREACODE_ENABLED) { position = read32Row(row, AREACODE_POSITION_OFFSET).longValue(); record.areacode = readStr(position, mydatabuffer, filehandle); } else { record.areacode = IPResult.NOT_SUPPORTED; } if (WEATHERSTATIONCODE_ENABLED) { position = read32Row(row, WEATHERSTATIONCODE_POSITION_OFFSET).longValue(); record.weatherstationcode = readStr(position, mydatabuffer, filehandle); } else { record.weatherstationcode = IPResult.NOT_SUPPORTED; } if (WEATHERSTATIONNAME_ENABLED) { position = read32Row(row, WEATHERSTATIONNAME_POSITION_OFFSET).longValue(); record.weatherstationname = readStr(position, mydatabuffer, filehandle); } else { record.weatherstationname = IPResult.NOT_SUPPORTED; } if (MCC_ENABLED) { position = read32Row(row, MCC_POSITION_OFFSET).longValue(); record.mcc = readStr(position, mydatabuffer, filehandle); } else { record.mcc = IPResult.NOT_SUPPORTED; } if (MNC_ENABLED) { position = read32Row(row, MNC_POSITION_OFFSET).longValue(); record.mnc = readStr(position, mydatabuffer, filehandle); } else { record.mnc = IPResult.NOT_SUPPORTED; } if (MOBILEBRAND_ENABLED) { position = read32Row(row, MOBILEBRAND_POSITION_OFFSET).longValue(); record.mobilebrand = readStr(position, mydatabuffer, filehandle); } else { record.mobilebrand = IPResult.NOT_SUPPORTED; } if (ELEVATION_ENABLED) { position = read32Row(row, ELEVATION_POSITION_OFFSET).longValue(); record.elevation = convertFloat(readStr(position, mydatabuffer, filehandle)); // due to value being stored as a string but output as float } else { record.elevation = 0.0F; } if (USAGETYPE_ENABLED) { position = read32Row(row, USAGETYPE_POSITION_OFFSET).longValue(); record.usagetype = readStr(position, mydatabuffer, filehandle); } else { record.usagetype = IPResult.NOT_SUPPORTED; } if (ADDRESSTYPE_ENABLED) { position = read32Row(row, ADDRESSTYPE_POSITION_OFFSET).longValue(); record.addresstype = readStr(position, mydatabuffer, filehandle); } else { record.addresstype = IPResult.NOT_SUPPORTED; } if (CATEGORY_ENABLED) { position = read32Row(row, CATEGORY_POSITION_OFFSET).longValue(); record.category = readStr(position, mydatabuffer, filehandle); } else { record.category = IPResult.NOT_SUPPORTED; } if (DISTRICT_ENABLED) { position = read32Row(row, DISTRICT_POSITION_OFFSET).longValue(); record.district = readStr(position, mydatabuffer, filehandle); } else { record.district = IPResult.NOT_SUPPORTED; } if (ASN_ENABLED) { position = read32Row(row, ASN_POSITION_OFFSET).longValue(); record.asn = readStr(position, mydatabuffer, filehandle); } else { record.asn = IPResult.NOT_SUPPORTED; } if (AS_ENABLED) { position = read32Row(row, AS_POSITION_OFFSET).longValue(); record.as = readStr(position, mydatabuffer, filehandle); } else { record.as = IPResult.NOT_SUPPORTED; } record.status = "OK"; break; } else { if (ipno.compareTo(ipfrom) < 0) { high = mid - 1; } else { low = mid + 1; } } } return record; } finally { if (filehandle != null) { filehandle.close(); } } } private String[] expandIPV6(final String myIP, final int myiptype) { final String tmp = "0000:0000:0000:0000:0000:"; final String padme = "0000"; final long hexoffset = 0xFF; String myIP2 = myIP.toUpperCase(); String rettype = String.valueOf(myiptype); // expand ipv4-mapped ipv6 if (myiptype == 4) { if (pattern4.matcher(myIP2).matches()) { myIP2 = myIP2.replaceAll("::", tmp); } else { Matcher mat = pattern5.matcher(myIP2); if (mat.matches()) { String mymatch = mat.group(1); String[] myarr = mymatch.replaceAll("^:+", "").replaceAll(":+$", "").split(":"); int len = myarr.length; StringBuffer bf = new StringBuffer(32); for (int x = 0; x < len; x++) { String unpadded = myarr[x]; bf.append(padme.substring(unpadded.length())).append(unpadded); // safe padding for JDK 1.4 } long mylong = new BigInteger(bf.toString(), 16).longValue(); long[] b = {0, 0, 0, 0}; // using long in place of bytes due to 2's complement signed issue for (int x = 0; x < 4; x++) { b[x] = mylong & hexoffset; mylong = mylong >> 8; } myIP2 = myIP2.replaceAll(mymatch + "$", ":" + b[3] + "." + b[2] + "." + b[1] + "." + b[0]); myIP2 = myIP2.replaceAll("::", tmp); } } } else if (myiptype == 6) { if (myIP2.equals("::")) { myIP2 = myIP2 + "0.0.0.0"; myIP2 = myIP2.replaceAll("::", tmp + "FFFF:"); rettype = "4"; } else { // same regex as myiptype 4 but different scenario Matcher mat = pattern4.matcher(myIP2); if (mat.matches()) { String v6part = mat.group(1); String v4part = mat.group(2); String[] v4arr = v4part.split("\\."); int[] v4intarr = new int[4]; int len = v4intarr.length; for (int x = 0; x < len; x++) { v4intarr[x] = Integer.parseInt(v4arr[x]); } int part1 = (v4intarr[0] << 8) + v4intarr[1]; int part2 = (v4intarr[2] << 8) + v4intarr[3]; String part1hex = Integer.toHexString(part1); String part2hex = Integer.toHexString(part2); StringBuffer bf = new StringBuffer(v6part.length() + 9); bf.append(v6part); bf.append(padme.substring(part1hex.length())); bf.append(part1hex); bf.append(":"); bf.append(padme.substring(part2hex.length())); bf.append(part2hex); myIP2 = bf.toString().toUpperCase(); String[] myarr = myIP2.split("::"); String[] leftside = myarr[0].split(":"); StringBuffer bf2 = new StringBuffer(40); StringBuffer bf3 = new StringBuffer(40); StringBuffer bf4 = new StringBuffer(40); len = leftside.length; int totalsegments = 0; for (int x = 0; x < len; x++) { if (leftside[x].length() > 0) { totalsegments++; bf2.append(padme.substring(leftside[x].length())); bf2.append(leftside[x]); bf2.append(":"); } } if (myarr.length > 1) { String[] rightside = myarr[1].split(":"); len = rightside.length; for (int x = 0; x < len; x++) { if (rightside[x].length() > 0) { totalsegments++; bf3.append(padme.substring(rightside[x].length())); bf3.append(rightside[x]); bf3.append(":"); } } } int totalsegmentsleft = 8 - totalsegments; if (totalsegmentsleft == 6) { for (int x = 1; x < totalsegmentsleft; x++) { bf4.append(padme); bf4.append(":"); } bf4.append("FFFF:"); bf4.append(v4part); rettype = "4"; myIP2 = bf4.toString(); } else { for (int x = 0; x < totalsegmentsleft; x++) { bf4.append(padme); bf4.append(":"); } bf2.append(bf4).append(bf3); myIP2 = bf2.toString().replaceAll(":$", ""); } } else { // expand IPv4-compatible IPv6 Matcher mat2 = pattern6.matcher(myIP2); if (mat2.matches()) { String mymatch = mat2.group(1); String[] myarr = mymatch.replaceAll("^:+", "").replaceAll(":+$", "").split(":"); int len = myarr.length; StringBuffer bf = new StringBuffer(32); for (int x = 0; x < len; x++) { String unpadded = myarr[x]; bf.append(padme.substring(unpadded.length())).append(unpadded); // safe padding for JDK 1.4 } long mylong = new BigInteger(bf.toString(), 16).longValue(); long[] b = {0, 0, 0, 0}; // using long in place of bytes due to 2's complement signed issue for (int x = 0; x < 4; x++) { b[x] = mylong & hexoffset; mylong = mylong >> 8; } myIP2 = myIP2.replaceAll(mymatch + "$", ":" + b[3] + "." + b[2] + "." + b[1] + "." + b[0]); myIP2 = myIP2.replaceAll("::", tmp + "FFFF:"); rettype = "4"; } else { // should be normal IPv6 case String[] myarr = myIP2.split("::"); String[] leftside = myarr[0].split(":"); StringBuffer bf2 = new StringBuffer(40); StringBuffer bf3 = new StringBuffer(40); StringBuffer bf4 = new StringBuffer(40); int len = leftside.length; int totalsegments = 0; for (int x = 0; x < len; x++) { if (leftside[x].length() > 0) { totalsegments++; bf2.append(padme.substring(leftside[x].length())); bf2.append(leftside[x]); bf2.append(":"); } } if (myarr.length > 1) { String[] rightside = myarr[1].split(":"); len = rightside.length; for (int x = 0; x < len; x++) { if (rightside[x].length() > 0) { totalsegments++; bf3.append(padme.substring(rightside[x].length())); bf3.append(rightside[x]); bf3.append(":"); } } } int totalsegmentsleft = 8 - totalsegments; for (int x = 0; x < totalsegmentsleft; x++) { bf4.append(padme); bf4.append(":"); } bf2.append(bf4).append(bf3); myIP2 = bf2.toString().replaceAll(":$", ""); } } } } return new String[]{myIP2, rettype}; } private float convertFloat(String mystr) { try { return Float.parseFloat(mystr); } catch (NumberFormatException e) { return 0.0F; } } private void reverse(byte[] array) { if (array == null) { return; } int i = 0; int j = array.length - 1; byte tmp; while (j > i) { tmp = array[j]; array[j] = array[i]; array[i] = tmp; j--; i++; } } private byte[] readRow(final long position, final long mylen, final ByteBuffer mybuffer, final FileLike filehandle) throws IOException { byte[] row = new byte[(int) mylen]; if (UseMemoryMappedFile) { mybuffer.position((int) position); mybuffer.get(row, 0, (int) mylen); } else { filehandle.seek(position - 1); filehandle.read(row, (int) 0, (int) mylen); } return row; } private BigInteger read32or128Row(byte[] row, final int from, final int len) throws IOException { byte[] buf = new byte[len]; System.arraycopy(row, from, buf, (int) 0, len); reverse(buf); return new BigInteger(1, buf); } private BigInteger read32or128(final long position, final int myiptype, final ByteBuffer mybuffer, final FileLike filehandle) throws IOException { if (myiptype == 4) { return read32(position, mybuffer, filehandle); } else if (myiptype == 6) { return read128(position, mybuffer, filehandle); } return BigInteger.ZERO; } private BigInteger read128(final long position, final ByteBuffer mybuffer, final FileLike filehandle) throws IOException { BigInteger retval; final int bsize = 16; byte[] buf = new byte[bsize]; if (UseMemoryMappedFile) { mybuffer.position((int) position); mybuffer.get(buf, 0, bsize); } else { filehandle.seek(position - 1); filehandle.read(buf, 0, bsize); } reverse(buf); retval = new BigInteger(1, buf); return retval; } private BigInteger read32Row(byte[] row, final int from) { final int len = 4; byte[] buf = new byte[len]; System.arraycopy(row, from, buf, (int) 0, len); reverse(buf); return new BigInteger(1, buf); } private BigInteger read32(final long position, final ByteBuffer mybuffer, final FileLike filehandle) throws IOException { if (UseMemoryMappedFile) { // simulate unsigned int by using long return BigInteger.valueOf(mybuffer.getInt((int) position) & 0xffffffffL); // use absolute offset to be thread-safe } else { final int bsize = 4; filehandle.seek(position - 1); byte[] buf = new byte[bsize]; filehandle.read(buf, (int) 0, bsize); reverse(buf); return new BigInteger(1, buf); } } private String readStr(long position, final ByteBuffer mydatabuffer, final FileLike filehandle) throws IOException { int size = 256; // max size of string field + 1 byte for the length final int len; final byte[] data = new byte[size]; byte[] buf; if (UseMemoryMappedFile) { position = position - _MapDataOffset; // position stored in BIN file is for full file, not just the mapped data segment, so need to minus try { mydatabuffer.position((int) position); if (mydatabuffer.remaining() < size) { size = mydatabuffer.remaining(); } mydatabuffer.get(data, 0, size); len = data[0]; buf = new byte[len]; System.arraycopy(data, 1, buf, (int) 0, len); } catch (NegativeArraySizeException e) { return null; } } else { filehandle.seek(position); try { filehandle.read(data, 0, size); len = data[0]; buf = new byte[len]; System.arraycopy(data, 1, buf, (int) 0, len); } catch (NegativeArraySizeException e) { return null; } } return new String(buf); } private float readFloatRow(byte[] row, final int from) { final int len = 4; byte[] buf = new byte[len]; System.arraycopy(row, from, buf, (int) 0, len); return Float.intBitsToFloat((buf[3] & 0xff) << 24 | (buf[2] & 0xff) << 16 | (buf[1] & 0xff) << 8 | (buf[0] & 0xff)); // the AND is converting byte to unsigned byte in the form of an int } private String setDecimalPlaces(float myfloat) { return GEO_COORDINATE_FORMAT.format(myfloat); } private BigInteger[] ip2No(String ipstring) throws UnknownHostException { BigInteger a1; BigInteger a2; BigInteger a3 = new BigInteger("4"); if (pattern.matcher(ipstring).matches()) { // should be IPv4 a1 = new BigInteger("4"); a2 = new BigInteger(String.valueOf(ipV4No(ipstring))); } else if (pattern2.matcher(ipstring).matches() || pattern3.matcher(ipstring).matches()) { throw new UnknownHostException(); } else { a3 = new BigInteger("6"); final InetAddress ia = InetAddress.getByName(ipstring); final byte[] byteArr = ia.getAddress(); String myiptype = "0"; // BigInteger needs String in the constructor if (ia instanceof Inet6Address) { myiptype = "6"; } else if (ia instanceof Inet4Address) { // this will run in cases of IPv4-mapped IPv6 addresses myiptype = "4"; } a2 = new BigInteger(1, byteArr); // confirmed correct for IPv6 if (a2.compareTo(FROM_6TO4) >= 0 && a2.compareTo(TO_6TO4) <= 0) { // 6to4 so need to remap to ipv4 myiptype = "4"; a2 = a2.shiftRight(80); a2 = a2.and(LAST_32BITS); a3 = new BigInteger("4"); } else if (a2.compareTo(FROM_TEREDO) >= 0 && a2.compareTo(TO_TEREDO) <= 0) { // Teredo so need to remap to ipv4 myiptype = "4"; a2 = a2.not(); a2 = a2.and(LAST_32BITS); a3 = new BigInteger("4"); } a1 = new BigInteger(myiptype); } return new BigInteger[]{a1, a2, a3}; } private long ipV4No(final String ipstring) { final String[] ipAddressInArray = ipstring.split("\\."); long result = 0; long ip; for (int x = 3; x >= 0; x--) { ip = Long.parseLong(ipAddressInArray[3 - x]); result |= ip << (x << 3); } return result; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy