diff --git a/.gitignore b/.gitignore
index 31c4496..842b789 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,4 @@ build
*.local
*.jks
.vscode
+**/tmp*
diff --git a/README.md b/README.md
index f265d6e..fecc076 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ This repository contains Android application sources of i2pd
sudo apt-get install g++ openjdk-11-jdk gradle
```
-If your system provides gradle with version < 5.1, download it from gradle homepage:
+If your system provides gradle with version < 5.1, download it from gradle homepage:
https://gradle.org/install/
@@ -54,10 +54,8 @@ export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
export ANDROID_HOME=/opt/android-sdk
export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/23.2.8568313
-pushd app/jni
-./build_boost.sh
-./build_openssl.sh
-./build_miniupnpc.sh
+pushd binary/jni
+./build_all.sh
popd
gradle clean assembleDebug
diff --git a/app/build.gradle b/app/build.gradle
index 5c94bfc..b29cb6c 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -28,8 +28,15 @@ android {
externalNativeBuild {
ndkBuild {
- arguments "NDK_MODULE_PATH:=${rootProject.projectDir}/app/jni"
+ arguments "NDK_MODULE_PATH:=${rootProject.projectDir}/binary/jni"
arguments "-j${Runtime.getRuntime().availableProcessors()}"
+ arguments 'V=1'
+ //targets
+ // You need to configure this executable and its sources in your
+ // Android.mk file like you would any other library, except you must
+ // specify "include $(BUILD_EXECUTABLE)". Building executables from
+ // your native sources is optional, and building native libraries to
+ // package into your APK satisfies most project requirements.
}
}
}
@@ -59,16 +66,19 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt'
}
debug {
- jniDebuggable = true
}
}
externalNativeBuild {
ndkBuild {
- path "${rootProject.projectDir}/app/jni/Android.mk"
+ path "${rootProject.projectDir}/binary/jni/Android.mk"
+ }
+ }
+ sourceSets {
+ main {
+ jniLibs.srcDir file("${rootProject.projectDir}/binary/libs")
}
}
-
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
diff --git a/app/src/main/java/org/purplei2p/i2pd/DaemonWrapper.java b/app/src/main/java/org/purplei2p/i2pd/DaemonWrapper.java
index 5ad2ac0..531ff79 100644
--- a/app/src/main/java/org/purplei2p/i2pd/DaemonWrapper.java
+++ b/app/src/main/java/org/purplei2p/i2pd/DaemonWrapper.java
@@ -13,6 +13,7 @@ import java.util.Set;
import java.util.Locale;
import android.annotation.TargetApi;
+import android.content.Context;
import android.content.res.AssetManager;
import android.net.ConnectivityManager;
import android.net.Network;
@@ -32,7 +33,9 @@ public class DaemonWrapper {
private String i2pdpath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/i2pd";
private boolean assetsCopied;
- private static final String appLocale = Locale.getDefault().getDisplayLanguage(Locale.ENGLISH).toLowerCase(); // lower-case system language (like "english")
+ private String getAppLocale() {
+ return Locale.getDefault().getDisplayLanguage(Locale.ENGLISH).toLowerCase(); // lower-case system language (like "english")
+ }
public interface StateUpdateListener {
void daemonStateUpdate(State oldValue, State newValue);
@@ -67,25 +70,25 @@ public class DaemonWrapper {
public synchronized void stopAcceptingTunnels() {
if (isStartedOkay()) {
setState(State.gracefulShutdownInProgress);
- I2PD_JNI.stopAcceptingTunnels();
+ I2pdApi.stopAcceptingTunnels();
}
}
public synchronized void startAcceptingTunnels() {
if (isStartedOkay()) {
setState(State.startedOkay);
- I2PD_JNI.startAcceptingTunnels();
+ I2pdApi.startAcceptingTunnels();
}
}
public synchronized void reloadTunnelsConfigs() {
if (isStartedOkay()) {
- I2PD_JNI.reloadTunnelsConfigs();
+ I2pdApi.reloadTunnelsConfigs();
}
}
public int getTransitTunnelsCount() {
- return I2PD_JNI.getTransitTunnelsCount();
+ return I2pdApi.getTransitTunnelsCount();
}
public enum State {
@@ -117,11 +120,11 @@ public class DaemonWrapper {
return state;
}
- public DaemonWrapper(AssetManager assetManager, ConnectivityManager connectivityManager){
+ public DaemonWrapper(Context ctx, AssetManager assetManager, ConnectivityManager connectivityManager){
this.assetManager = assetManager;
this.connectivityManager = connectivityManager;
setState(State.starting);
- startDaemon();
+ startDaemon(ctx);
}
private Throwable lastThrowable;
@@ -146,16 +149,6 @@ public class DaemonWrapper {
return daemonStartResult;
}
- public static String getDataDir() { // for settings iniEditor
- return I2PD_JNI.getDataDir();
- }
-
- public void changeDataDir(String dataDir, Boolean updateAssets) {
- I2PD_JNI.setDataDir(dataDir);
- if (updateAssets) processAssets();
- //ToDo: move old dir to new dir?
- }
-
public boolean isStartedOkay() {
return getState().isStartedOkay();
}
@@ -163,19 +156,18 @@ public class DaemonWrapper {
public synchronized void stopDaemon() {
if (isStartedOkay()) {
try {
- I2PD_JNI.stopDaemon();
+ I2pdApi.stopDaemon();
} catch (Throwable tr) {
Log.e(TAG, "", tr);
}
setState(State.stopped);
}
}
- public synchronized void startDaemon() {
+ public synchronized void startDaemon(Context ctx) {
if( getState() != State.stopped && getState() != State.starting ) return;
new Thread(() -> {
try {
processAssets();
- I2PD_JNI.loadLibraries();
//registerNetworkCallback();
} catch (Throwable tr) {
lastThrowable = tr;
@@ -184,12 +176,10 @@ public class DaemonWrapper {
}
try {
synchronized (DaemonWrapper.this) {
- I2PD_JNI.setDataDir(i2pdpath); // (Environment.getExternalStorageDirectory().getAbsolutePath() + "/i2pd");
-
- Log.i(TAG, "setting webconsole language to " + appLocale);
- I2PD_JNI.setLanguage(appLocale);
+ String locale = getAppLocale();
+ Log.i(TAG, "setting webconsole language to " + locale);
- daemonStartResult = I2PD_JNI.startDaemon();
+ daemonStartResult = I2pdApi.startDaemon(ctx, i2pdpath, locale);
if ("ok".equals(daemonStartResult)) {
setState(State.startedOkay);
} else
@@ -209,31 +199,13 @@ public class DaemonWrapper {
Log.d(TAG, "checking assets");
if (holderFile.exists()) {
- try { // if holder file exists, read assets version string
- FileReader fileReader = new FileReader(holderFile);
-
- try {
- BufferedReader br = new BufferedReader(fileReader);
-
- try {
+ try (FileReader fileReader = new FileReader(holderFile)) { // if holder file exists, read assets version string
+ try (BufferedReader br = new BufferedReader(fileReader)) {
String line;
while ((line = br.readLine()) != null) {
text.append(line);
}
- }finally {
- try {
- br.close();
- } catch (IOException e) {
- Log.e(TAG, "", e);
- }
- }
- } finally {
- try {
- fileReader.close();
- } catch (IOException e) {
- Log.e(TAG, "", e);
- }
}
} catch (IOException e) {
Log.e(TAG, "", e);
@@ -258,15 +230,9 @@ public class DaemonWrapper {
copyAsset("tunnels.conf");
// update holder file about successful copying
- FileWriter writer = new FileWriter(holderFile);
- try {
+ ;
+ try (FileWriter writer = new FileWriter(holderFile)) {
writer.append(versionName);
- } finally {
- try {
- writer.close();
- } catch (IOException e) {
- Log.e(TAG,"on writer close", e);
- }
}
}
catch (Throwable tr)
@@ -372,14 +338,14 @@ public class DaemonWrapper {
@Override
public void onAvailable(Network network) {
super.onAvailable(network);
- I2PD_JNI.onNetworkStateChanged(true);
+ I2pdApi.onNetworkStateChanged(true);
Log.d(TAG, "NetworkCallback.onAvailable");
}
@Override
public void onLost(Network network) {
super.onLost(network);
- I2PD_JNI.onNetworkStateChanged(false);
+ I2pdApi.onNetworkStateChanged(false);
Log.d(TAG, " NetworkCallback.onLost");
}
}
diff --git a/app/src/main/java/org/purplei2p/i2pd/I2PDActivity.java b/app/src/main/java/org/purplei2p/i2pd/I2PDActivity.java
index 0c0ecc7..febfe4e 100644
--- a/app/src/main/java/org/purplei2p/i2pd/I2PDActivity.java
+++ b/app/src/main/java/org/purplei2p/i2pd/I2PDActivity.java
@@ -45,11 +45,11 @@ public class I2PDActivity extends Activity {
public static final String PACKAGE_URI_SCHEME = "package:";
private TextView textView;
- private CheckBox HTTPProxyState;
- private CheckBox SOCKSProxyState;
- private CheckBox BOBState;
- private CheckBox SAMState;
- private CheckBox I2CPState;
+// private CheckBox HTTPProxyState;
+// private CheckBox SOCKSProxyState;
+// private CheckBox BOBState;
+// private CheckBox SAMState;
+// private CheckBox I2CPState;
private static volatile DaemonWrapper daemon;
@@ -75,13 +75,13 @@ public class I2PDActivity extends Activity {
DaemonWrapper.State state = daemon.getState();
- if (daemon.isStartedOkay()) {
- HTTPProxyState.setChecked(I2PD_JNI.getHTTPProxyState());
- SOCKSProxyState.setChecked(I2PD_JNI.getSOCKSProxyState());
- BOBState.setChecked(I2PD_JNI.getBOBState());
- SAMState.setChecked(I2PD_JNI.getSAMState());
- I2CPState.setChecked(I2PD_JNI.getI2CPState());
- }
+// if (daemon.isStartedOkay()) {
+// HTTPProxyState.setChecked(I2pdApi.getHTTPProxyState());
+// SOCKSProxyState.setChecked(I2pdApi.getSOCKSProxyState());
+// BOBState.setChecked(I2pdApi.getBOBState());
+// SAMState.setChecked(I2pdApi.getSAMState());
+// I2CPState.setChecked(I2pdApi.getI2CPState());
+// }
String startResultStr = DaemonWrapper.State.startFailed.equals(state) ? String.format(": %s", daemon.getDaemonStartResult()) : "";
String graceStr = DaemonWrapper.State.gracefulShutdownInProgress.equals(state) ? String.format(": %s %s", formatGraceTimeRemaining(), getText(R.string.remaining)) : "";
@@ -113,15 +113,15 @@ public class I2PDActivity extends Activity {
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(R.id.appStatusText);
- HTTPProxyState = (CheckBox) findViewById(R.id.service_httpproxy_box);
- SOCKSProxyState = (CheckBox) findViewById(R.id.service_socksproxy_box);
- BOBState = (CheckBox) findViewById(R.id.service_bob_box);
- SAMState = (CheckBox) findViewById(R.id.service_sam_box);
- I2CPState = (CheckBox) findViewById(R.id.service_i2cp_box);
+// HTTPProxyState = (CheckBox) findViewById(R.id.service_httpproxy_box);
+// SOCKSProxyState = (CheckBox) findViewById(R.id.service_socksproxy_box);
+// BOBState = (CheckBox) findViewById(R.id.service_bob_box);
+// SAMState = (CheckBox) findViewById(R.id.service_sam_box);
+// I2CPState = (CheckBox) findViewById(R.id.service_i2cp_box);
if (daemon == null) {
ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
- daemon = new DaemonWrapper(getAssets(), connectivityManager);
+ daemon = new DaemonWrapper(getApplicationContext(), getAssets(), connectivityManager);
}
ForegroundService.init(daemon);
@@ -261,6 +261,8 @@ public class I2PDActivity extends Activity {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.options_main, menu);
menu.findItem(R.id.action_battery_otimizations).setVisible(isBatteryOptimizationsOpenOsDialogApiAvailable());
+ //TODO
+ menu.findItem(R.id.action_reload_tunnels_config).setVisible(false);
this.optionsMenu = menu;
return true;
}
@@ -295,6 +297,7 @@ public class I2PDActivity extends Activity {
return true;
case R.id.action_reload_tunnels_config:
+ //TODO
onReloadTunnelsConfig();
return true;
@@ -302,7 +305,7 @@ public class I2PDActivity extends Activity {
if(daemon.isStartedOkay())
startActivity(new Intent(getApplicationContext(), WebConsoleActivity.class));
else
- Toast.makeText(this,"I2Pd not was started!", Toast.LENGTH_SHORT).show();
+ Toast.makeText(this, R.string.error_i2pd_not_running, Toast.LENGTH_SHORT).show();
return true;
case R.id.action_settings:
startActivity(new Intent(getApplicationContext(), SettingsActivity.class));
diff --git a/app/src/main/java/org/purplei2p/i2pd/I2PD_JNI.java b/app/src/main/java/org/purplei2p/i2pd/I2PD_JNI.java
deleted file mode 100644
index c1f7d1c..0000000
--- a/app/src/main/java/org/purplei2p/i2pd/I2PD_JNI.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package org.purplei2p.i2pd;
-
-public class I2PD_JNI {
- public static native String getABICompiledWith();
-
- public static void loadLibraries() {
- System.loadLibrary("i2pd");
- }
-
- /**
- * returns error info if failed
- * returns "ok" if daemon initialized and started okay
- */
- public static native String startDaemon();
- public static native void stopDaemon();
-
- public static native void startAcceptingTunnels();
- public static native void stopAcceptingTunnels();
- public static native void reloadTunnelsConfigs();
-
- public static native void setDataDir(String jdataDir);
- public static native void setLanguage(String jlanguage);
-
- public static native int getTransitTunnelsCount();
- public static native String getWebConsAddr();
- public static native String getDataDir();
-
- public static native boolean getHTTPProxyState();
- public static native boolean getSOCKSProxyState();
- public static native boolean getBOBState();
- public static native boolean getSAMState();
- public static native boolean getI2CPState();
-
- public static native void onNetworkStateChanged(boolean isConnected);
-}
diff --git a/app/src/main/java/org/purplei2p/i2pd/I2pdApi.java b/app/src/main/java/org/purplei2p/i2pd/I2pdApi.java
new file mode 100644
index 0000000..cb62d0f
--- /dev/null
+++ b/app/src/main/java/org/purplei2p/i2pd/I2pdApi.java
@@ -0,0 +1,99 @@
+package org.purplei2p.i2pd;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+
+/** i2pd process API calls via TCP between the Android Java app and i2pd C++-only process.
+ * TODO
+ */
+public class I2pdApi {
+ private static String dataDir;
+ private static Process i2pdProcess;
+ private static final String TAG = "I2pdApi";
+
+ /**
+ * returns error info if failed
+ * returns "ok" if daemon initialized and started okay
+ */
+ public static String startDaemon(Context ctx, String dataDir, String language){
+ try {
+ I2pdApi.dataDir = dataDir;
+ Process p = I2pdApi.i2pdProcess = Runtime.getRuntime().exec(new String[] {
+ ctx.getApplicationInfo().nativeLibraryDir+"/libi2pd.so",
+ "--datadir="+dataDir
+ });
+ new Thread(() -> {
+ try {
+ try (BufferedInputStream bis = new BufferedInputStream(p.getInputStream())) {
+ try (InputStreamReader sr = new InputStreamReader(bis)) {
+ try (BufferedReader r = new BufferedReader(sr)) {
+ while(true){
+ String s = r.readLine();
+ if (s == null) break;
+ Log.i(TAG, s);
+ }
+ }
+ }
+ }
+ }catch(Throwable tr){
+ Log.e(TAG, "", tr);
+ }
+ }, "i2pd-stdout");
+ new Thread(() -> {
+ try {
+ try (BufferedInputStream bis = new BufferedInputStream(p.getErrorStream())) {
+ try (InputStreamReader sr = new InputStreamReader(bis)) {
+ try (BufferedReader r = new BufferedReader(sr)) {
+ while(true){
+ String s = r.readLine();
+ if (s == null) break;
+ Log.i(TAG, s);
+ }
+ }
+ }
+ }
+ }catch(Throwable tr){
+ Log.e(TAG, "", tr);
+ }
+ }, "i2pd-stderr");
+ return "ok";
+ } catch (Throwable tr) {
+ Log.e(TAG, "", tr);
+ return "Error in exec(): " + tr;
+ }
+ }
+
+ public static void stopDaemon(){
+ Process p = i2pdProcess;
+ if (p != null) {
+ try {
+ p.destroy();
+ } catch (Throwable tr) {
+ Log.e(TAG, "", tr);
+ }
+ i2pdProcess = null;
+ }
+ }
+
+ public static void startAcceptingTunnels(){}
+ public static void stopAcceptingTunnels(){}
+ public static void reloadTunnelsConfigs(){}
+
+ public static int getTransitTunnelsCount(){return -1;}
+ public static String getWebConsAddr(){return "";}
+ public static String getDataDir() {
+ return dataDir;
+ }
+
+ public static boolean getHTTPProxyState(){return false;}
+ public static boolean getSOCKSProxyState(){return false;}
+ public static boolean getBOBState(){return false;}
+ public static boolean getSAMState(){return false;}
+ public static boolean getI2CPState(){return false;}
+
+ public static void onNetworkStateChanged(boolean isConnected){}
+}
diff --git a/app/src/main/java/org/purplei2p/i2pd/NetworkStateChangeReceiver.java b/app/src/main/java/org/purplei2p/i2pd/NetworkStateChangeReceiver.java
index 35975a0..a440d99 100644
--- a/app/src/main/java/org/purplei2p/i2pd/NetworkStateChangeReceiver.java
+++ b/app/src/main/java/org/purplei2p/i2pd/NetworkStateChangeReceiver.java
@@ -19,7 +19,7 @@ public class NetworkStateChangeReceiver extends BroadcastReceiver {
NetworkInfo activeNetworkInfo = cm.getActiveNetworkInfo();
boolean isConnected = activeNetworkInfo != null && activeNetworkInfo.isConnected();
- I2PD_JNI.onNetworkStateChanged(isConnected);
+ I2pdApi.onNetworkStateChanged(isConnected);
} catch (Throwable tr) {
Log.e(TAG, "", tr);
}
diff --git a/app/src/main/java/org/purplei2p/i2pd/SettingsActivity.java b/app/src/main/java/org/purplei2p/i2pd/SettingsActivity.java
index 43d9b1a..0075959 100644
--- a/app/src/main/java/org/purplei2p/i2pd/SettingsActivity.java
+++ b/app/src/main/java/org/purplei2p/i2pd/SettingsActivity.java
@@ -18,11 +18,9 @@ import java.util.List;
import java.util.Objects;
-//import org.purplei2p.i2pd.iniedotr.IniEditor;
public class SettingsActivity extends Activity {
- //protected IniEditor iniedit = new IniEditor();
- private String TAG = "i2pdSrvcSettings";
+ private static final String TAG = "i2pdSttngs";
private File cacheDir;
public static String onBootFileName = "/onBoot"; // just file, empty, if exist the do autostart, if not then no.
@@ -59,7 +57,7 @@ public class SettingsActivity extends Activity {
startActivity(intent);
}
} catch (Exception e) {
- Log.e("exceptionAutostarti2pd" , String.valueOf(e));
+ Log.e("Autostarti2pd" , "", e);
}
}
@@ -99,7 +97,7 @@ public class SettingsActivity extends Activity {
if (!onBoot.createNewFile())
Log.d(TAG, "Cant create new wile on: "+onBoot.getAbsolutePath());
} catch (Exception e) {
- Log.e(TAG, "error: " + e.toString());
+ Log.e(TAG, "", e);
}
}
} else {
diff --git a/app/src/main/java/org/purplei2p/i2pd/WebConsoleActivity.java b/app/src/main/java/org/purplei2p/i2pd/WebConsoleActivity.java
index e96d068..9f563fb 100644
--- a/app/src/main/java/org/purplei2p/i2pd/WebConsoleActivity.java
+++ b/app/src/main/java/org/purplei2p/i2pd/WebConsoleActivity.java
@@ -27,7 +27,7 @@ public class WebConsoleActivity extends Activity {
final WebSettings webSettings = webView.getSettings();
webSettings.setBuiltInZoomControls(true);
webSettings.setJavaScriptEnabled(false);
- webView.loadUrl(I2PD_JNI.getWebConsAddr());
+ webView.loadUrl(I2pdApi.getWebConsAddr());
swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe);
swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
diff --git a/app/src/main/java/org/purplei2p/i2pd/iniedotr/IniEditor.java b/app/src/main/java/org/purplei2p/i2pd/iniedotr/IniEditor.java
deleted file mode 100644
index 8bb2b4c..0000000
--- a/app/src/main/java/org/purplei2p/i2pd/iniedotr/IniEditor.java
+++ /dev/null
@@ -1,1182 +0,0 @@
-/*
- IniEditor is Copyright (c) 2003-2013, Nik Haldimann
- All rights reserved. Distributed under a BSD-style license.
-
- Redistribution and use in source and binary forms, with or without
- modification, are permitted provided that the following conditions are
- met:
-
- * Redistributions of source code must retain the above copyright notice,
- this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above copyright
- notice, this list of conditions and the following disclaimer in the
- documentation and/or other materials provided with the distribution.
-
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
- IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
- TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
- OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
- EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
- PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
- LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-*/
-
-package org.purplei2p.i2pd.iniedotr;
-
-import java.io.*;
-import java.util.*;
-
-/**
- * Loads, edits and saves INI-style configuration files. While loading from and
- * saving to streams and files, IniEditor
preserves comments and
- * blank lines as well as the order of sections and lines in general.
- *
- * IniEditor
assumes configuration files to be split in sections.
- * A section starts out with a header, which consists of the section name
- * enclosed in brackets ('['
and ']'
). Everything
- * before the first section header is ignored when loading from a stream or
- * file. The {@link IniEditor.Section}
class can be used to load
- * configuration files without sections (ie Java-style properties).
- *
- * A "common section" may be named. All sections inherit the options of this - * section but can overwrite them. - *
- * IniEditor
represents an INI file (or rather, its sections) line
- * by line, as comment, blank and option lines. A comment is a line which has a
- * comment delimiter as its first non-white space character. The default comment
- * delimiters, which may be overwritten, are '#'
and
- * ';'
.
- *
- * A blank line is any line that consists only of white space. - *
- * Everything else is an option line. Option names and values are separated by
- * option delimiters '='
, ':'
or white space (spaces
- * and tabs).
- *
- * Here's a minimal example. Suppose, we have this in a file called
- * users.ini
:
- *
- * [root] - * role = administrator - * last_login = 2003-05-04 - * - * [joe] - * role = author - * last_login = 2003-05-13 - *- * Let's load that file, add something to it and save the changes: - *
- * IniEditor users = new IniEditor(); - * users.load("users.ini"); - * users.set("root", "last_login", "2003-05-16"); - * users.addComment("root", "Must change password often"); - * users.set("root", "change_pwd", "10 days"); - * users.addBlankLine("root"); - * users.save("users.ini"); - *- * Now, the file looks like this: - *
- * [root] - * role = administrator - * last_login = 2003-05-16 - * - * # Must change password often - * change_pwd = 10 days - * - * [joe] - * role = author - * last_login = 2003-05-13 - *- *
- * IniEditor provides services simliar to the standard Java API class
- * java.util.Properties
. It uses its own parser, though, which
- * differs in these respects from that of Properties
:
- *
'='
, ':'
and white space).%s
three times, these will be replaced
- * with the option name, the option separator and the option value in this
- * order. Literal percentage signs must be escaped by preceding them with
- * another percentage sign (i.e., %%
corresponds to one
- * percentage sign). The default format string is "%s %s %s"
.
- *
- * Option formats may look like format strings as supported by Java 1.5, but
- * the string is in fact parsed in a custom fashion to guarantee backwards
- * compatibility. So don't try clever stuff like using format conversion
- * types other than %s
.
- *
- * @param formatString a format string, containing %s
exactly
- * three times
- * @throws IllegalArgumentException if the format string is illegal
- */
- public void setOptionFormatString(String formatString) {
- this.optionFormat = new OptionFormat(formatString);
- }
-
- /**
- * Returns the value of a given option in a given section or null if either
- * the section or the option don't exist. If a common section was defined
- * options are also looked up there if they're not present in the specific
- * section.
- *
- * @param section the section's name
- * @param option the option's name
- * @return the option's value
- * @throws NullPointerException any of the arguments is null
- */
- public String get(String section, String option) {
- if (hasSection(section)) {
- Section sect = getSection(section);
- if (sect.hasOption(option)) {
- return sect.get(option);
- }
- if (this.commonName != null) {
- return getSection(this.commonName).get(option);
- }
- }
- return null;
- }
-
- /**
- * Returns a map of a given section with option/value pairs or null if the
- * section doesn't exist.
- *
- * @param section the section's name
- * @return HashMap of option/value pairs from the section
- * @throws NullPointerException if section is null
- */
- public Map< String, String > getSectionMap(String section) {
- Map< String, String > sectionMap = new HashMap< String, String >();
- if (hasSection(section)) {
- Section sect = getSection(section);
- for(String key : sect.options.keySet()) {
- sectionMap.put(key, sect.options.get(key).value);
- }
- return sectionMap;
- }
- return null;
- }
-
- /**
- * Sets the value of an option in a section, if the option exist, otherwise
- * adds the option to the section. Trims white space from the start and the
- * end of the value and deletes newline characters it might contain.
- *
- * @param section the section's name
- * @param option the option's name
- * @param value the option's value
- * @throws IniEditor.NoSuchSectionException no section with the given name exists
- * @throws IllegalArgumentException the option name is illegal,
- * ie contains a '=' character or consists only of white space
- * @throws NullPointerException section or option are null
- */
- public void set(String section, String option, String value) {
- if (hasSection(section)) {
- getSection(section).set(option, value);
- }
- else {
- throw new NoSuchSectionException(section);
- }
- }
-
- /**
- * Removes an option from a section if it exists. Will not remove options
- * from the common section if it's not directly addressed.
- *
- * @param section the section's name
- * @param option the option's name
- * @return true
if the option was actually removed
- * @throws IniEditor.NoSuchSectionException no section with the given name exists
- */
- public boolean remove(String section, String option) {
- if (hasSection(section)) {
- return getSection(section).remove(option);
- }
- else {
- throw new NoSuchSectionException(section);
- }
- }
-
- /**
- * Checks whether an option exists in a given section. Options in the
- * common section are assumed to not exist in particular sections,
- * unless they're overwritten.
- *
- * @param section the section's name
- * @param option the option's name
- * @return true if the given section has the option
- */
- public boolean hasOption(String section, String option) {
- return hasSection(section) && getSection(section).hasOption(option);
- }
-
- /**
- * Checks whether a section with a particular name exists in this instance.
- *
- * @param name the name of the section
- * @return true if the section exists
- */
- public boolean hasSection(String name) {
- return this.sections.containsKey(normSection(name));
- }
-
- /**
- * Adds a section if it doesn't exist yet.
- *
- * @param name the name of the section
- * @return true
if the section didn't already exist
- * @throws IllegalArgumentException the name is illegal, ie contains one
- * of the characters '[' and ']' or consists only of white space
- */
- public boolean addSection(String name) {
- String normName = normSection(name);
- if (!hasSection(normName)) {
- // Section constructor might throw IllegalArgumentException
- Section section = new Section(normName, this.commentDelims,
- this.isCaseSensitive);
- section.setOptionFormat(this.optionFormat);
- this.sections.put(normName, section);
- this.sectionOrder.add(normName);
- return true;
- }
- else {
- return false;
- }
- }
-
- /**
- * Removes a section if it exists.
- *
- * @param name the section's name
- * @return true
if the section actually existed
- * @throws IllegalArgumentException when trying to remove the common section
- */
- public boolean removeSection(String name) {
- String normName = normSection(name);
- if (this.commonName != null && this.commonName.equals(normName)) {
- throw new IllegalArgumentException("Can't remove common section");
- }
- if (hasSection(normName)) {
- this.sections.remove(normName);
- this.sectionOrder.remove(normName);
- return true;
- }
- else {
- return false;
- }
- }
-
- /**
- * Returns all section names in this instance minus the common section if
- * one was defined.
- *
- * @return list of the section names in original/insertion order
- */
- public ListOutputStream
for maximum flexibility, internally
- * it does of course use a writer for character based output.
- *
- * @param stream where to write
- * @throws IOException at an I/O problem
- */
- public void save(OutputStream stream) throws IOException {
- save(new OutputStreamWriter(stream));
- }
-
- /**
- * Writes this instance in INI format to an output stream writer.
- *
- * @param streamWriter where to write
- * @throws IOException at an I/O problem
- */
- public void save(OutputStreamWriter streamWriter) throws IOException {
- IteratorInputStream
- * for maximum flexibility, internally it does use a reader (using the
- * default character encoding) for character based input. Everything in the
- * stream before the first section header is ignored.
- *
- * @param stream where to read from
- * @throws IOException at an I/O problem
- */
- public void load(InputStream stream) throws IOException {
- load(new InputStreamReader(stream));
- }
-
- /**
- * Loads INI formatted input from a stream reader into this instance.
- * Everything in the stream before the first section header is ignored.
- *
- * @param streamReader where to read from
- * @throws IOException at an I/O problem
- */
- public void load(InputStreamReader streamReader) throws IOException {
- BufferedReader reader = new BufferedReader(streamReader);
- String curSection = null;
- String line = null;
-
- while (reader.ready()) {
- line = reader.readLine().trim();
- if (line.length() > 0 && line.charAt(0) == Section.HEADER_START) {
- int endIndex = line.indexOf(Section.HEADER_END);
- if (endIndex >= 0) {
- curSection = line.substring(1, endIndex);
- addSection(curSection);
- }
- }
- if (curSection != null) {
- Section sect = getSection(curSection);
- sect.load(reader);
- }
- }
- }
-
- /**
- * Returns a section by name or null
if not found.
- *
- * @param name the section's name
- * @return the section
- */
- private Section getSection(String name) {
- return sections.get(normSection(name));
- }
-
- /**
- * Normalizes an arbitrary string for use as a section name. Currently
- * only makes the string lower-case (provided this instance isn't case-
- * sensitive) and trims leading and trailing white space. Note that
- * normalization isn't enforced by the Section class.
- *
- * @param name the string to be used as section name
- * @return a normalized section name
- */
- private String normSection(String name) {
- if (!this.isCaseSensitive) {
- name = name.toLowerCase();
- }
- return name.trim();
- }
-
- private static String[] toStringArray(Collection coll) {
- Object[] objArray = coll.toArray();
- String[] strArray = new String[objArray.length];
- for (int i = 0; i < objArray.length; i++) {
- strArray[i] = (String)objArray[i];
- }
- return strArray;
- }
-
- /**
- * Loads, edits and saves a section of an INI-style configuration file. This
- * class does actually belong to the internals of {@link IniEditor} and
- * should rarely ever be used directly. It's exposed because it can be
- * useful for plain, section-less configuration files (Java-style
- * properties, for example).
- */
- public static class Section {
-
- private String name;
- private Map%s
three times, these
- * will be replaced with the option name, the option separator and the
- * option value in this order. Literal percentage signs must be escaped
- * by preceding them with another percentage sign (i.e., %%
- * corresponds to one percentage sign). The default format string is
- * "%s %s %s"
.
- *
- * Option formats may look like format strings as supported by Java 1.5,
- * but the string is in fact parsed in a custom fashion to guarantee
- * backwards compatibility. So don't try clever stuff like using format
- * conversion types other than %s
.
- *
- * @param formatString a format string, containing %s
- * exactly three times
- * @throws IllegalArgumentException if the format string is illegal
- */
- public void setOptionFormatString(String formatString) {
- this.setOptionFormat(new OptionFormat(formatString));
- }
-
- /**
- * Sets the option format for this section. Options will be rendered
- * according to the given format when printed.
- *
- * @param format a compiled option format
- */
- public void setOptionFormat(OptionFormat format) {
- this.optionFormat = format;
- }
-
- /**
- * Returns the names of all options in this section.
- *
- * @return list of names of this section's options in
- * original/insertion order
- */
- public Listnull
if no
- * option with the specified name exists
- */
- public String get(String option) {
- String normed = normOption(option);
- if (hasOption(normed)) {
- return getOption(normed).value();
- }
- return null;
- }
-
- /**
- * Sets an option's value and creates the option if it doesn't exist.
- *
- * @param option the option's name
- * @param value the option's value
- * @throws IllegalArgumentException the option name is illegal,
- * ie contains a '=' character or consists only of white space
- */
- public void set(String option, String value) {
- set(option, value, this.optionDelims[0]);
- }
-
- /**
- * Sets an option's value and creates the option if it doesn't exist.
- *
- * @param option the option's name
- * @param value the option's value
- * @param delim the delimiter between name and value for this option
- * @throws IllegalArgumentException the option name is illegal,
- * ie contains a '=' character or consists only of white space
- */
- public void set(String option, String value, char delim) {
- String normed = normOption(option);
- if (hasOption(normed)) {
- getOption(normed).set(value);
- }
- else {
- // Option constructor might throw IllegalArgumentException
- Option opt = new Option(normed, value, delim, this.optionFormat);
- this.options.put(normed, opt);
- this.lines.add(opt);
- }
- }
-
- /**
- * Removes an option if it exists.
- *
- * @param option the name of the option
- * @return true
if the option was actually removed
- */
- public boolean remove(String option) {
- String normed = normOption(option);
- if (hasOption(normed)) {
- this.lines.remove(getOption(normed));
- this.options.remove(normed);
- return true;
- }
- else {
- return false;
- }
- }
-
- /**
- * Adds a comment line to the end of this section. A comment spanning
- * several lines (ie with line breaks) will be split up, one comment
- * line for each line.
- *
- * @param comment the comment
- */
- public void addComment(String comment) {
- addComment(comment, this.commentDelims[0]);
- }
-
- /**
- * Adds a comment line to the end of this section. A comment spanning
- * several lines (ie with line breaks) will be split up, one comment
- * line for each line.
- *
- * @param comment the comment
- * @param delim the delimiter used to mark the start of this comment
- */
- public void addComment(String comment, char delim) {
- StringTokenizer st = new StringTokenizer(comment.trim(), NEWLINE_CHARS);
- while (st.hasMoreTokens()) {
- this.lines.add(new Comment(st.nextToken(), delim));
- }
- }
- private static final String NEWLINE_CHARS = "\n\r";
-
- /**
- * Adds a blank line to the end of this section.
- */
- public void addBlankLine() {
- this.lines.add(BLANK_LINE);
- }
-
- /**
- * Loads options from a reader into this instance. Will read from the
- * stream until it hits a section header, ie a '[' character, and resets
- * the reader to point to this character.
- *
- * @param reader where to read from
- * @throws IOException at an I/O problem
- */
- public void load(BufferedReader reader) throws IOException {
- while (reader.ready()) {
- reader.mark(NAME_MAXLENGTH);
- String line = reader.readLine().trim();
-
- // Check for section header
- if (line.length() > 0 && line.charAt(0) == HEADER_START) {
- reader.reset();
- return;
- }
-
- int delimIndex = -1;
- // blank line
- if (line.equals("")) {
- this.addBlankLine();
- }
- // comment line
- else if ((delimIndex = Arrays.binarySearch(this.commentDelimsSorted, line.charAt(0))) >= 0) {
- addComment(line.substring(1), this.commentDelimsSorted[delimIndex]);
- }
- // option line
- else {
- delimIndex = -1;
- int delimNum = -1;
- int lastSpaceIndex = -1;
- for (int i = 0, l = line.length(); i < l && delimIndex < 0; i++) {
- delimNum = Arrays.binarySearch(this.optionDelimsSorted, line.charAt(i));
- if (delimNum >= 0) {
- delimIndex = i;
- }
- else {
- boolean isSpace = Arrays.binarySearch(
- this.OPTION_DELIMS_WHITESPACE, line.charAt(i)) >= 0;
- if (!isSpace && lastSpaceIndex >= 0) {
- break;
- }
- else if (isSpace) {
- lastSpaceIndex = i;
- }
- }
- }
- // delimiter at start of line
- if (delimIndex == 0) {
- // XXX what's a man got to do?
- }
- // no delimiter found
- else if (delimIndex < 0) {
- if (lastSpaceIndex < 0) {
- this.set(line, "");
- }
- else {
- this.set(line.substring(0, lastSpaceIndex)
- , line.substring(lastSpaceIndex+1));
- }
- }
- // delimiter found
- else {
- this.set(line.substring(0, delimIndex)
- , line.substring(delimIndex+1), line.charAt(delimIndex));
- }
- }
- }
- }
-
- /**
- * Prints this section to a print writer.
- *
- * @param writer where to write
- * @throws IOException at an I/O problem
- */
- public void save(PrintWriter writer) throws IOException {
- Iterator