auto copy original apk to assets

This commit is contained in:
327135569 2021-01-31 12:06:57 +08:00
parent 2c80821646
commit 581aaf74f7
3 changed files with 796 additions and 788 deletions

View File

@ -1,309 +1,315 @@
package com.storm.wind.xpatch; package com.storm.wind.xpatch;
import com.storm.wind.xpatch.base.BaseCommand; import com.storm.wind.xpatch.base.BaseCommand;
import com.storm.wind.xpatch.task.ApkModifyTask; import com.storm.wind.xpatch.task.ApkModifyTask;
import com.storm.wind.xpatch.task.BuildAndSignApkTask; import com.storm.wind.xpatch.task.BuildAndSignApkTask;
import com.storm.wind.xpatch.task.SaveApkSignatureTask; import com.storm.wind.xpatch.task.SaveApkSignatureTask;
import com.storm.wind.xpatch.task.SaveOriginalApplicationNameTask; import com.storm.wind.xpatch.task.SaveOriginalApplicationNameTask;
import com.storm.wind.xpatch.task.SoAndDexCopyTask; import com.storm.wind.xpatch.task.SoAndDexCopyTask;
import com.storm.wind.xpatch.util.FileUtils; import com.storm.wind.xpatch.util.FileUtils;
import com.storm.wind.xpatch.util.ManifestParser; import com.storm.wind.xpatch.util.ManifestParser;
import com.wind.meditor.core.FileProcesser; import com.wind.meditor.core.FileProcesser;
import com.wind.meditor.property.AttributeItem; import com.wind.meditor.property.AttributeItem;
import com.wind.meditor.property.ModificationProperty; import com.wind.meditor.property.ModificationProperty;
import com.wind.meditor.utils.NodeValue; import com.wind.meditor.utils.NodeValue;
import java.io.File; import java.io.File;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class MainCommand extends BaseCommand { public class MainCommand extends BaseCommand {
private String apkPath; private String apkPath;
private String unzipApkFilePath; private String unzipApkFilePath;
@Opt(opt = "o", longOpt = "output", description = "output .apk file, default is " + @Opt(opt = "o", longOpt = "output", description = "output .apk file, default is " +
"$source_apk_dir/[file-name]-xposed-signed.apk", argName = "out-apk-file") "$source_apk_dir/[file-name]-xposed-signed.apk", argName = "out-apk-file")
private String output; // 输出的apk文件的目录以及名称 private String output; // 输出的apk文件的目录以及名称
@Opt(opt = "f", longOpt = "force", hasArg = false, description = "force overwrite") @Opt(opt = "f", longOpt = "force", hasArg = false, description = "force overwrite")
private boolean forceOverwrite = false; private boolean forceOverwrite = false;
@Opt(opt = "k", longOpt = "keep", hasArg = false, description = "not delete the jar file " + @Opt(opt = "k", longOpt = "keep", hasArg = false, description = "not delete the jar file " +
"that is changed by dex2jar and the apk zip files") "that is changed by dex2jar and the apk zip files")
private boolean keepBuildFiles = false; private boolean keepBuildFiles = false;
@Opt(opt = "l", longOpt = "log", hasArg = false, description = "show all the debug logs") @Opt(opt = "l", longOpt = "log", hasArg = false, description = "show all the debug logs")
private boolean showAllLogs = false; private boolean showAllLogs = false;
@Opt(opt = "c", longOpt = "crach", hasArg = false, @Opt(opt = "c", longOpt = "crach", hasArg = false,
description = "disable craching the apk's signature.") description = "disable craching the apk's signature.")
private boolean disableCrackSignature = false; private boolean disableCrackSignature = false;
@Opt(opt = "xm", longOpt = "xposed-modules", description = "the xposed module files to be packaged into the apk, " + @Opt(opt = "xm", longOpt = "xposed-modules", description = "the xposed module files to be packaged into the apk, " +
"multi files should be seperated by :(mac) or ;(win) ", argName = "xposed module file path") "multi files should be seperated by :(mac) or ;(win) ", argName = "xposed module file path")
private String xposedModules; private String xposedModules;
// 使用dex文件中插入代码的方式修改apk而不是默认的修改Manifest中Application name的方式 // 使用dex文件中插入代码的方式修改apk而不是默认的修改Manifest中Application name的方式
@Opt(opt = "dex", longOpt = "dex", hasArg = false, description = "insert code into the dex file, not modify manifest application name attribute") @Opt(opt = "dex", longOpt = "dex", hasArg = false, description = "insert code into the dex file, not modify manifest application name attribute")
private boolean dexModificationMode = false; private boolean dexModificationMode = false;
@Opt(opt = "pkg", longOpt = "packageName", description = "modify the apk package name", argName = "new package name") @Opt(opt = "pkg", longOpt = "packageName", description = "modify the apk package name", argName = "new package name")
private String newPackageName; private String newPackageName;
@Opt(opt = "d", longOpt = "debuggable", description = "set 1 to make the app debuggable = true, " + @Opt(opt = "d", longOpt = "debuggable", description = "set 1 to make the app debuggable = true, " +
"set 0 to make the app debuggable = false", argName = "0 or 1") "set 0 to make the app debuggable = false", argName = "0 or 1")
private int debuggable = -1; // 0: debuggable = false 1: debuggable = true private int debuggable = -1; // 0: debuggable = false 1: debuggable = true
@Opt(opt = "vc", longOpt = "versionCode", description = "set the app version code", @Opt(opt = "vc", longOpt = "versionCode", description = "set the app version code",
argName = "new-version-code") argName = "new-version-code")
private int versionCode; private int versionCode;
@Opt(opt = "vn", longOpt = "versionName", description = "set the app version name", @Opt(opt = "vn", longOpt = "versionName", description = "set the app version name",
argName = "new-version-name") argName = "new-version-name")
private String versionName; private String versionName;
@Opt(opt = "w", longOpt = "whale", hasArg = false, description = "Change hook framework to Lody's whale") @Opt(opt = "w", longOpt = "whale", hasArg = false, description = "Change hook framework to Lody's whale")
private boolean useWhaleHookFramework = false; // 是否使用whale hook框架默认使用的是SandHook private boolean useWhaleHookFramework = false; // 是否使用whale hook框架默认使用的是SandHook
// 原来apk中dex文件的数量 // 原来apk中dex文件的数量
private int dexFileCount = 0; private int dexFileCount = 0;
private static final String UNZIP_APK_FILE_NAME = "apk-unzip-files"; private static final String UNZIP_APK_FILE_NAME = "apk-unzip-files";
private static final String PROXY_APPLICATION_NAME = "com.wind.xpatch.proxy.XpatchProxyApplication"; private static final String PROXY_APPLICATION_NAME = "com.wind.xpatch.proxy.XpatchProxyApplication";
private List<Runnable> mXpatchTasks = new ArrayList<>(); private List<Runnable> mXpatchTasks = new ArrayList<>();
public static void main(String... args) { public static void main(String... args) {
new MainCommand().doMain(args); new MainCommand().doMain(args);
} }
@Override @Override
protected void doCommandLine() { protected void doCommandLine() {
if (remainingArgs.length != 1) { if (remainingArgs.length != 1) {
if (remainingArgs.length == 0) { if (remainingArgs.length == 0) {
System.out.println("Please choose one apk file you want to process. "); System.out.println("Please choose one apk file you want to process. ");
} }
if (remainingArgs.length > 1) { if (remainingArgs.length > 1) {
System.out.println("This tool can only used with one apk file."); System.out.println("This tool can only used with one apk file.");
} }
usage(); usage();
return; return;
} }
apkPath = remainingArgs[0]; apkPath = remainingArgs[0];
File srcApkFile = new File(apkPath); File srcApkFile = new File(apkPath);
if (!srcApkFile.exists()) { if (!srcApkFile.exists()) {
System.out.println(" The source apk file not exsit, please choose another one. " + System.out.println(" The source apk file not exsit, please choose another one. " +
"current apk file is = " + apkPath); "current apk file is = " + apkPath);
return; return;
} }
String currentDir = new File(".").getAbsolutePath(); // 当前命令行所在的目录 String currentDir = new File(".").getAbsolutePath(); // 当前命令行所在的目录
if (showAllLogs) { if (showAllLogs) {
System.out.println(" currentDir = " + currentDir + " \n apkPath = " + apkPath); System.out.println(" currentDir = " + currentDir + " \n apkPath = " + apkPath);
} }
if (output == null || output.length() == 0) { if (output == null || output.length() == 0) {
output = getBaseName(apkPath) + "-xposed-signed.apk"; output = getBaseName(apkPath) + "-xposed-signed.apk";
} }
File outputFile = new File(output); File outputFile = new File(output);
if (outputFile.exists() && !forceOverwrite) { if (outputFile.exists() && !forceOverwrite) {
System.err.println(output + " exists, use --force to overwrite"); System.err.println(output + " exists, use --force to overwrite");
usage(); usage();
return; return;
} }
String outputApkFileParentPath = outputFile.getParent(); String outputApkFileParentPath = outputFile.getParent();
if (outputApkFileParentPath == null) { if (outputApkFileParentPath == null) {
String absPath = outputFile.getAbsolutePath(); String absPath = outputFile.getAbsolutePath();
int index = absPath.lastIndexOf(File.separatorChar); int index = absPath.lastIndexOf(File.separatorChar);
outputApkFileParentPath = absPath.substring(0, index); outputApkFileParentPath = absPath.substring(0, index);
} }
System.out.println(" !!!!! output apk path --> " + output + System.out.println(" !!!!! output apk path --> " + output +
" disableCrackSignature --> " + disableCrackSignature); " disableCrackSignature --> " + disableCrackSignature);
String apkFileName = getBaseName(srcApkFile); String apkFileName = getBaseName(srcApkFile);
// 中间文件临时存储的位置 // 中间文件临时存储的位置
String tempFilePath = outputApkFileParentPath + File.separator + String tempFilePath = outputApkFileParentPath + File.separator +
currentTimeStr() + "-tmp" + File.separator; currentTimeStr() + "-tmp" + File.separator;
// apk文件解压的目录 // apk文件解压的目录
unzipApkFilePath = tempFilePath + apkFileName + "-" + UNZIP_APK_FILE_NAME + File.separator; unzipApkFilePath = tempFilePath + apkFileName + "-" + UNZIP_APK_FILE_NAME + File.separator;
if (showAllLogs) { if (showAllLogs) {
System.out.println(" !!!!! outputApkFileParentPath = " + outputApkFileParentPath + System.out.println(" !!!!! outputApkFileParentPath = " + outputApkFileParentPath +
"\n unzipApkFilePath = " + unzipApkFilePath); "\n unzipApkFilePath = " + unzipApkFilePath);
} }
if (!disableCrackSignature) { if (!disableCrackSignature) {
// save the apk original signature info, to support crach signature. // save the apk original signature info, to support crach signature.
new SaveApkSignatureTask(apkPath, unzipApkFilePath).run(); new SaveApkSignatureTask(apkPath, unzipApkFilePath).run();
} }
// 先解压apk到指定目录下 // 先解压apk到指定目录下
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
FileUtils.decompressZip(apkPath, unzipApkFilePath); FileUtils.decompressZip(apkPath, unzipApkFilePath);
if (showAllLogs) { if (showAllLogs) {
System.out.println(" decompress apk cost time: " + (System.currentTimeMillis() - currentTime)); System.out.println(" decompress apk cost time: " + (System.currentTimeMillis() - currentTime));
} }
// Get the dex count in the apk zip file // Get the dex count in the apk zip file
dexFileCount = findDexFileCount(unzipApkFilePath); dexFileCount = findDexFileCount(unzipApkFilePath);
if (showAllLogs) { if (showAllLogs) {
System.out.println(" --- dexFileCount = " + dexFileCount); System.out.println(" --- dexFileCount = " + dexFileCount);
} }
String manifestFilePath = unzipApkFilePath + "AndroidManifest.xml"; String manifestFilePath = unzipApkFilePath + "AndroidManifest.xml";
currentTime = System.currentTimeMillis(); currentTime = System.currentTimeMillis();
// parse the app main application full name from the manifest file // parse the app main application full name from the manifest file
ManifestParser.Pair pair = ManifestParser.parseManifestFile(manifestFilePath); ManifestParser.Pair pair = ManifestParser.parseManifestFile(manifestFilePath);
String applicationName = null; String applicationName = null;
if (pair != null && pair.applicationName != null) { if (pair != null && pair.applicationName != null) {
applicationName = pair.applicationName; applicationName = pair.applicationName;
} }
if (showAllLogs) { if (showAllLogs) {
System.out.println(" Get application name cost time: " + (System.currentTimeMillis() - currentTime)); System.out.println(" Get application name cost time: " + (System.currentTimeMillis() - currentTime));
System.out.println(" Get the application name --> " + applicationName); System.out.println(" Get the application name --> " + applicationName);
} }
// modify manifest // modify manifest
File manifestFile = new File(manifestFilePath); File manifestFile = new File(manifestFilePath);
String manifestFilePathNew = unzipApkFilePath + "AndroidManifest" + "-" + currentTimeStr() + ".xml"; String manifestFilePathNew = unzipApkFilePath + "AndroidManifest" + "-" + currentTimeStr() + ".xml";
File manifestFileNew = new File(manifestFilePathNew); File manifestFileNew = new File(manifestFilePathNew);
manifestFile.renameTo(manifestFileNew); manifestFile.renameTo(manifestFileNew);
modifyManifestFile(manifestFilePathNew, manifestFilePath, applicationName); modifyManifestFile(manifestFilePathNew, manifestFilePath, applicationName);
// new manifest may not exist // new manifest may not exist
if (manifestFile.exists() && manifestFile.length() > 0) { if (manifestFile.exists() && manifestFile.length() > 0) {
manifestFileNew.delete(); manifestFileNew.delete();
} else { } else {
manifestFileNew.renameTo(manifestFile); manifestFileNew.renameTo(manifestFile);
} }
// save original main application name to asset file // save original main application name to asset file
if (isNotEmpty(applicationName)) { if (isNotEmpty(applicationName)) {
mXpatchTasks.add(new SaveOriginalApplicationNameTask(applicationName, unzipApkFilePath)); mXpatchTasks.add(new SaveOriginalApplicationNameTask(applicationName, unzipApkFilePath));
} }
// modify the apk dex file to make xposed can run in it // modify the apk dex file to make xposed can run in it
if (dexModificationMode && isNotEmpty(applicationName)) { if (dexModificationMode && isNotEmpty(applicationName)) {
mXpatchTasks.add(new ApkModifyTask(showAllLogs, keepBuildFiles, unzipApkFilePath, applicationName, mXpatchTasks.add(new ApkModifyTask(showAllLogs, keepBuildFiles, unzipApkFilePath, applicationName,
dexFileCount)); dexFileCount));
} }
// copy xposed so and dex files into the unzipped apk // copy xposed so and dex files into the unzipped apk
mXpatchTasks.add(new SoAndDexCopyTask(dexFileCount, unzipApkFilePath, mXpatchTasks.add(new SoAndDexCopyTask(dexFileCount, unzipApkFilePath,
getXposedModules(xposedModules), useWhaleHookFramework)); getXposedModules(xposedModules), useWhaleHookFramework));
// compress all files into an apk and then sign it. // compress all files into an apk and then sign it.
mXpatchTasks.add(new BuildAndSignApkTask(keepBuildFiles, unzipApkFilePath, output)); mXpatchTasks.add(new BuildAndSignApkTask(keepBuildFiles, unzipApkFilePath, output));
// excute these tasks // copy origin apk to assets
for (Runnable executor : mXpatchTasks) { // convenient to bypass some check like CRC
currentTime = System.currentTimeMillis(); if(!FileUtils.copyFile(srcApkFile, new File(unzipApkFilePath, "assets/origin_apk.bin"))){
executor.run(); throw new IllegalStateException("orignal apk copy fail");
}
if (showAllLogs) {
System.out.println(executor.getClass().getSimpleName() + " cost time: " // excute these tasks
+ (System.currentTimeMillis() - currentTime)); for (Runnable executor : mXpatchTasks) {
} currentTime = System.currentTimeMillis();
} executor.run();
// 5. delete all the build files that is useless now if (showAllLogs) {
File unzipApkFile = new File(unzipApkFilePath); System.out.println(executor.getClass().getSimpleName() + " cost time: "
if (!keepBuildFiles && unzipApkFile.exists()) { + (System.currentTimeMillis() - currentTime));
FileUtils.deleteDir(unzipApkFile); }
} }
File tempFile = new File(tempFilePath); // 5. delete all the build files that is useless now
if (!keepBuildFiles && tempFile.exists()) { File unzipApkFile = new File(unzipApkFilePath);
tempFile.delete(); if (!keepBuildFiles && unzipApkFile.exists()) {
} FileUtils.deleteDir(unzipApkFile);
} }
private void modifyManifestFile(String filePath, String dstFilePath, String originalApplicationName) { File tempFile = new File(tempFilePath);
ModificationProperty property = new ModificationProperty(); if (!keepBuildFiles && tempFile.exists()) {
boolean modifyEnabled = false; tempFile.delete();
if (isNotEmpty(newPackageName)) { }
modifyEnabled = true; }
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.PACKAGE, newPackageName).setNamespace(null));
} private void modifyManifestFile(String filePath, String dstFilePath, String originalApplicationName) {
ModificationProperty property = new ModificationProperty();
if (versionCode > 0) { boolean modifyEnabled = false;
modifyEnabled = true; if (isNotEmpty(newPackageName)) {
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_CODE, versionCode)); modifyEnabled = true;
} property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.PACKAGE, newPackageName).setNamespace(null));
}
if (isNotEmpty(versionName)) {
modifyEnabled = true; if (versionCode > 0) {
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_NAME, versionName)); modifyEnabled = true;
} property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_CODE, versionCode));
}
if (debuggable >= 0) {
modifyEnabled = true; if (isNotEmpty(versionName)) {
property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggable != 0)); modifyEnabled = true;
} property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_NAME, versionName));
}
if (!dexModificationMode || !isNotEmpty(originalApplicationName)) {
modifyEnabled = true; if (debuggable >= 0) {
property.addApplicationAttribute(new AttributeItem(NodeValue.Application.NAME, PROXY_APPLICATION_NAME)); modifyEnabled = true;
} property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggable != 0));
}
if (modifyEnabled) {
FileProcesser.processManifestFile(filePath, dstFilePath, property); if (!dexModificationMode || !isNotEmpty(originalApplicationName)) {
} modifyEnabled = true;
} property.addApplicationAttribute(new AttributeItem(NodeValue.Application.NAME, PROXY_APPLICATION_NAME));
}
private int findDexFileCount(String unzipApkFilePath) {
File zipfileRoot = new File(unzipApkFilePath); if (modifyEnabled) {
if (!zipfileRoot.exists()) { FileProcesser.processManifestFile(filePath, dstFilePath, property);
return 0; }
} }
File[] childFiles = zipfileRoot.listFiles();
if (childFiles == null || childFiles.length == 0) { private int findDexFileCount(String unzipApkFilePath) {
return 0; File zipfileRoot = new File(unzipApkFilePath);
} if (!zipfileRoot.exists()) {
int count = 0; return 0;
for (File file : childFiles) { }
String fileName = file.getName(); File[] childFiles = zipfileRoot.listFiles();
if (Pattern.matches("classes.*\\.dex", fileName)) { if (childFiles == null || childFiles.length == 0) {
count++; return 0;
} }
} int count = 0;
return count; for (File file : childFiles) {
} String fileName = file.getName();
if (Pattern.matches("classes.*\\.dex", fileName)) {
// Use the current timestamp as the name of the build file count++;
private String currentTimeStr() { }
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");//设置日期格式 }
return df.format(new Date()); return count;
} }
private String[] getXposedModules(String modules) { // Use the current timestamp as the name of the build file
if (modules == null || modules.isEmpty()) { private String currentTimeStr() {
return null; SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");//设置日期格式
} return df.format(new Date());
return modules.split(File.pathSeparator); }
}
private String[] getXposedModules(String modules) {
private static boolean isNotEmpty(String str) { if (modules == null || modules.isEmpty()) {
return str != null && !str.isEmpty(); return null;
} }
} return modules.split(File.pathSeparator);
}
private static boolean isNotEmpty(String str) {
return str != null && !str.isEmpty();
}
}

