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;
import com.storm.wind.xpatch.base.BaseCommand;
import com.storm.wind.xpatch.task.ApkModifyTask;
import com.storm.wind.xpatch.task.BuildAndSignApkTask;
import com.storm.wind.xpatch.task.SaveApkSignatureTask;
import com.storm.wind.xpatch.task.SaveOriginalApplicationNameTask;
import com.storm.wind.xpatch.task.SoAndDexCopyTask;
import com.storm.wind.xpatch.util.FileUtils;
import com.storm.wind.xpatch.util.ManifestParser;
import com.wind.meditor.core.FileProcesser;
import com.wind.meditor.property.AttributeItem;
import com.wind.meditor.property.ModificationProperty;
import com.wind.meditor.utils.NodeValue;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.regex.Pattern;
public class MainCommand extends BaseCommand {
private String apkPath;
private String unzipApkFilePath;
@Opt(opt = "o", longOpt = "output", description = "output .apk file, default is " +
"$source_apk_dir/[file-name]-xposed-signed.apk", argName = "out-apk-file")
private String output; // 输出的apk文件的目录以及名称
@Opt(opt = "f", longOpt = "force", hasArg = false, description = "force overwrite")
private boolean forceOverwrite = false;
@Opt(opt = "k", longOpt = "keep", hasArg = false, description = "not delete the jar file " +
"that is changed by dex2jar and the apk zip files")
private boolean keepBuildFiles = false;
@Opt(opt = "l", longOpt = "log", hasArg = false, description = "show all the debug logs")
private boolean showAllLogs = false;
@Opt(opt = "c", longOpt = "crach", hasArg = false,
description = "disable craching the apk's signature.")
private boolean disableCrackSignature = false;
@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")
private String xposedModules;
// 使用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")
private boolean dexModificationMode = false;
@Opt(opt = "pkg", longOpt = "packageName", description = "modify the apk package name", argName = "new package name")
private String newPackageName;
@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")
private int debuggable = -1; // 0: debuggable = false 1: debuggable = true
@Opt(opt = "vc", longOpt = "versionCode", description = "set the app version code",
argName = "new-version-code")
private int versionCode;
@Opt(opt = "vn", longOpt = "versionName", description = "set the app version name",
argName = "new-version-name")
private String versionName;
@Opt(opt = "w", longOpt = "whale", hasArg = false, description = "Change hook framework to Lody's whale")
private boolean useWhaleHookFramework = false; // 是否使用whale hook框架默认使用的是SandHook
// 原来apk中dex文件的数量
private int dexFileCount = 0;
private static final String UNZIP_APK_FILE_NAME = "apk-unzip-files";
private static final String PROXY_APPLICATION_NAME = "com.wind.xpatch.proxy.XpatchProxyApplication";
private List<Runnable> mXpatchTasks = new ArrayList<>();
public static void main(String... args) {
new MainCommand().doMain(args);
}
@Override
protected void doCommandLine() {
if (remainingArgs.length != 1) {
if (remainingArgs.length == 0) {
System.out.println("Please choose one apk file you want to process. ");
}
if (remainingArgs.length > 1) {
System.out.println("This tool can only used with one apk file.");
}
usage();
return;
}
apkPath = remainingArgs[0];
File srcApkFile = new File(apkPath);
if (!srcApkFile.exists()) {
System.out.println(" The source apk file not exsit, please choose another one. " +
"current apk file is = " + apkPath);
return;
}
String currentDir = new File(".").getAbsolutePath(); // 当前命令行所在的目录
if (showAllLogs) {
System.out.println(" currentDir = " + currentDir + " \n apkPath = " + apkPath);
}
if (output == null || output.length() == 0) {
output = getBaseName(apkPath) + "-xposed-signed.apk";
}
File outputFile = new File(output);
if (outputFile.exists() && !forceOverwrite) {
System.err.println(output + " exists, use --force to overwrite");
usage();
return;
}
String outputApkFileParentPath = outputFile.getParent();
if (outputApkFileParentPath == null) {
String absPath = outputFile.getAbsolutePath();
int index = absPath.lastIndexOf(File.separatorChar);
outputApkFileParentPath = absPath.substring(0, index);
}
System.out.println(" !!!!! output apk path --> " + output +
" disableCrackSignature --> " + disableCrackSignature);
String apkFileName = getBaseName(srcApkFile);
// 中间文件临时存储的位置
String tempFilePath = outputApkFileParentPath + File.separator +
currentTimeStr() + "-tmp" + File.separator;
// apk文件解压的目录
unzipApkFilePath = tempFilePath + apkFileName + "-" + UNZIP_APK_FILE_NAME + File.separator;
if (showAllLogs) {
System.out.println(" !!!!! outputApkFileParentPath = " + outputApkFileParentPath +
"\n unzipApkFilePath = " + unzipApkFilePath);
}
if (!disableCrackSignature) {
// save the apk original signature info, to support crach signature.
new SaveApkSignatureTask(apkPath, unzipApkFilePath).run();
}
// 先解压apk到指定目录下
long currentTime = System.currentTimeMillis();
FileUtils.decompressZip(apkPath, unzipApkFilePath);
if (showAllLogs) {
System.out.println(" decompress apk cost time: " + (System.currentTimeMillis() - currentTime));
}
// Get the dex count in the apk zip file
dexFileCount = findDexFileCount(unzipApkFilePath);
if (showAllLogs) {
System.out.println(" --- dexFileCount = " + dexFileCount);
}
String manifestFilePath = unzipApkFilePath + "AndroidManifest.xml";
currentTime = System.currentTimeMillis();
// parse the app main application full name from the manifest file
ManifestParser.Pair pair = ManifestParser.parseManifestFile(manifestFilePath);
String applicationName = null;
if (pair != null && pair.applicationName != null) {
applicationName = pair.applicationName;
}
if (showAllLogs) {
System.out.println(" Get application name cost time: " + (System.currentTimeMillis() - currentTime));
System.out.println(" Get the application name --> " + applicationName);
}
// modify manifest
File manifestFile = new File(manifestFilePath);
String manifestFilePathNew = unzipApkFilePath + "AndroidManifest" + "-" + currentTimeStr() + ".xml";
File manifestFileNew = new File(manifestFilePathNew);
manifestFile.renameTo(manifestFileNew);
modifyManifestFile(manifestFilePathNew, manifestFilePath, applicationName);
// new manifest may not exist
if (manifestFile.exists() && manifestFile.length() > 0) {
manifestFileNew.delete();
} else {
manifestFileNew.renameTo(manifestFile);
}
// save original main application name to asset file
if (isNotEmpty(applicationName)) {
mXpatchTasks.add(new SaveOriginalApplicationNameTask(applicationName, unzipApkFilePath));
}
// modify the apk dex file to make xposed can run in it
if (dexModificationMode && isNotEmpty(applicationName)) {
mXpatchTasks.add(new ApkModifyTask(showAllLogs, keepBuildFiles, unzipApkFilePath, applicationName,
dexFileCount));
}
// copy xposed so and dex files into the unzipped apk
mXpatchTasks.add(new SoAndDexCopyTask(dexFileCount, unzipApkFilePath,
getXposedModules(xposedModules), useWhaleHookFramework));
// compress all files into an apk and then sign it.
mXpatchTasks.add(new BuildAndSignApkTask(keepBuildFiles, unzipApkFilePath, output));
// excute these tasks
for (Runnable executor : mXpatchTasks) {
currentTime = System.currentTimeMillis();
executor.run();
if (showAllLogs) {
System.out.println(executor.getClass().getSimpleName() + " cost time: "
+ (System.currentTimeMillis() - currentTime));
}
}
// 5. delete all the build files that is useless now
File unzipApkFile = new File(unzipApkFilePath);
if (!keepBuildFiles && unzipApkFile.exists()) {
FileUtils.deleteDir(unzipApkFile);
}
File tempFile = new File(tempFilePath);
if (!keepBuildFiles && tempFile.exists()) {
tempFile.delete();
}
}
private void modifyManifestFile(String filePath, String dstFilePath, String originalApplicationName) {
ModificationProperty property = new ModificationProperty();
boolean modifyEnabled = false;
if (isNotEmpty(newPackageName)) {
modifyEnabled = true;
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.PACKAGE, newPackageName).setNamespace(null));
}
if (versionCode > 0) {
modifyEnabled = true;
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_CODE, versionCode));
}
if (isNotEmpty(versionName)) {
modifyEnabled = true;
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_NAME, versionName));
}
if (debuggable >= 0) {
modifyEnabled = true;
property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggable != 0));
}
if (!dexModificationMode || !isNotEmpty(originalApplicationName)) {
modifyEnabled = true;
property.addApplicationAttribute(new AttributeItem(NodeValue.Application.NAME, PROXY_APPLICATION_NAME));
}
if (modifyEnabled) {
FileProcesser.processManifestFile(filePath, dstFilePath, property);
}
}
private int findDexFileCount(String unzipApkFilePath) {
File zipfileRoot = new File(unzipApkFilePath);
if (!zipfileRoot.exists()) {
return 0;
}
File[] childFiles = zipfileRoot.listFiles();
if (childFiles == null || childFiles.length == 0) {
return 0;
}
int count = 0;
for (File file : childFiles) {
String fileName = file.getName();
if (Pattern.matches("classes.*\\.dex", fileName)) {
count++;
}
}
return count;
}
// Use the current timestamp as the name of the build file
private String currentTimeStr() {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");//设置日期格式
return df.format(new Date());
}
private String[] getXposedModules(String modules) {
if (modules == null || modules.isEmpty()) {
return null;
}
return modules.split(File.pathSeparator);
}
private static boolean isNotEmpty(String str) {
return str != null && !str.isEmpty();
}
}
package com.storm.wind.xpatch;
import com.storm.wind.xpatch.base.BaseCommand;
import com.storm.wind.xpatch.task.ApkModifyTask;
import com.storm.wind.xpatch.task.BuildAndSignApkTask;
import com.storm.wind.xpatch.task.SaveApkSignatureTask;
import com.storm.wind.xpatch.task.SaveOriginalApplicationNameTask;
import com.storm.wind.xpatch.task.SoAndDexCopyTask;
import com.storm.wind.xpatch.util.FileUtils;
import com.storm.wind.xpatch.util.ManifestParser;
import com.wind.meditor.core.FileProcesser;
import com.wind.meditor.property.AttributeItem;
import com.wind.meditor.property.ModificationProperty;
import com.wind.meditor.utils.NodeValue;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.regex.Pattern;
public class MainCommand extends BaseCommand {
private String apkPath;
private String unzipApkFilePath;
@Opt(opt = "o", longOpt = "output", description = "output .apk file, default is " +
"$source_apk_dir/[file-name]-xposed-signed.apk", argName = "out-apk-file")
private String output; // 输出的apk文件的目录以及名称
@Opt(opt = "f", longOpt = "force", hasArg = false, description = "force overwrite")
private boolean forceOverwrite = false;
@Opt(opt = "k", longOpt = "keep", hasArg = false, description = "not delete the jar file " +
"that is changed by dex2jar and the apk zip files")
private boolean keepBuildFiles = false;
@Opt(opt = "l", longOpt = "log", hasArg = false, description = "show all the debug logs")
private boolean showAllLogs = false;
@Opt(opt = "c", longOpt = "crach", hasArg = false,
description = "disable craching the apk's signature.")
private boolean disableCrackSignature = false;
@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")
private String xposedModules;
// 使用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")
private boolean dexModificationMode = false;
@Opt(opt = "pkg", longOpt = "packageName", description = "modify the apk package name", argName = "new package name")
private String newPackageName;
@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")
private int debuggable = -1; // 0: debuggable = false 1: debuggable = true
@Opt(opt = "vc", longOpt = "versionCode", description = "set the app version code",
argName = "new-version-code")
private int versionCode;
@Opt(opt = "vn", longOpt = "versionName", description = "set the app version name",
argName = "new-version-name")
private String versionName;
@Opt(opt = "w", longOpt = "whale", hasArg = false, description = "Change hook framework to Lody's whale")
private boolean useWhaleHookFramework = false; // 是否使用whale hook框架默认使用的是SandHook
// 原来apk中dex文件的数量
private int dexFileCount = 0;
private static final String UNZIP_APK_FILE_NAME = "apk-unzip-files";
private static final String PROXY_APPLICATION_NAME = "com.wind.xpatch.proxy.XpatchProxyApplication";
private List<Runnable> mXpatchTasks = new ArrayList<>();
public static void main(String... args) {
new MainCommand().doMain(args);
}
@Override
protected void doCommandLine() {
if (remainingArgs.length != 1) {
if (remainingArgs.length == 0) {
System.out.println("Please choose one apk file you want to process. ");
}
if (remainingArgs.length > 1) {
System.out.println("This tool can only used with one apk file.");
}
usage();
return;
}
apkPath = remainingArgs[0];
File srcApkFile = new File(apkPath);
if (!srcApkFile.exists()) {
System.out.println(" The source apk file not exsit, please choose another one. " +
"current apk file is = " + apkPath);
return;
}
String currentDir = new File(".").getAbsolutePath(); // 当前命令行所在的目录
if (showAllLogs) {
System.out.println(" currentDir = " + currentDir + " \n apkPath = " + apkPath);
}
if (output == null || output.length() == 0) {
output = getBaseName(apkPath) + "-xposed-signed.apk";
}
File outputFile = new File(output);
if (outputFile.exists() && !forceOverwrite) {
System.err.println(output + " exists, use --force to overwrite");
usage();
return;
}
String outputApkFileParentPath = outputFile.getParent();
if (outputApkFileParentPath == null) {
String absPath = outputFile.getAbsolutePath();
int index = absPath.lastIndexOf(File.separatorChar);
outputApkFileParentPath = absPath.substring(0, index);
}
System.out.println(" !!!!! output apk path --> " + output +
" disableCrackSignature --> " + disableCrackSignature);
String apkFileName = getBaseName(srcApkFile);
// 中间文件临时存储的位置
String tempFilePath = outputApkFileParentPath + File.separator +
currentTimeStr() + "-tmp" + File.separator;
// apk文件解压的目录
unzipApkFilePath = tempFilePath + apkFileName + "-" + UNZIP_APK_FILE_NAME + File.separator;
if (showAllLogs) {
System.out.println(" !!!!! outputApkFileParentPath = " + outputApkFileParentPath +
"\n unzipApkFilePath = " + unzipApkFilePath);
}
if (!disableCrackSignature) {
// save the apk original signature info, to support crach signature.
new SaveApkSignatureTask(apkPath, unzipApkFilePath).run();
}
// 先解压apk到指定目录下
long currentTime = System.currentTimeMillis();
FileUtils.decompressZip(apkPath, unzipApkFilePath);
if (showAllLogs) {
System.out.println(" decompress apk cost time: " + (System.currentTimeMillis() - currentTime));
}
// Get the dex count in the apk zip file
dexFileCount = findDexFileCount(unzipApkFilePath);
if (showAllLogs) {
System.out.println(" --- dexFileCount = " + dexFileCount);
}
String manifestFilePath = unzipApkFilePath + "AndroidManifest.xml";
currentTime = System.currentTimeMillis();
// parse the app main application full name from the manifest file
ManifestParser.Pair pair = ManifestParser.parseManifestFile(manifestFilePath);
String applicationName = null;
if (pair != null && pair.applicationName != null) {
applicationName = pair.applicationName;
}
if (showAllLogs) {
System.out.println(" Get application name cost time: " + (System.currentTimeMillis() - currentTime));
System.out.println(" Get the application name --> " + applicationName);
}
// modify manifest
File manifestFile = new File(manifestFilePath);
String manifestFilePathNew = unzipApkFilePath + "AndroidManifest" + "-" + currentTimeStr() + ".xml";
File manifestFileNew = new File(manifestFilePathNew);
manifestFile.renameTo(manifestFileNew);
modifyManifestFile(manifestFilePathNew, manifestFilePath, applicationName);
// new manifest may not exist
if (manifestFile.exists() && manifestFile.length() > 0) {
manifestFileNew.delete();
} else {
manifestFileNew.renameTo(manifestFile);
}
// save original main application name to asset file
if (isNotEmpty(applicationName)) {
mXpatchTasks.add(new SaveOriginalApplicationNameTask(applicationName, unzipApkFilePath));
}
// modify the apk dex file to make xposed can run in it
if (dexModificationMode && isNotEmpty(applicationName)) {
mXpatchTasks.add(new ApkModifyTask(showAllLogs, keepBuildFiles, unzipApkFilePath, applicationName,
dexFileCount));
}
// copy xposed so and dex files into the unzipped apk
mXpatchTasks.add(new SoAndDexCopyTask(dexFileCount, unzipApkFilePath,
getXposedModules(xposedModules), useWhaleHookFramework));
// compress all files into an apk and then sign it.
mXpatchTasks.add(new BuildAndSignApkTask(keepBuildFiles, unzipApkFilePath, output));
// copy origin apk to assets
// convenient to bypass some check like CRC
if(!FileUtils.copyFile(srcApkFile, new File(unzipApkFilePath, "assets/origin_apk.bin"))){
throw new IllegalStateException("orignal apk copy fail");
}
// excute these tasks
for (Runnable executor : mXpatchTasks) {
currentTime = System.currentTimeMillis();
executor.run();
if (showAllLogs) {
System.out.println(executor.getClass().getSimpleName() + " cost time: "
+ (System.currentTimeMillis() - currentTime));
}
}
// 5. delete all the build files that is useless now
File unzipApkFile = new File(unzipApkFilePath);
if (!keepBuildFiles && unzipApkFile.exists()) {
FileUtils.deleteDir(unzipApkFile);
}
File tempFile = new File(tempFilePath);
if (!keepBuildFiles && tempFile.exists()) {
tempFile.delete();
}
}
private void modifyManifestFile(String filePath, String dstFilePath, String originalApplicationName) {
ModificationProperty property = new ModificationProperty();
boolean modifyEnabled = false;
if (isNotEmpty(newPackageName)) {
modifyEnabled = true;
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.PACKAGE, newPackageName).setNamespace(null));
}
if (versionCode > 0) {
modifyEnabled = true;
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_CODE, versionCode));
}
if (isNotEmpty(versionName)) {
modifyEnabled = true;
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_NAME, versionName));
}
if (debuggable >= 0) {
modifyEnabled = true;
property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggable != 0));
}
if (!dexModificationMode || !isNotEmpty(originalApplicationName)) {
modifyEnabled = true;
property.addApplicationAttribute(new AttributeItem(NodeValue.Application.NAME, PROXY_APPLICATION_NAME));
}
if (modifyEnabled) {
FileProcesser.processManifestFile(filePath, dstFilePath, property);
}
}
private int findDexFileCount(String unzipApkFilePath) {
File zipfileRoot = new File(unzipApkFilePath);
if (!zipfileRoot.exists()) {
return 0;
}
File[] childFiles = zipfileRoot.listFiles();
if (childFiles == null || childFiles.length == 0) {
return 0;
}
int count = 0;
for (File file : childFiles) {
String fileName = file.getName();
if (Pattern.matches("classes.*\\.dex", fileName)) {
count++;
}
}
return count;
}
// Use the current timestamp as the name of the build file
private String currentTimeStr() {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");//设置日期格式
return df.format(new Date());
}
private String[] getXposedModules(String modules) {
if (modules == null || modules.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;
import java.io.File;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
/**
* Created by Wind
*/
public abstract class BaseCommand {
private String onlineHelp;
protected Map<String, Option> optMap = new HashMap<String, Option>();
@Opt(opt = "h", longOpt = "help", hasArg = false, description = "Print this help message")
private boolean printHelp = false;
protected String remainingArgs[];
protected String orginalArgs[];
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = { ElementType.FIELD })
static public @interface Opt {
String argName() default "";
String description() default "";
boolean hasArg() default true;
String longOpt() default "";
String opt() default "";
boolean required() default false;
}
static protected class Option implements Comparable<Option> {
public String argName = "arg";
public String description;
public Field field;
public boolean hasArg = true;
public String longOpt;
public String opt;
public boolean required = false;
@Override
public int compareTo(Option o) {
int result = s(this.opt, o.opt);
if (result == 0) {
result = s(this.longOpt, o.longOpt);
if (result == 0) {
result = s(this.argName, o.argName);
if (result == 0) {
result = s(this.description, o.description);
}
}
}
return result;
}
private static int s(String a, String b) {
if (a != null && b != null) {
return a.compareTo(b);
} else if (a != null) {
return 1;
} else if (b != null) {
return -1;
} else {
return 0;
}
}
public String getOptAndLongOpt() {
StringBuilder sb = new StringBuilder();
boolean havePrev = false;
if (opt != null && opt.length() > 0) {
sb.append("-").append(opt);
havePrev = true;
}
if (longOpt != null && longOpt.length() > 0) {
if (havePrev) {
sb.append(",");
}
sb.append("--").append(longOpt);
}
return sb.toString();
}
}
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = { ElementType.TYPE })
static public @interface Syntax {
String cmd();
String desc() default "";
String onlineHelp() default "";
String syntax() default "";
}
public void doMain(String... args) {
try {
initOptions();
parseSetArgs(args);
doCommandLine();
} catch (HelpException e) {
String msg = e.getMessage();
if (msg != null && msg.length() > 0) {
System.err.println("ERROR: " + msg);
}
usage();
} catch (Exception e) {
e.printStackTrace(System.err);
}
}
protected abstract void doCommandLine() throws Exception;
protected String getVersionString() {
return getClass().getPackage().getImplementationVersion();
}
protected void initOptions() {
initOptionFromClass(this.getClass());
}
protected void initOptionFromClass(Class<?> clz) {
if (clz == null) {
return;
} else {
initOptionFromClass(clz.getSuperclass());
}
Syntax syntax = clz.getAnnotation(Syntax.class);
if (syntax != null) {
this.onlineHelp = syntax.onlineHelp();
}
Field[] fs = clz.getDeclaredFields();
for (Field f : fs) {
Opt opt = f.getAnnotation(Opt.class);
if (opt != null) {
f.setAccessible(true);
Option option = new Option();
option.field = f;
option.description = opt.description();
option.hasArg = opt.hasArg();
option.required = opt.required();
if ("".equals(opt.longOpt()) && "".equals(opt.opt())) { // into automode
option.longOpt = fromCamel(f.getName());
if (f.getType().equals(boolean.class)) {
option.hasArg=false;
try {
if (f.getBoolean(this)) {
throw new RuntimeException("the value of " + f +
" must be false, as it is declared as no args");
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
checkConflict(option, "--" + option.longOpt);
continue;
}
if (!opt.hasArg()) {
if (!f.getType().equals(boolean.class)) {
throw new RuntimeException("the type of " + f
+ " must be boolean, as it is declared as no args");
}
try {
if (f.getBoolean(this)) {
throw new RuntimeException("the value of " + f +
" must be false, as it is declared as no args");
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
boolean haveLongOpt = false;
if (!"".equals(opt.longOpt())) {
option.longOpt = opt.longOpt();
checkConflict(option, "--" + option.longOpt);
haveLongOpt = true;
}
if (!"".equals(opt.argName())) {
option.argName = opt.argName();
}
if (!"".equals(opt.opt())) {
option.opt = opt.opt();
checkConflict(option, "-" + option.opt);
} else {
if (!haveLongOpt) {
throw new RuntimeException("opt or longOpt is not set in @Opt(...) " + f);
}
}
}
}
}
private void checkConflict(Option option, String key) {
if (optMap.containsKey(key)) {
Option preOption = optMap.get(key);
throw new RuntimeException(String.format("[@Opt(...) %s] conflict with [@Opt(...) %s]",
preOption.field.toString(), option.field
));
}
optMap.put(key, option);
}
private static String fromCamel(String name) {
if (name.length() == 0) {
return "";
}
StringBuilder sb = new StringBuilder();
char[] charArray = name.toCharArray();
sb.append(Character.toLowerCase(charArray[0]));
for (int i = 1; i < charArray.length; i++) {
char c = charArray[i];
if (Character.isUpperCase(c)) {
sb.append("-").append(Character.toLowerCase(c));
} else {
sb.append(c);
}
}
return sb.toString();
}
protected void parseSetArgs(String... args) throws IllegalArgumentException, IllegalAccessException {
this.orginalArgs = args;
List<String> remainsOptions = new ArrayList<String>();
Set<Option> requiredOpts = collectRequriedOptions(optMap);
Option needArgOpt = null;
for (String s : args) {
if (needArgOpt != null) {
Field field = needArgOpt.field;
Class clazz = field.getType();
if (clazz.equals(List.class)) {
try {
List<Object> object = ((List<Object>) field.get(this));
// 获取List对象的泛型类型
ParameterizedType listGenericType = (ParameterizedType) field.getGenericType();
Type[] listActualTypeArguments = listGenericType.getActualTypeArguments();
Class typeClazz = (Class) listActualTypeArguments[0];
object.add(convert(s, typeClazz));
} catch (Exception e) {
e.printStackTrace();
}
} else {
field.set(this, convert(s, clazz));
}
needArgOpt = null;
} else if (s.startsWith("-")) {// its a short or long option
Option opt = optMap.get(s);
requiredOpts.remove(opt);
if (opt == null) {
System.err.println("ERROR: Unrecognized option: " + s);
throw new HelpException();
} else {
if (opt.hasArg) {
needArgOpt = opt;
} else {
opt.field.set(this, true);
}
}
} else {
remainsOptions.add(s);
}
}
if (needArgOpt != null) {
System.err.println("ERROR: Option " + needArgOpt.getOptAndLongOpt() + " need an argument value");
throw new HelpException();
}
this.remainingArgs = remainsOptions.toArray(new String[remainsOptions.size()]);
if (this.printHelp) {
throw new HelpException();
}
if (!requiredOpts.isEmpty()) {
StringBuilder sb = new StringBuilder();
sb.append("ERROR: Options: ");
boolean first = true;
for (Option option : requiredOpts) {
if (first) {
first = false;
} else {
sb.append(" and ");
}
sb.append(option.getOptAndLongOpt());
}
sb.append(" is required");
System.err.println(sb.toString());
throw new HelpException();
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
protected Object convert(String value, Class type) {
if (type.equals(String.class)) {
return value;
}
if (type.equals(int.class) || type.equals(Integer.class)) {
return Integer.parseInt(value);
}
if (type.equals(long.class) || type.equals(Long.class)) {
return Long.parseLong(value);
}
if (type.equals(float.class) || type.equals(Float.class)) {
return Float.parseFloat(value);
}
if (type.equals(double.class) || type.equals(Double.class)) {
return Double.parseDouble(value);
}
if (type.equals(boolean.class) || type.equals(Boolean.class)) {
return Boolean.parseBoolean(value);
}
if (type.equals(File.class)) {
return new File(value);
}
if (type.equals(Path.class)) {
return new File(value).toPath();
}
try {
type.asSubclass(Enum.class);
return Enum.valueOf(type, value);
} catch (Exception e) {
}
throw new RuntimeException("can't convert [" + value + "] to type " + type);
}
private Set<Option> collectRequriedOptions(Map<String, Option> optMap) {
Set<Option> options = new HashSet<Option>();
for (Map.Entry<String, Option> e : optMap.entrySet()) {
Option option = e.getValue();
if (option.required) {
options.add(option);
}
}
return options;
}
@SuppressWarnings("serial")
protected static class HelpException extends RuntimeException {
public HelpException() {
super();
}
public HelpException(String message) {
super(message);
}
}
protected void usage() {
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.err, StandardCharsets.UTF_8), true);
final int maxLength = 80;
final int maxPaLength = 40;
// out.println(this.cmdName + " -- " + desc);
// out.println("usage: " + this.cmdName + " " + cmdLineSyntax);
if (this.optMap.size() > 0) {
out.println("options:");
}
// [PART.A.........][Part.B
// .-a,--aa.<arg>...desc1
// .................desc2
// .-b,--bb
TreeSet<Option> options = new TreeSet<Option>(this.optMap.values());
int palength = -1;
for (Option option : options) {
int pa = 4 + option.getOptAndLongOpt().length();
if (option.hasArg) {
pa += 3 + option.argName.length();
}
if (pa < maxPaLength) {
if (pa > palength) {
palength = pa;
}
}
}
int pblength = maxLength - palength;
StringBuilder sb = new StringBuilder();
for (Option option : options) {
sb.setLength(0);
sb.append(" ").append(option.getOptAndLongOpt());
if (option.hasArg) {
sb.append(" <").append(option.argName).append(">");
}
String desc = option.description;
if (desc == null || desc.length() == 0) {// no description
out.println(sb);
} else {
for (int i = palength - sb.length(); i > 0; i--) {
sb.append(' ');
}
if (sb.length() > maxPaLength) {// to huge part A
out.println(sb);
sb.setLength(0);
for (int i = 0; i < palength; i++) {
sb.append(' ');
}
}
int nextStart = 0;
while (nextStart < desc.length()) {
if (desc.length() - nextStart < pblength) {// can put in one line
sb.append(desc.substring(nextStart));
out.println(sb);
nextStart = desc.length();
sb.setLength(0);
} else {
sb.append(desc.substring(nextStart, nextStart + pblength));
out.println(sb);
nextStart += pblength;
sb.setLength(0);
if (nextStart < desc.length()) {
for (int i = 0; i < palength; i++) {
sb.append(' ');
}
}
}
}
if (sb.length() > 0) {
out.println(sb);
sb.setLength(0);
}
}
}
String ver = getVersionString();
if (ver != null && !"".equals(ver)) {
out.println("version: " + ver);
}
if (onlineHelp != null && !"".equals(onlineHelp)) {
if (onlineHelp.length() + "online help: ".length() > maxLength) {
out.println("online help: ");
out.println(onlineHelp);
} else {
out.println("online help: " + onlineHelp);
}
}
out.flush();
}
public static String getBaseName(String fn) {
int x = fn.lastIndexOf('.');
return x >= 0 ? fn.substring(0, x) : fn;
}
// 获取文件不包含后缀的名称
public static String getBaseName(File fn) {
return getBaseName(fn.getName());
}
}
package com.storm.wind.xpatch.base;
import java.io.File;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
/**
* Created by Wind
*/
public abstract class BaseCommand {
private String onlineHelp;
protected Map<String, Option> optMap = new HashMap<String, Option>();
@Opt(opt = "h", longOpt = "help", hasArg = false, description = "Print this help message")
private boolean printHelp = false;
protected String[] remainingArgs;
protected String[] orginalArgs;
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = { ElementType.FIELD })
static public @interface Opt {
String argName() default "";
String description() default "";
boolean hasArg() default true;
String longOpt() default "";
String opt() default "";
boolean required() default false;
}
static protected class Option implements Comparable<Option> {
public String argName = "arg";
public String description;
public Field field;
public boolean hasArg = true;
public String longOpt;
public String opt;
public boolean required = false;
@Override
public int compareTo(Option o) {
int result = s(this.opt, o.opt);
if (result == 0) {
result = s(this.longOpt, o.longOpt);
if (result == 0) {
result = s(this.argName, o.argName);
if (result == 0) {
result = s(this.description, o.description);
}
}
}
return result;
}
private static int s(String a, String b) {
if (a != null && b != null) {
return a.compareTo(b);
} else if (a != null) {
return 1;
} else if (b != null) {
return -1;
} else {
return 0;
}
}
public String getOptAndLongOpt() {
StringBuilder sb = new StringBuilder();
boolean havePrev = false;
if (opt != null && opt.length() > 0) {
sb.append("-").append(opt);
havePrev = true;
}
if (longOpt != null && longOpt.length() > 0) {
if (havePrev) {
sb.append(",");
}
sb.append("--").append(longOpt);
}
return sb.toString();
}
}
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = { ElementType.TYPE })
static public @interface Syntax {
String cmd();
String desc() default "";
String onlineHelp() default "";
String syntax() default "";
}
public void doMain(String... args) {
try {
initOptions();
parseSetArgs(args);
doCommandLine();
} catch (HelpException e) {
String msg = e.getMessage();
if (msg != null && msg.length() > 0) {
System.err.println("ERROR: " + msg);
}
usage();
} catch (Exception e) {
e.printStackTrace(System.err);
}
}
protected abstract void doCommandLine() throws Exception;
protected String getVersionString() {
return getClass().getPackage().getImplementationVersion();
}
protected void initOptions() {
initOptionFromClass(this.getClass());
}
protected void initOptionFromClass(Class<?> clz) {
if (clz == null) {
return;
} else {
initOptionFromClass(clz.getSuperclass());
}
Syntax syntax = clz.getAnnotation(Syntax.class);
if (syntax != null) {
this.onlineHelp = syntax.onlineHelp();
}
Field[] fs = clz.getDeclaredFields();
for (Field f : fs) {
Opt opt = f.getAnnotation(Opt.class);
if (opt != null) {
f.setAccessible(true);
Option option = new Option();
option.field = f;
option.description = opt.description();
option.hasArg = opt.hasArg();
option.required = opt.required();
if ("".equals(opt.longOpt()) && "".equals(opt.opt())) { // into automode
option.longOpt = fromCamel(f.getName());
if (f.getType().equals(boolean.class)) {
option.hasArg=false;
try {
if (f.getBoolean(this)) {
throw new RuntimeException("the value of " + f +
" must be false, as it is declared as no args");
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
checkConflict(option, "--" + option.longOpt);
continue;
}
if (!opt.hasArg()) {
if (!f.getType().equals(boolean.class)) {
throw new RuntimeException("the type of " + f
+ " must be boolean, as it is declared as no args");
}
try {
if (f.getBoolean(this)) {
throw new RuntimeException("the value of " + f +
" must be false, as it is declared as no args");
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
boolean haveLongOpt = false;
if (!"".equals(opt.longOpt())) {
option.longOpt = opt.longOpt();
checkConflict(option, "--" + option.longOpt);
haveLongOpt = true;
}
if (!"".equals(opt.argName())) {
option.argName = opt.argName();
}
if (!"".equals(opt.opt())) {
option.opt = opt.opt();
checkConflict(option, "-" + option.opt);
} else {
if (!haveLongOpt) {
throw new RuntimeException("opt or longOpt is not set in @Opt(...) " + f);
}
}
}
}
}
private void checkConflict(Option option, String key) {
if (optMap.containsKey(key)) {
Option preOption = optMap.get(key);
throw new RuntimeException(String.format("[@Opt(...) %s] conflict with [@Opt(...) %s]",
preOption.field.toString(), option.field
));
}
optMap.put(key, option);
}
private static String fromCamel(String name) {
if (name.length() == 0) {
return "";
}
StringBuilder sb = new StringBuilder();
char[] charArray = name.toCharArray();
sb.append(Character.toLowerCase(charArray[0]));
for (int i = 1; i < charArray.length; i++) {
char c = charArray[i];
if (Character.isUpperCase(c)) {
sb.append("-").append(Character.toLowerCase(c));
} else {
sb.append(c);
}
}
return sb.toString();
}
protected void parseSetArgs(String... args) throws IllegalArgumentException, IllegalAccessException {
this.orginalArgs = args;
List<String> remainsOptions = new ArrayList<String>();
Set<Option> requiredOpts = collectRequriedOptions(optMap);
Option needArgOpt = null;
for (String s : args) {
if (needArgOpt != null) {
Field field = needArgOpt.field;
Class clazz = field.getType();
if (clazz.equals(List.class)) {
try {
List<Object> object = ((List<Object>) field.get(this));
// 获取List对象的泛型类型
ParameterizedType listGenericType = (ParameterizedType) field.getGenericType();
Type[] listActualTypeArguments = listGenericType.getActualTypeArguments();
Class typeClazz = (Class) listActualTypeArguments[0];
object.add(convert(s, typeClazz));
} catch (Exception e) {
e.printStackTrace();
}
} else {
field.set(this, convert(s, clazz));
}
needArgOpt = null;
} else if (s.startsWith("-")) {// its a short or long option
Option opt = optMap.get(s);
requiredOpts.remove(opt);
if (opt == null) {
System.err.println("ERROR: Unrecognized option: " + s);
throw new HelpException();
} else {
if (opt.hasArg) {
needArgOpt = opt;
} else {
opt.field.set(this, true);
}
}
} else {
remainsOptions.add(s);
}
}
if (needArgOpt != null) {
System.err.println("ERROR: Option " + needArgOpt.getOptAndLongOpt() + " need an argument value");
throw new HelpException();
}
this.remainingArgs = remainsOptions.toArray(new String[remainsOptions.size()]);
if (this.printHelp) {
throw new HelpException();
}
if (!requiredOpts.isEmpty()) {
StringBuilder sb = new StringBuilder();
sb.append("ERROR: Options: ");
boolean first = true;
for (Option option : requiredOpts) {
if (first) {
first = false;
} else {
sb.append(" and ");
}
sb.append(option.getOptAndLongOpt());
}
sb.append(" is required");
System.err.println(sb.toString());
throw new HelpException();
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
protected Object convert(String value, Class type) {
if (type.equals(String.class)) {
return value;
}
if (type.equals(int.class) || type.equals(Integer.class)) {
return Integer.parseInt(value);
}
if (type.equals(long.class) || type.equals(Long.class)) {
return Long.parseLong(value);
}
if (type.equals(float.class) || type.equals(Float.class)) {
return Float.parseFloat(value);
}
if (type.equals(double.class) || type.equals(Double.class)) {
return Double.parseDouble(value);
}
if (type.equals(boolean.class) || type.equals(Boolean.class)) {
return Boolean.parseBoolean(value);
}
if (type.equals(File.class)) {
return new File(value);
}
if (type.equals(Path.class)) {
return new File(value).toPath();
}
try {
type.asSubclass(Enum.class);
return Enum.valueOf(type, value);
} catch (Exception e) {
}
throw new RuntimeException("can't convert [" + value + "] to type " + type);
}
private Set<Option> collectRequriedOptions(Map<String, Option> optMap) {
Set<Option> options = new HashSet<Option>();
for (Map.Entry<String, Option> e : optMap.entrySet()) {
Option option = e.getValue();
if (option.required) {
options.add(option);
}
}
return options;
}
@SuppressWarnings("serial")
protected static class HelpException extends RuntimeException {
public HelpException() {
super();
}
public HelpException(String message) {
super(message);
}
}
protected void usage() {
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.err, StandardCharsets.UTF_8), true);
final int maxLength = 80;
final int maxPaLength = 40;
// out.println(this.cmdName + " -- " + desc);
// out.println("usage: " + this.cmdName + " " + cmdLineSyntax);
if (this.optMap.size() > 0) {
out.println("options:");
}
// [PART.A.........][Part.B
// .-a,--aa.<arg>...desc1
// .................desc2
// .-b,--bb
TreeSet<Option> options = new TreeSet<Option>(this.optMap.values());
int palength = -1;
for (Option option : options) {
int pa = 4 + option.getOptAndLongOpt().length();
if (option.hasArg) {
pa += 3 + option.argName.length();
}
if (pa < maxPaLength) {
if (pa > palength) {
palength = pa;
}
}
}
int pblength = maxLength - palength;
StringBuilder sb = new StringBuilder();
for (Option option : options) {
sb.setLength(0);
sb.append(" ").append(option.getOptAndLongOpt());
if (option.hasArg) {
sb.append(" <").append(option.argName).append(">");
}
String desc = option.description;
if (desc == null || desc.length() == 0) {// no description
out.println(sb);
} else {
for (int i = palength - sb.length(); i > 0; i--) {
sb.append(' ');
}
if (sb.length() > maxPaLength) {// to huge part A
out.println(sb);
sb.setLength(0);
for (int i = 0; i < palength; i++) {
sb.append(' ');
}
}
int nextStart = 0;
while (nextStart < desc.length()) {
if (desc.length() - nextStart < pblength) {// can put in one line
sb.append(desc.substring(nextStart));
out.println(sb);
nextStart = desc.length();
sb.setLength(0);
} else {
sb.append(desc.substring(nextStart, nextStart + pblength));
out.println(sb);
nextStart += pblength;
sb.setLength(0);
if (nextStart < desc.length()) {
for (int i = 0; i < palength; i++) {
sb.append(' ');
}
}
}
}
if (sb.length() > 0) {
out.println(sb);
sb.setLength(0);
}
}
}
String ver = getVersionString();
if (ver != null && !"".equals(ver)) {
out.println("version: " + ver);
}
if (onlineHelp != null && !"".equals(onlineHelp)) {
if (onlineHelp.length() + "online help: ".length() > maxLength) {
out.println("online help: ");
out.println(onlineHelp);
} else {
out.println("online help: " + onlineHelp);
}
}
out.flush();
}
public static String getBaseName(String fn) {
int x = fn.lastIndexOf('.');
return x >= 0 ? fn.substring(0, x) : fn;
}
// 获取文件不包含后缀的名称
public static String getBaseName(File fn) {
return getBaseName(fn.getName());
}
}

View File

@ -125,7 +125,7 @@ public class FileUtils {
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;
FileOutputStream outputStream = null;
@ -146,12 +146,14 @@ public class FileUtils {
buffer.position(0);
oChannel.write(buffer);
}
return true;
} catch (IOException e) {
e.printStackTrace();
} finally {
close(inputStream);
close(outputStream);
}
return false;
}
public static void deleteDir(File file) {