View File

@ -1,478 +1,478 @@
package com.storm.wind.xpatch.base; package com.storm.wind.xpatch.base;
import java.io.File; import java.io.File;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType; import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
/** /**
* Created by Wind * Created by Wind
*/ */
public abstract class BaseCommand { public abstract class BaseCommand {
private String onlineHelp; private String onlineHelp;
protected Map<String, Option> optMap = new HashMap<String, Option>(); protected Map<String, Option> optMap = new HashMap<String, Option>();
@Opt(opt = "h", longOpt = "help", hasArg = false, description = "Print this help message") @Opt(opt = "h", longOpt = "help", hasArg = false, description = "Print this help message")
private boolean printHelp = false; private boolean printHelp = false;
protected String remainingArgs[]; protected String[] remainingArgs;
protected String orginalArgs[]; protected String[] orginalArgs;
@Retention(value = RetentionPolicy.RUNTIME) @Retention(value = RetentionPolicy.RUNTIME)
@Target(value = { ElementType.FIELD }) @Target(value = { ElementType.FIELD })
static public @interface Opt { static public @interface Opt {
String argName() default ""; String argName() default "";
String description() default ""; String description() default "";
boolean hasArg() default true; boolean hasArg() default true;
String longOpt() default ""; String longOpt() default "";
String opt() default ""; String opt() default "";
boolean required() default false; boolean required() default false;
} }
static protected class Option implements Comparable<Option> { static protected class Option implements Comparable<Option> {
public String argName = "arg"; public String argName = "arg";
public String description; public String description;
public Field field; public Field field;
public boolean hasArg = true; public boolean hasArg = true;
public String longOpt; public String longOpt;
public String opt; public String opt;
public boolean required = false; public boolean required = false;
@Override @Override
public int compareTo(Option o) { public int compareTo(Option o) {
int result = s(this.opt, o.opt); int result = s(this.opt, o.opt);
if (result == 0) { if (result == 0) {
result = s(this.longOpt, o.longOpt); result = s(this.longOpt, o.longOpt);
if (result == 0) { if (result == 0) {
result = s(this.argName, o.argName); result = s(this.argName, o.argName);
if (result == 0) { if (result == 0) {
result = s(this.description, o.description); result = s(this.description, o.description);
} }
} }
} }
return result; return result;
} }
private static int s(String a, String b) { private static int s(String a, String b) {
if (a != null && b != null) { if (a != null && b != null) {
return a.compareTo(b); return a.compareTo(b);
} else if (a != null) { } else if (a != null) {
return 1; return 1;
} else if (b != null) { } else if (b != null) {
return -1; return -1;
} else { } else {
return 0; return 0;
} }
} }
public String getOptAndLongOpt() { public String getOptAndLongOpt() {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
boolean havePrev = false; boolean havePrev = false;
if (opt != null && opt.length() > 0) { if (opt != null && opt.length() > 0) {
sb.append("-").append(opt); sb.append("-").append(opt);
havePrev = true; havePrev = true;
} }
if (longOpt != null && longOpt.length() > 0) { if (longOpt != null && longOpt.length() > 0) {
if (havePrev) { if (havePrev) {
sb.append(","); sb.append(",");
} }
sb.append("--").append(longOpt); sb.append("--").append(longOpt);
} }
return sb.toString(); return sb.toString();
} }
} }
@Retention(value = RetentionPolicy.RUNTIME) @Retention(value = RetentionPolicy.RUNTIME)
@Target(value = { ElementType.TYPE }) @Target(value = { ElementType.TYPE })
static public @interface Syntax { static public @interface Syntax {
String cmd(); String cmd();
String desc() default ""; String desc() default "";
String onlineHelp() default ""; String onlineHelp() default "";
String syntax() default ""; String syntax() default "";
} }
public void doMain(String... args) { public void doMain(String... args) {
try { try {
initOptions(); initOptions();
parseSetArgs(args); parseSetArgs(args);
doCommandLine(); doCommandLine();
} catch (HelpException e) { } catch (HelpException e) {
String msg = e.getMessage(); String msg = e.getMessage();
if (msg != null && msg.length() > 0) { if (msg != null && msg.length() > 0) {
System.err.println("ERROR: " + msg); System.err.println("ERROR: " + msg);
} }
usage(); usage();
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(System.err); e.printStackTrace(System.err);
} }
} }
protected abstract void doCommandLine() throws Exception; protected abstract void doCommandLine() throws Exception;
protected String getVersionString() { protected String getVersionString() {
return getClass().getPackage().getImplementationVersion(); return getClass().getPackage().getImplementationVersion();
} }
protected void initOptions() { protected void initOptions() {
initOptionFromClass(this.getClass()); initOptionFromClass(this.getClass());
} }
protected void initOptionFromClass(Class<?> clz) { protected void initOptionFromClass(Class<?> clz) {
if (clz == null) { if (clz == null) {
return; return;
} else { } else {
initOptionFromClass(clz.getSuperclass()); initOptionFromClass(clz.getSuperclass());
} }
Syntax syntax = clz.getAnnotation(Syntax.class); Syntax syntax = clz.getAnnotation(Syntax.class);
if (syntax != null) { if (syntax != null) {
this.onlineHelp = syntax.onlineHelp(); this.onlineHelp = syntax.onlineHelp();
} }
Field[] fs = clz.getDeclaredFields(); Field[] fs = clz.getDeclaredFields();
for (Field f : fs) { for (Field f : fs) {
Opt opt = f.getAnnotation(Opt.class); Opt opt = f.getAnnotation(Opt.class);
if (opt != null) { if (opt != null) {
f.setAccessible(true); f.setAccessible(true);
Option option = new Option(); Option option = new Option();
option.field = f; option.field = f;
option.description = opt.description(); option.description = opt.description();
option.hasArg = opt.hasArg(); option.hasArg = opt.hasArg();
option.required = opt.required(); option.required = opt.required();
if ("".equals(opt.longOpt()) && "".equals(opt.opt())) { // into automode if ("".equals(opt.longOpt()) && "".equals(opt.opt())) { // into automode
option.longOpt = fromCamel(f.getName()); option.longOpt = fromCamel(f.getName());
if (f.getType().equals(boolean.class)) { if (f.getType().equals(boolean.class)) {
option.hasArg=false; option.hasArg=false;
try { try {
if (f.getBoolean(this)) { if (f.getBoolean(this)) {
throw new RuntimeException("the value of " + f + throw new RuntimeException("the value of " + f +
" must be false, as it is declared as no args"); " must be false, as it is declared as no args");
} }
} catch (IllegalAccessException e) { } catch (IllegalAccessException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
checkConflict(option, "--" + option.longOpt); checkConflict(option, "--" + option.longOpt);
continue; continue;
} }
if (!opt.hasArg()) { if (!opt.hasArg()) {
if (!f.getType().equals(boolean.class)) { if (!f.getType().equals(boolean.class)) {
throw new RuntimeException("the type of " + f throw new RuntimeException("the type of " + f
+ " must be boolean, as it is declared as no args"); + " must be boolean, as it is declared as no args");
} }
try { try {
if (f.getBoolean(this)) { if (f.getBoolean(this)) {
throw new RuntimeException("the value of " + f + throw new RuntimeException("the value of " + f +
" must be false, as it is declared as no args"); " must be false, as it is declared as no args");
} }
} catch (IllegalAccessException e) { } catch (IllegalAccessException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
boolean haveLongOpt = false; boolean haveLongOpt = false;
if (!"".equals(opt.longOpt())) { if (!"".equals(opt.longOpt())) {
option.longOpt = opt.longOpt(); option.longOpt = opt.longOpt();
checkConflict(option, "--" + option.longOpt); checkConflict(option, "--" + option.longOpt);
haveLongOpt = true; haveLongOpt = true;
} }
if (!"".equals(opt.argName())) { if (!"".equals(opt.argName())) {
option.argName = opt.argName(); option.argName = opt.argName();
} }
if (!"".equals(opt.opt())) { if (!"".equals(opt.opt())) {
option.opt = opt.opt(); option.opt = opt.opt();
checkConflict(option, "-" + option.opt); checkConflict(option, "-" + option.opt);
} else { } else {
if (!haveLongOpt) { if (!haveLongOpt) {
throw new RuntimeException("opt or longOpt is not set in @Opt(...) " + f); throw new RuntimeException("opt or longOpt is not set in @Opt(...) " + f);
} }
} }
} }
} }
} }
private void checkConflict(Option option, String key) { private void checkConflict(Option option, String key) {
if (optMap.containsKey(key)) { if (optMap.containsKey(key)) {
Option preOption = optMap.get(key); Option preOption = optMap.get(key);
throw new RuntimeException(String.format("[@Opt(...) %s] conflict with [@Opt(...) %s]", throw new RuntimeException(String.format("[@Opt(...) %s] conflict with [@Opt(...) %s]",
preOption.field.toString(), option.field preOption.field.toString(), option.field
)); ));
} }
optMap.put(key, option); optMap.put(key, option);
} }
private static String fromCamel(String name) { private static String fromCamel(String name) {
if (name.length() == 0) { if (name.length() == 0) {
return ""; return "";
} }
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
char[] charArray = name.toCharArray(); char[] charArray = name.toCharArray();
sb.append(Character.toLowerCase(charArray[0])); sb.append(Character.toLowerCase(charArray[0]));
for (int i = 1; i < charArray.length; i++) { for (int i = 1; i < charArray.length; i++) {
char c = charArray[i]; char c = charArray[i];
if (Character.isUpperCase(c)) { if (Character.isUpperCase(c)) {
sb.append("-").append(Character.toLowerCase(c)); sb.append("-").append(Character.toLowerCase(c));
} else { } else {
sb.append(c); sb.append(c);
} }
} }
return sb.toString(); return sb.toString();
} }
protected void parseSetArgs(String... args) throws IllegalArgumentException, IllegalAccessException { protected void parseSetArgs(String... args) throws IllegalArgumentException, IllegalAccessException {
this.orginalArgs = args; this.orginalArgs = args;
List<String> remainsOptions = new ArrayList<String>(); List<String> remainsOptions = new ArrayList<String>();
Set<Option> requiredOpts = collectRequriedOptions(optMap); Set<Option> requiredOpts = collectRequriedOptions(optMap);
Option needArgOpt = null; Option needArgOpt = null;
for (String s : args) { for (String s : args) {
if (needArgOpt != null) { if (needArgOpt != null) {
Field field = needArgOpt.field; Field field = needArgOpt.field;
Class clazz = field.getType(); Class clazz = field.getType();
if (clazz.equals(List.class)) { if (clazz.equals(List.class)) {
try { try {
List<Object> object = ((List<Object>) field.get(this)); List<Object> object = ((List<Object>) field.get(this));
// 获取List对象的泛型类型 // 获取List对象的泛型类型
ParameterizedType listGenericType = (ParameterizedType) field.getGenericType(); ParameterizedType listGenericType = (ParameterizedType) field.getGenericType();
Type[] listActualTypeArguments = listGenericType.getActualTypeArguments(); Type[] listActualTypeArguments = listGenericType.getActualTypeArguments();
Class typeClazz = (Class) listActualTypeArguments[0]; Class typeClazz = (Class) listActualTypeArguments[0];
object.add(convert(s, typeClazz)); object.add(convert(s, typeClazz));
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
} else { } else {
field.set(this, convert(s, clazz)); field.set(this, convert(s, clazz));
} }
needArgOpt = null; needArgOpt = null;
} else if (s.startsWith("-")) {// its a short or long option } else if (s.startsWith("-")) {// its a short or long option
Option opt = optMap.get(s); Option opt = optMap.get(s);
requiredOpts.remove(opt); requiredOpts.remove(opt);
if (opt == null) { if (opt == null) {
System.err.println("ERROR: Unrecognized option: " + s); System.err.println("ERROR: Unrecognized option: " + s);
throw new HelpException(); throw new HelpException();
} else { } else {
if (opt.hasArg) { if (opt.hasArg) {
needArgOpt = opt; needArgOpt = opt;
} else { } else {
opt.field.set(this, true); opt.field.set(this, true);
} }
} }
} else { } else {
remainsOptions.add(s); remainsOptions.add(s);
} }
} }
if (needArgOpt != null) { if (needArgOpt != null) {
System.err.println("ERROR: Option " + needArgOpt.getOptAndLongOpt() + " need an argument value"); System.err.println("ERROR: Option " + needArgOpt.getOptAndLongOpt() + " need an argument value");
throw new HelpException(); throw new HelpException();
} }
this.remainingArgs = remainsOptions.toArray(new String[remainsOptions.size()]); this.remainingArgs = remainsOptions.toArray(new String[remainsOptions.size()]);
if (this.printHelp) { if (this.printHelp) {
throw new HelpException(); throw new HelpException();
} }
if (!requiredOpts.isEmpty()) { if (!requiredOpts.isEmpty()) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("ERROR: Options: "); sb.append("ERROR: Options: ");
boolean first = true; boolean first = true;
for (Option option : requiredOpts) { for (Option option : requiredOpts) {
if (first) { if (first) {
first = false; first = false;
} else { } else {
sb.append(" and "); sb.append(" and ");
} }
sb.append(option.getOptAndLongOpt()); sb.append(option.getOptAndLongOpt());
} }
sb.append(" is required"); sb.append(" is required");
System.err.println(sb.toString()); System.err.println(sb.toString());
throw new HelpException(); throw new HelpException();
} }
} }
@SuppressWarnings({ "rawtypes", "unchecked" }) @SuppressWarnings({ "rawtypes", "unchecked" })
protected Object convert(String value, Class type) { protected Object convert(String value, Class type) {
if (type.equals(String.class)) { if (type.equals(String.class)) {
return value; return value;
} }
if (type.equals(int.class) || type.equals(Integer.class)) { if (type.equals(int.class) || type.equals(Integer.class)) {
return Integer.parseInt(value); return Integer.parseInt(value);
} }
if (type.equals(long.class) || type.equals(Long.class)) { if (type.equals(long.class) || type.equals(Long.class)) {
return Long.parseLong(value); return Long.parseLong(value);
} }
if (type.equals(float.class) || type.equals(Float.class)) { if (type.equals(float.class) || type.equals(Float.class)) {
return Float.parseFloat(value); return Float.parseFloat(value);
} }
if (type.equals(double.class) || type.equals(Double.class)) { if (type.equals(double.class) || type.equals(Double.class)) {
return Double.parseDouble(value); return Double.parseDouble(value);
} }
if (type.equals(boolean.class) || type.equals(Boolean.class)) { if (type.equals(boolean.class) || type.equals(Boolean.class)) {
return Boolean.parseBoolean(value); return Boolean.parseBoolean(value);
} }
if (type.equals(File.class)) { if (type.equals(File.class)) {
return new File(value); return new File(value);
} }
if (type.equals(Path.class)) { if (type.equals(Path.class)) {
return new File(value).toPath(); return new File(value).toPath();
} }
try { try {
type.asSubclass(Enum.class); type.asSubclass(Enum.class);
return Enum.valueOf(type, value); return Enum.valueOf(type, value);
} catch (Exception e) { } catch (Exception e) {
} }
throw new RuntimeException("can't convert [" + value + "] to type " + type); throw new RuntimeException("can't convert [" + value + "] to type " + type);
} }
private Set<Option> collectRequriedOptions(Map<String, Option> optMap) { private Set<Option> collectRequriedOptions(Map<String, Option> optMap) {
Set<Option> options = new HashSet<Option>(); Set<Option> options = new HashSet<Option>();
for (Map.Entry<String, Option> e : optMap.entrySet()) { for (Map.Entry<String, Option> e : optMap.entrySet()) {
Option option = e.getValue(); Option option = e.getValue();
if (option.required) { if (option.required) {
options.add(option); options.add(option);
} }
} }
return options; return options;
} }
@SuppressWarnings("serial") @SuppressWarnings("serial")
protected static class HelpException extends RuntimeException { protected static class HelpException extends RuntimeException {
public HelpException() { public HelpException() {
super(); super();
} }
public HelpException(String message) { public HelpException(String message) {
super(message); super(message);
} }
} }
protected void usage() { protected void usage() {
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.err, StandardCharsets.UTF_8), true); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.err, StandardCharsets.UTF_8), true);
final int maxLength = 80; final int maxLength = 80;
final int maxPaLength = 40; final int maxPaLength = 40;
// out.println(this.cmdName + " -- " + desc); // out.println(this.cmdName + " -- " + desc);
// out.println("usage: " + this.cmdName + " " + cmdLineSyntax); // out.println("usage: " + this.cmdName + " " + cmdLineSyntax);
if (this.optMap.size() > 0) { if (this.optMap.size() > 0) {
out.println("options:"); out.println("options:");
} }
// [PART.A.........][Part.B // [PART.A.........][Part.B
// .-a,--aa.<arg>...desc1 // .-a,--aa.<arg>...desc1
// .................desc2 // .................desc2
// .-b,--bb // .-b,--bb
TreeSet<Option> options = new TreeSet<Option>(this.optMap.values()); TreeSet<Option> options = new TreeSet<Option>(this.optMap.values());
int palength = -1; int palength = -1;
for (Option option : options) { for (Option option : options) {
int pa = 4 + option.getOptAndLongOpt().length(); int pa = 4 + option.getOptAndLongOpt().length();
if (option.hasArg) { if (option.hasArg) {
pa += 3 + option.argName.length(); pa += 3 + option.argName.length();
} }
if (pa < maxPaLength) { if (pa < maxPaLength) {
if (pa > palength) { if (pa > palength) {
palength = pa; palength = pa;
} }
} }
} }
int pblength = maxLength - palength; int pblength = maxLength - palength;
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
for (Option option : options) { for (Option option : options) {
sb.setLength(0); sb.setLength(0);
sb.append(" ").append(option.getOptAndLongOpt()); sb.append(" ").append(option.getOptAndLongOpt());
if (option.hasArg) { if (option.hasArg) {
sb.append(" <").append(option.argName).append(">"); sb.append(" <").append(option.argName).append(">");
} }
String desc = option.description; String desc = option.description;
if (desc == null || desc.length() == 0) {// no description if (desc == null || desc.length() == 0) {// no description
out.println(sb); out.println(sb);
} else { } else {
for (int i = palength - sb.length(); i > 0; i--) { for (int i = palength - sb.length(); i > 0; i--) {
sb.append(' '); sb.append(' ');
} }
if (sb.length() > maxPaLength) {// to huge part A if (sb.length() > maxPaLength) {// to huge part A
out.println(sb); out.println(sb);
sb.setLength(0); sb.setLength(0);
for (int i = 0; i < palength; i++) { for (int i = 0; i < palength; i++) {
sb.append(' '); sb.append(' ');
} }
} }
int nextStart = 0; int nextStart = 0;
while (nextStart < desc.length()) { while (nextStart < desc.length()) {
if (desc.length() - nextStart < pblength) {// can put in one line if (desc.length() - nextStart < pblength) {// can put in one line
sb.append(desc.substring(nextStart)); sb.append(desc.substring(nextStart));
out.println(sb); out.println(sb);
nextStart = desc.length(); nextStart = desc.length();
sb.setLength(0); sb.setLength(0);
} else { } else {
sb.append(desc.substring(nextStart, nextStart + pblength)); sb.append(desc.substring(nextStart, nextStart + pblength));
out.println(sb); out.println(sb);
nextStart += pblength; nextStart += pblength;
sb.setLength(0); sb.setLength(0);
if (nextStart < desc.length()) { if (nextStart < desc.length()) {
for (int i = 0; i < palength; i++) { for (int i = 0; i < palength; i++) {
sb.append(' '); sb.append(' ');
} }
} }
} }
} }
if (sb.length() > 0) { if (sb.length() > 0) {
out.println(sb); out.println(sb);
sb.setLength(0); sb.setLength(0);
} }
} }
} }
String ver = getVersionString(); String ver = getVersionString();
if (ver != null && !"".equals(ver)) { if (ver != null && !"".equals(ver)) {
out.println("version: " + ver); out.println("version: " + ver);
} }
if (onlineHelp != null && !"".equals(onlineHelp)) { if (onlineHelp != null && !"".equals(onlineHelp)) {
if (onlineHelp.length() + "online help: ".length() > maxLength) { if (onlineHelp.length() + "online help: ".length() > maxLength) {
out.println("online help: "); out.println("online help: ");
out.println(onlineHelp); out.println(onlineHelp);
} else { } else {
out.println("online help: " + onlineHelp); out.println("online help: " + onlineHelp);
} }
} }
out.flush(); out.flush();
} }
public static String getBaseName(String fn) { public static String getBaseName(String fn) {
int x = fn.lastIndexOf('.'); int x = fn.lastIndexOf('.');
return x >= 0 ? fn.substring(0, x) : fn; return x >= 0 ? fn.substring(0, x) : fn;
} }
// 获取文件不包含后缀的名称 // 获取文件不包含后缀的名称
public static String getBaseName(File fn) { public static String getBaseName(File fn) {
return getBaseName(fn.getName()); return getBaseName(fn.getName());
} }
} }

View File

@ -125,7 +125,7 @@ public class FileUtils {
copyFile(new File(sourcePath), new File(targetPath)); copyFile(new File(sourcePath), new File(targetPath));
} }
public static void copyFile(File source, File target) { public static boolean copyFile(File source, File target) {
FileInputStream inputStream = null; FileInputStream inputStream = null;
FileOutputStream outputStream = null; FileOutputStream outputStream = null;
@ -146,12 +146,14 @@ public class FileUtils {
buffer.position(0); buffer.position(0);
oChannel.write(buffer); oChannel.write(buffer);
} }
return true;
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} finally { } finally {
close(inputStream); close(inputStream);
close(outputStream); close(outputStream);
} }
return false;
} }
public static void deleteDir(File file) { public static void deleteDir(File file) {