diff --git a/app/build.gradle b/app/build.gradle index 9c794f1..b6e22b0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,6 +28,9 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' implementation("com.squareup.okhttp3:okhttp:4.2.2") + implementation 'io.reactivex.rxjava2:rxjava:2.2.17' + implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' +// implementation "io.reactivex.rxjava3:rxjava:3.0.0-RC9" implementation project(':zhttp') } diff --git a/app/src/main/java/com/zpj/http/demo/MainActivity.java b/app/src/main/java/com/zpj/http/demo/MainActivity.java index 3fa8767..e6b4d6c 100644 --- a/app/src/main/java/com/zpj/http/demo/MainActivity.java +++ b/app/src/main/java/com/zpj/http/demo/MainActivity.java @@ -1,19 +1,17 @@ package com.zpj.http.demo; -import androidx.appcompat.app.AppCompatActivity; - import android.os.Bundle; import android.util.Log; import android.widget.TextView; +import androidx.appcompat.app.AppCompatActivity; + import com.zpj.http.ZHttp; import com.zpj.http.core.IHttp; -import java.io.IOException; - -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; +import io.reactivex.Scheduler; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; public class MainActivity extends AppCompatActivity { @@ -24,71 +22,93 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final TextView contentText = findViewById(R.id.text_content); - new Thread(new Runnable() { - @Override - public void run() { - try { -// long time3 = System.currentTimeMillis(); -// Log.d(TAG, "time3=" + time3); -// OkHttpClient client = new OkHttpClient(); -// Request request = new Request.Builder() -// .url("https://www.baidu.com/") -// .build(); -// try (Response response = client.newCall(request).execute()) { -// final String content = response.body().string(); -// long time4 = System.currentTimeMillis(); -// Log.d(TAG, "time4=" + time4); -// Log.d(TAG, "delta2=" + (time4 - time3)); -//// contentText.post(new Runnable() { -//// @Override -//// public void run() { -//// contentText.setText(content); -//// } -//// }); -// } - - - - - -// long time1 = System.currentTimeMillis(); -// Log.d(TAG, "time1=" + time1); -// final String body = ZHttp.get("https://www.baidu.com/").toStr(); -// long time2 = System.currentTimeMillis(); -// Log.d(TAG, "time2=" + time2); -// Log.d(TAG, "delta1=" + (time2 - time1)); +// new Thread(new Runnable() { +// @Override +// public void run() { +// try { +//// long time3 = System.currentTimeMillis(); +//// Log.d(TAG, "time3=" + time3); +//// OkHttpClient client = new OkHttpClient(); +//// Request request = new Request.Builder() +//// .url("https://www.baidu.com/") +//// .build(); +//// try (Response response = client.newCall(request).execute()) { +//// final String content = response.body().string(); +//// long time4 = System.currentTimeMillis(); +//// Log.d(TAG, "time4=" + time4); +//// Log.d(TAG, "delta2=" + (time4 - time3)); +////// contentText.post(new Runnable() { +////// @Override +////// public void run() { +////// contentText.setText(content); +////// } +////// }); +//// } +// +// +// +// +// +//// long time1 = System.currentTimeMillis(); +//// Log.d(TAG, "time1=" + time1); +//// final String body = ZHttp.get("https://www.baidu.com/").toStr(); +//// long time2 = System.currentTimeMillis(); +//// Log.d(TAG, "time2=" + time2); +//// Log.d(TAG, "delta1=" + (time2 - time1)); +//// contentText.post(new Runnable() { +//// @Override +//// public void run() { +//// contentText.setText(body); +//// } +//// }); +// +// // https://api.heweather.com/x3/weather +//// "city", "beijing" +//// "key", "d17ce22ec5404ed883e1cfcaca0ecaa7" +// final String b = ZHttp.get("https://api.heweather.com/x3/weather") +// .data("city", "beijing") +// .data("key", "d17ce22ec5404ed883e1cfcaca0ecaa7") +// .onRedirect(new IHttp.OnRedirectListener() { +// @Override +// public boolean onRedirect(String redirectUrl) { +// Log.d("onRedirect", "redirectUrl=" + redirectUrl); +// return true; +// } +// }) +// .toStr(); // contentText.post(new Runnable() { // @Override // public void run() { -// contentText.setText(body); +// contentText.setText(b); // } // }); +// +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } +// }).start(); - // https://api.heweather.com/x3/weather -// "city", "beijing" -// "key", "d17ce22ec5404ed883e1cfcaca0ecaa7" - final String b = ZHttp.get("https://api.heweather.com/x3/weather") - .data("city", "beijing") - .data("key", "d17ce22ec5404ed883e1cfcaca0ecaa7") - .onRedirect(new IHttp.OnRedirectListener() { - @Override - public boolean onRedirect(String redirectUrl) { - Log.d("onRedirect", "redirectUrl=" + redirectUrl); - return true; - } - }) - .toStr(); - contentText.post(new Runnable() { - @Override - public void run() { - contentText.setText(b); - } - }); + ZHttp.get("https://api.heweather.com/x3/weather") + .data("city", "beijing") + .data("key", "d17ce22ec5404ed883e1cfcaca0ecaa7") + .onRedirect(new IHttp.OnRedirectListener() { + @Override + public boolean onRedirect(String redirectUrl) { + Log.d("onRedirect", "redirectUrl=" + redirectUrl); + return true; + } + }) + .toStr() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .onSuccess(new IHttp.OnSuccessListener() { + @Override + public void onSuccess(String data) { + contentText.setText(data); + } + }) + .subscribe(); - } catch (IOException e) { - e.printStackTrace(); - } - } - }).start(); } } diff --git a/zhttp/build.gradle b/zhttp/build.gradle index 8eefb60..cd592f8 100644 --- a/zhttp/build.gradle +++ b/zhttp/build.gradle @@ -31,4 +31,8 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + +// implementation("com.squareup.okhttp3:okhttp:4.2.2") + implementation 'io.reactivex.rxjava2:rxjava:2.2.17' + implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' } diff --git a/zhttp/src/main/java/com/zpj/http/ZHttp.java b/zhttp/src/main/java/com/zpj/http/ZHttp.java index 02ada54..8dff1bd 100644 --- a/zhttp/src/main/java/com/zpj/http/ZHttp.java +++ b/zhttp/src/main/java/com/zpj/http/ZHttp.java @@ -196,28 +196,28 @@ public static Document parseBodyFragment(String bodyHtml) { return Parser.parseBodyFragment(bodyHtml, ""); } - /** - Fetch a URL, and parse it as HTML. Provided for compatibility; in most cases use {@link #connect(String)} instead. -

- The encoding character set is determined by the content-type header or http-equiv meta tag, or falls back to {@code UTF-8}. - - @param url URL to fetch (with a GET). The protocol must be {@code http} or {@code https}. - @param timeoutMillis Connection and read timeout, in milliseconds. If exceeded, IOException is thrown. - @return The parsed HTML. - - @throws java.net.MalformedURLException if the request URL is not a HTTP or HTTPS URL, or is otherwise malformed - @throws HttpStatusException if the response is not OK and HTTP response errors are not ignored - @throws UnsupportedMimeTypeException if the response mime type is not supported and those errors are not ignored - @throws java.net.SocketTimeoutException if the connection times out - @throws IOException if a connection or read error occurs - - @see #connect(String) - */ - public static Document parse(URL url, int timeoutMillis) throws IOException { - Connection con = ConnectionFactory.createHttpConnection(url); - con.timeout(timeoutMillis); - return con.toHtml(); - } +// /** +// Fetch a URL, and parse it as HTML. Provided for compatibility; in most cases use {@link #connect(String)} instead. +//

+// The encoding character set is determined by the content-type header or http-equiv meta tag, or falls back to {@code UTF-8}. +// +// @param url URL to fetch (with a GET). The protocol must be {@code http} or {@code https}. +// @param timeoutMillis Connection and read timeout, in milliseconds. If exceeded, IOException is thrown. +// @return The parsed HTML. +// +// @throws java.net.MalformedURLException if the request URL is not a HTTP or HTTPS URL, or is otherwise malformed +// @throws HttpStatusException if the response is not OK and HTTP response errors are not ignored +// @throws UnsupportedMimeTypeException if the response mime type is not supported and those errors are not ignored +// @throws java.net.SocketTimeoutException if the connection times out +// @throws IOException if a connection or read error occurs +// +// @see #connect(String) +// */ +// public static Document parse(URL url, int timeoutMillis) throws IOException { +// Connection con = ConnectionFactory.createHttpConnection(url); +// con.timeout(timeoutMillis); +// return con.toHtml(); +// } /** Get safe HTML from untrusted input HTML, by parsing input HTML and filtering it through a white-list of permitted diff --git a/zhttp/src/main/java/com/zpj/http/core/AbstractConnection.java b/zhttp/src/main/java/com/zpj/http/core/AbstractConnection.java index 800dd3c..38a6e79 100644 --- a/zhttp/src/main/java/com/zpj/http/core/AbstractConnection.java +++ b/zhttp/src/main/java/com/zpj/http/core/AbstractConnection.java @@ -9,37 +9,31 @@ import org.json.JSONException; import org.json.JSONObject; -import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.Proxy; -import java.net.URI; -import java.net.URISyntaxException; import java.net.URL; import java.util.Collection; import java.util.Map; import javax.net.ssl.SSLSocketFactory; -public abstract class AbstractConnection implements Connection { +import io.reactivex.Observable; +import io.reactivex.ObservableEmitter; +import io.reactivex.ObservableOnSubscribe; - public static final String DEFAULT_UA = - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36"; - private static final String USER_AGENT = "User-Agent"; - private static final String REFERER = "Referer"; - private static final String COOKIE = "Cookie"; - public static final String CONTENT_TYPE = "Content-Type"; - public static final String MULTIPART_FORM_DATA = "multipart/form-data"; +public abstract class AbstractConnection implements Connection { // protected IHttp.OnRedirectListener onRedirectListener; - protected IHttp.OnSuccessListener onSuccessListener; - protected IHttp.OnErrorListener onErrorListener; +// protected IHttp.OnSubscribeListener onSubscribeListener; +// protected IHttp.OnSuccessListener onSuccessListener; +// protected IHttp.OnErrorListener onErrorListener; +// protected IHttp.OnCompleteListener onCompleteListener; - protected final Connection.Request req; - protected Connection.Response res; + + protected final Request req; + protected Response res; public AbstractConnection() { req = createRequest(); @@ -105,6 +99,12 @@ public Connection ignoreContentType(boolean ignoreContentType) { return this; } + @Override + public Connection validateTLSCertificates(boolean value) { + req.validateTLSCertificates(value); + return this; + } + @Override public Connection data(String key, String value) { req.data(HttpKeyVal.create(key, value)); @@ -147,10 +147,10 @@ public Connection data(Map data) { @Override public Connection data(String... keyvals) { Validate.notNull(keyvals, "Data key value pairs must not be null"); - Validate.isTrue(keyvals.length %2 == 0, "Must supply an even number of key value pairs"); + Validate.isTrue(keyvals.length % 2 == 0, "Must supply an even number of key value pairs"); for (int i = 0; i < keyvals.length; i += 2) { String key = keyvals[i]; - String value = keyvals[i+1]; + String value = keyvals[i + 1]; Validate.notEmpty(key, "Data key must not be empty"); Validate.notNull(value, "Data value must not be null"); req.data(HttpKeyVal.create(key, value)); @@ -161,16 +161,16 @@ public Connection data(String... keyvals) { @Override public Connection data(Collection data) { Validate.notNull(data, "Data collection must not be null"); - for (Connection.KeyVal entry: data) { + for (KeyVal entry : data) { req.data(entry); } return this; } @Override - public Connection.KeyVal data(String key) { + public KeyVal data(String key) { Validate.notEmpty(key, "Data key must not be empty"); - for (Connection.KeyVal keyVal : request().data()) { + for (KeyVal keyVal : request().data()) { if (keyVal.key().equals(key)) return keyVal; } @@ -190,10 +190,10 @@ public Connection header(String name, String value) { } @Override - public Connection headers(Map headers) { + public Connection headers(Map headers) { Validate.notNull(headers, "Header map must not be null"); - for (Map.Entry entry : headers.entrySet()) { - req.header(entry.getKey(),entry.getValue()); + for (Map.Entry entry : headers.entrySet()) { + req.header(entry.getKey(), entry.getValue()); } return this; } @@ -201,7 +201,7 @@ public Connection headers(Map headers) { @Override public Connection cookie(String cookie) { Validate.notNull(cookie, "Cookie must not be null"); - req.header(COOKIE, cookie); + req.header(HttpHeader.COOKIE, cookie); return this; } @@ -235,46 +235,134 @@ public Connection postDataCharset(String charset) { //------------------------------------------------------------------------------------------------------ - public Connection.Request request() { + public Request request() { return req; } - public Connection.Response response() { + public Response response() { return res; } - @Override - public String toStr() throws IOException { - res = execute(); - return res.body(); - } - - @Override - public Document toHtml() throws IOException { - res = execute(); - return res.parse(); - } - - @Override - public JSONObject toJsonObject() throws IOException, JSONException { - res = execute(); - return new JSONObject(res.body()); - } - - @Override - public JSONArray toJsonArray() throws IOException, JSONException { - res = execute(); - return new JSONArray(res.body()); - } - - @Override - public Document toXml() throws IOException { - res = execute(); - return res.parse(); +// @Override +// public String toStr() throws IOException { +// res = execute(); +// return res.body(); +// } +// +// @Override +// public Document toHtml() throws IOException { +// res = execute(); +// return res.parse(); +// } +// +// @Override +// public JSONObject toJsonObject() throws IOException, JSONException { +// res = execute(); +// return new JSONObject(res.body()); +// } +// +// @Override +// public JSONArray toJsonArray() throws IOException, JSONException { +// res = execute(); +// return new JSONArray(res.body()); +// } +// +// @Override +// public Document toXml() throws IOException { +// res = execute(); +// return res.parse(); +// } + + + @Override + public final HttpObservable execute() { + Observable observable = Observable.create(new ObservableOnSubscribe() { + + @Override + public void subscribe(ObservableEmitter emitter) throws Exception { + res = onExecute(); + emitter.onNext(res); + emitter.onComplete(); + } + }); + return new HttpObservable<>(observable); + } + + @Override + public final HttpObservable toStr() { + Observable observable = Observable.create(new ObservableOnSubscribe() { + + @Override + public void subscribe(ObservableEmitter emitter) throws Exception { + res = onExecute(); + emitter.onNext(res.body()); + emitter.onComplete(); + } + }); + return new HttpObservable<>(observable); + } + + @Override + public final HttpObservable toHtml() { + Observable observable = Observable.create(new ObservableOnSubscribe() { + + @Override + public void subscribe(ObservableEmitter emitter) throws Exception { + res = onExecute(); + Document doc = res.parse(); + emitter.onNext(doc); + emitter.onComplete(); + } + }); + return new HttpObservable<>(observable); + } + + @Override + public final HttpObservable toJsonObject() { + Observable observable = Observable.create(new ObservableOnSubscribe() { + + @Override + public void subscribe(ObservableEmitter emitter) throws Exception { + res = onExecute(); + JSONObject jsonArray = new JSONObject(res.body()); + emitter.onNext(jsonArray); + emitter.onComplete(); + } + }); + return new HttpObservable<>(observable); + } + + @Override + public final HttpObservable toJsonArray() { + Observable observable = Observable.create(new ObservableOnSubscribe() { + + @Override + public void subscribe(ObservableEmitter emitter) throws Exception { + res = onExecute(); + JSONArray jsonArray = new JSONArray(res.body()); + emitter.onNext(jsonArray); + emitter.onComplete(); + } + }); + return new HttpObservable<>(observable); + } + + @Override + public final HttpObservable toXml() { + Observable observable = Observable.create(new ObservableOnSubscribe() { + + @Override + public void subscribe(ObservableEmitter emitter) throws Exception { + res = onExecute(); + Document doc = res.parse(); + emitter.onNext(doc); + emitter.onComplete(); + } + }); + return new HttpObservable<>(observable); } - //-----------------------------------------------listeners------------------------------------------------- @Override @@ -284,48 +372,58 @@ public final Connection onRedirect(IHttp.OnRedirectListener listener) { return this; } - @Override - public final Connection onError(IHttp.OnErrorListener listener) { - this.onErrorListener = listener; - return this; - } - - @Override - public final Connection onSuccess(IHttp.OnSuccessListener listener) { - this.onSuccessListener = listener; - return this; - } - - - +// @Override +// public Connection onSubscribe(IHttp.OnSubscribeListener listener) { +// this.onSubscribeListener = listener; +// return this; +// } +// +// @Override +// public final Connection onError(IHttp.OnErrorListener listener) { +// this.onErrorListener = listener; +// return this; +// } +// +// @Override +// public final Connection onSuccess(IHttp.OnSuccessListener listener) { +// this.onSuccessListener = listener; +// return this; +// } +// +// @Override +// public Connection onComplete(IHttp.OnCompleteListener listener) { +// this.onCompleteListener = listener; +// return this; +// } //----------------------------------------------------headers--------------------------------------------------- @Override public Connection userAgent(String userAgent) { Validate.notNull(userAgent, "User-Agent must not be null"); - req.header(USER_AGENT, userAgent); + req.header(HttpHeader.USER_AGENT, userAgent); return this; } @Override public Connection referrer(String referrer) { Validate.notNull(referrer, "Referrer must not be null"); - req.header(REFERER, referrer); + req.header(HttpHeader.REFERER, referrer); return this; } @Override public Connection contentType(String contentType) { Validate.notNull(contentType, "Content-Type must not be null"); - req.header(CONTENT_TYPE, contentType); + req.header(HttpHeader.CONTENT_TYPE, contentType); return this; } //-----------------------------------------------------abstract methods----------------------------------------------------- public abstract Request createRequest(); + // public abstract Response createResponse(); - public abstract Connection.Response execute() throws IOException; + public abstract Response onExecute() throws IOException; //-------------------------------------------------static methods--------------------------------------------------------- diff --git a/zhttp/src/main/java/com/zpj/http/core/Connection.java b/zhttp/src/main/java/com/zpj/http/core/Connection.java index f805929..434d9eb 100644 --- a/zhttp/src/main/java/com/zpj/http/core/Connection.java +++ b/zhttp/src/main/java/com/zpj/http/core/Connection.java @@ -156,6 +156,27 @@ public final boolean hasBody() { */ Connection ignoreContentType(boolean ignoreContentType); + /** + * Disable/enable TLS certificates validation for HTTPS requests. + *

+ * By default this is true; all + * connections over HTTPS perform normal validation of certificates, and will abort requests if the provided + * certificate does not validate. + *

+ *

+ * Some servers use expired, self-generated certificates; or your JDK may not + * support SNI hosts. In which case, you may want to enable this setting. + *

+ *

+ * Be careful and understand why you need to disable these validations. + *

+ * @param value if should validate TLS (SSL) certificates. true by default. + * @return this Connection, for chaining + * @deprecated as distributions (specifically Google Play) are starting to show warnings if these checks are + * disabled. + */ + Connection validateTLSCertificates(boolean value); + /** * Set custom SSL socket factory * @param sslSocketFactory custom SSL socket factory @@ -320,32 +341,48 @@ public final boolean hasBody() { // OutputStream outputStream() throws IOException; - String toStr() throws IOException; +// /** +// * Execute the request. +// * @return a response object +// * @throws java.net.MalformedURLException if the request URL is not a HTTP or HTTPS URL, or is otherwise malformed +// * @throws HttpStatusException if the response is not OK and HTTP response errors are not ignored +// * @throws UnsupportedMimeTypeException if the response mime type is not supported and those errors are not ignored +// * @throws java.net.SocketTimeoutException if the connection times out +// * @throws IOException on error +// */ +// Response execute() throws IOException; - Document toHtml() throws IOException; +// String toStr() throws IOException; +// +// Document toHtml() throws IOException; +// +// JSONObject toJsonObject() throws IOException, JSONException; +// +// JSONArray toJsonArray() throws IOException, JSONException; +// +// Document toXml() throws IOException; - JSONObject toJsonObject() throws IOException, JSONException; + HttpObservable execute(); - JSONArray toJsonArray() throws IOException, JSONException; + HttpObservable toStr(); - Document toXml() throws IOException; + HttpObservable toHtml(); - Connection onRedirect(IHttp.OnRedirectListener listener); + HttpObservable toJsonObject(); - Connection onError(IHttp.OnErrorListener listener); + HttpObservable toJsonArray(); - Connection onSuccess(IHttp.OnSuccessListener listener); + HttpObservable toXml(); - /** - * Execute the request. - * @return a response object - * @throws java.net.MalformedURLException if the request URL is not a HTTP or HTTPS URL, or is otherwise malformed - * @throws HttpStatusException if the response is not OK and HTTP response errors are not ignored - * @throws UnsupportedMimeTypeException if the response mime type is not supported and those errors are not ignored - * @throws java.net.SocketTimeoutException if the connection times out - * @throws IOException on error - */ - Response execute() throws IOException; + Connection onRedirect(IHttp.OnRedirectListener listener); + +// Connection onError(IHttp.OnErrorListener listener); +// +// Connection onSuccess(IHttp.OnSuccessListener listener); +// +// Connection onComplete(IHttp.OnCompleteListener listener); +// +// Connection onSubscribe(IHttp.OnSubscribeListener listener); /** * Get the request object associated with this connection @@ -614,6 +651,22 @@ interface Request extends Base { */ Request ignoreContentType(boolean ignoreContentType); + /** + * Get the current state of TLS (SSL) certificate validation. + * @return true if TLS cert validation enabled + * @deprecated + */ + boolean validateTLSCertificates(); + + /** + * Set TLS certificate validation. True by default. + * @param value set false to ignore TLS (SSL) certificates + * @deprecated as distributions (specifically Google Play) are starting to show warnings if these checks are + * disabled. This method will be removed in the next release. + * @see #sslSocketFactory(SSLSocketFactory) + */ + void validateTLSCertificates(boolean value); + /** * Get the current custom SSL socket factory, if any. * @return custom SSL socket factory if set, null otherwise diff --git a/zhttp/src/main/java/com/zpj/http/core/HttpConnection.java b/zhttp/src/main/java/com/zpj/http/core/HttpConnection.java index 3a8a509..186257b 100644 --- a/zhttp/src/main/java/com/zpj/http/core/HttpConnection.java +++ b/zhttp/src/main/java/com/zpj/http/core/HttpConnection.java @@ -64,17 +64,12 @@ public class HttpConnection extends AbstractConnection { @Override - public Connection.Request createRequest() { + public Request createRequest() { return new HttpRequest(); } -// @Override -// public Connection.Response createResponse() { -// return new HttpResponse(); -// } - @Override - public Connection.Response execute() throws IOException { + public Response onExecute() throws IOException { return HttpResponse.execute(req); } diff --git a/zhttp/src/main/java/com/zpj/http/core/HttpConnection2.java b/zhttp/src/main/java/com/zpj/http/core/HttpConnection2.java index d2e991b..acec97c 100644 --- a/zhttp/src/main/java/com/zpj/http/core/HttpConnection2.java +++ b/zhttp/src/main/java/com/zpj/http/core/HttpConnection2.java @@ -1,4 +1,4 @@ -//package com.zpj.http.core; +package com.zpj.http.core;//package com.zpj.http.core; // //import com.zpj.http.ZHttp; //import com.zpj.http.exception.HttpStatusException; diff --git a/zhttp/src/main/java/com/zpj/http/core/HttpObservable.java b/zhttp/src/main/java/com/zpj/http/core/HttpObservable.java new file mode 100644 index 0000000..29e6223 --- /dev/null +++ b/zhttp/src/main/java/com/zpj/http/core/HttpObservable.java @@ -0,0 +1,87 @@ +package com.zpj.http.core; + +import io.reactivex.Observer; +import io.reactivex.Scheduler; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.annotations.NonNull; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +public class HttpObservable { + + private final io.reactivex.Observable observable; + + private IHttp.OnSubscribeListener onSubscribeListener; + private IHttp.OnSuccessListener onSuccessListener; + private IHttp.OnErrorListener onErrorListener; + private IHttp.OnCompleteListener onCompleteListener; + + HttpObservable(io.reactivex.Observable observable) { + this.observable = observable.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + public HttpObservable subscribeOn(Scheduler scheduler) { + observable.subscribeOn(scheduler); + return this; + } + + public HttpObservable observeOn(Scheduler scheduler) { + observable.observeOn(scheduler); + return this; + } + + public HttpObservable onSubscribe(IHttp.OnSubscribeListener listener) { + this.onSubscribeListener = listener; + return this; + } + + public final HttpObservable onError(IHttp.OnErrorListener listener) { + this.onErrorListener = listener; + return this; + } + + public final HttpObservable onSuccess(IHttp.OnSuccessListener listener) { + this.onSuccessListener = listener; + return this; + } + + public HttpObservable onComplete(IHttp.OnCompleteListener listener) { + this.onCompleteListener = listener; + return this; + } + + public HttpObservable subscribe() { + observable.subscribe(new Observer() { + @Override + public void onSubscribe(Disposable d) { + if (onSubscribeListener != null) { + onSubscribeListener.onSubscribe(d); + } + } + + @Override + public void onNext(T data) { + if (onSuccessListener != null) { + onSuccessListener.onSuccess(data); + } + } + + @Override + public void onError(Throwable e) { + if (onErrorListener != null) { + onErrorListener.onError(e); + } + } + + @Override + public void onComplete() { + if (onCompleteListener != null) { + onCompleteListener.onComplete(); + } + } + }); + return this; + } + +} diff --git a/zhttp/src/main/java/com/zpj/http/core/HttpRequest.java b/zhttp/src/main/java/com/zpj/http/core/HttpRequest.java index 62fcca6..8447760 100644 --- a/zhttp/src/main/java/com/zpj/http/core/HttpRequest.java +++ b/zhttp/src/main/java/com/zpj/http/core/HttpRequest.java @@ -30,6 +30,7 @@ public class HttpRequest extends HttpBase implements Connect private String body = null; private boolean ignoreHttpErrors = false; private boolean ignoreContentType = false; + private boolean validateTSLCertificates = true; private Parser parser; private boolean parserDefined = false; // called parser(...) vs initialized in ctor private String postDataCharset = DataUtil.defaultCharset; @@ -116,6 +117,16 @@ public Connection.Request ignoreContentType(boolean ignoreContentType) { return this; } + @Override + public boolean validateTLSCertificates() { + return validateTSLCertificates; + } + + @Override + public void validateTLSCertificates(boolean value) { + validateTSLCertificates = value; + } + public HttpRequest data(Connection.KeyVal keyval) { Validate.notNull(keyval, "Key val must not be null"); data.add(keyval); diff --git a/zhttp/src/main/java/com/zpj/http/core/HttpResponse.java b/zhttp/src/main/java/com/zpj/http/core/HttpResponse.java index dc1547e..693b7a7 100644 --- a/zhttp/src/main/java/com/zpj/http/core/HttpResponse.java +++ b/zhttp/src/main/java/com/zpj/http/core/HttpResponse.java @@ -31,6 +31,9 @@ import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; @@ -41,7 +44,13 @@ import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; +import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; import static com.zpj.http.core.Connection.Method.HEAD; @@ -52,6 +61,9 @@ public class HttpResponse extends HttpBase implements Conne private static final int HTTP_TEMP_REDIR = 307; // http/1.1 temporary redirect, not in Java's set. private static final String DefaultUploadType = "application/octet-stream"; private static final int MAX_REDIRECTS = 20; + + private static SSLSocketFactory sslSocketFactory; + private int statusCode; private String statusMessage; private ByteBuffer byteData; @@ -297,8 +309,21 @@ private static HttpURLConnection createConnection(Connection.Request req) throws conn.setConnectTimeout(req.timeout()); conn.setReadTimeout(req.timeout() / 2); // gets reduced after connection is made and status is read - if (req.sslSocketFactory() != null && conn instanceof HttpsURLConnection) - ((HttpsURLConnection) conn).setSSLSocketFactory(req.sslSocketFactory()); +// if (req.sslSocketFactory() != null && conn instanceof HttpsURLConnection) +// ((HttpsURLConnection) conn).setSSLSocketFactory(req.sslSocketFactory()); + + if (conn instanceof HttpsURLConnection) { + SSLSocketFactory socketFactory = req.sslSocketFactory(); + + if (socketFactory != null) { + ((HttpsURLConnection) conn).setSSLSocketFactory(socketFactory); + } else if (!req.validateTLSCertificates()) { + initUnSecureTSL(); + ((HttpsURLConnection)conn).setSSLSocketFactory(sslSocketFactory); + ((HttpsURLConnection)conn).setHostnameVerifier(getInsecureVerifier()); + } + } + if (req.method().hasBody()) conn.setDoOutput(true); if (req.cookies().size() > 0) @@ -311,6 +336,59 @@ private static HttpURLConnection createConnection(Connection.Request req) throws return conn; } + /** + * Initialise Trust manager that does not validate certificate chains and + * add it to current SSLContext. + *

+ * please not that this method will only perform action if sslSocketFactory is not yet + * instantiated. + * + * @throws IOException on SSL init errors + */ + private static synchronized void initUnSecureTSL() throws IOException { + if (sslSocketFactory == null) { + // Create a trust manager that does not validate certificate chains + final TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() { + + public void checkClientTrusted(final X509Certificate[] chain, final String authType) { + } + + public void checkServerTrusted(final X509Certificate[] chain, final String authType) { + } + + public X509Certificate[] getAcceptedIssuers() { + return null; + } + }}; + + // Install the all-trusting trust manager + final SSLContext sslContext; + try { + sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); + // Create an ssl socket factory with our all-trusting manager + sslSocketFactory = sslContext.getSocketFactory(); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new IOException("Can't create unsecure trust manager"); + } + } + } + + /** + * Instantiate Hostname Verifier that does nothing. + * This is used for connections with disabled SSL certificates validation. + * + * + * @return Hostname Verifier that does nothing and accepts all hostnames + */ + private static HostnameVerifier getInsecureVerifier() { + return new HostnameVerifier() { + public boolean verify(String urlHostName, SSLSession session) { + return true; + } + }; + } + /** * Call on completion of stream read, to close the body (or error) stream. The connection.disconnect allows * keep-alives to work (as the underlying connection is actually held open, despite the name). diff --git a/zhttp/src/main/java/com/zpj/http/core/IHttp.java b/zhttp/src/main/java/com/zpj/http/core/IHttp.java index 16bc51a..1208b02 100644 --- a/zhttp/src/main/java/com/zpj/http/core/IHttp.java +++ b/zhttp/src/main/java/com/zpj/http/core/IHttp.java @@ -1,5 +1,7 @@ package com.zpj.http.core; +import io.reactivex.disposables.Disposable; + public interface IHttp { interface OnRedirectListener { @@ -7,11 +9,19 @@ interface OnRedirectListener { } interface OnErrorListener { - void onError(); + void onError(Throwable throwable); + } + + interface OnSuccessListener { + void onSuccess(T data); + } + + interface OnCompleteListener { + void onComplete(); } - interface OnSuccessListener { - void onSuccess(); + interface OnSubscribeListener { + void onSubscribe(Disposable d); } interface OnStreamWriteListener { diff --git a/zhttp/src/main/java/com/zpj/http/examples/HtmlToPlainText.java b/zhttp/src/main/java/com/zpj/http/examples/HtmlToPlainText.java index 5594c07..5226f8e 100644 --- a/zhttp/src/main/java/com/zpj/http/examples/HtmlToPlainText.java +++ b/zhttp/src/main/java/com/zpj/http/examples/HtmlToPlainText.java @@ -1,4 +1,4 @@ -//package com.zpj.http.examples; +package com.zpj.http.examples;//package com.zpj.http.examples; // //import com.zpj.http.ZHttp; //import com.zpj.http.utils.StringUtil; diff --git a/zhttp/src/main/java/com/zpj/http/examples/ListLinks.java b/zhttp/src/main/java/com/zpj/http/examples/ListLinks.java index 5211b25..3295b07 100644 --- a/zhttp/src/main/java/com/zpj/http/examples/ListLinks.java +++ b/zhttp/src/main/java/com/zpj/http/examples/ListLinks.java @@ -1,4 +1,4 @@ -//package com.zpj.http.examples; +package com.zpj.http.examples;//package com.zpj.http.examples; // //import com.zpj.http.ZHttp; //import com.zpj.http.utils.Validate; diff --git a/zhttp/src/main/java/com/zpj/http/examples/Wikipedia.java b/zhttp/src/main/java/com/zpj/http/examples/Wikipedia.java index a1aac10..41bbecc 100644 --- a/zhttp/src/main/java/com/zpj/http/examples/Wikipedia.java +++ b/zhttp/src/main/java/com/zpj/http/examples/Wikipedia.java @@ -1,4 +1,4 @@ -//package com.zpj.http.examples; +package com.zpj.http.examples;//package com.zpj.http.examples; // //import com.zpj.http.ZHttp; //import com.zpj.http.parser.html.nodes.Document; diff --git a/zhttp/src/main/java/com/zpj/http/parser/html/CharacterReader.java b/zhttp/src/main/java/com/zpj/http/parser/html/CharacterReader.java index c09f768..6cf3a37 100644 --- a/zhttp/src/main/java/com/zpj/http/parser/html/CharacterReader.java +++ b/zhttp/src/main/java/com/zpj/http/parser/html/CharacterReader.java @@ -15,7 +15,7 @@ public final class CharacterReader { static final char EOF = (char) -1; private static final int maxStringCacheLen = 12; - static final int maxBufferLen = 1024 * 32; // visible for testing + private static final int maxBufferLen = 1024 * 32; // visible for testing private static final int readAheadLimit = (int) (maxBufferLen * 0.75); private final char[] charBuf; @@ -34,9 +34,10 @@ public CharacterReader(Reader input, int sz) { charBuf = new char[sz > maxBufferLen ? maxBufferLen : sz]; bufferUp(); - if (isBinary()) { - throw new UncheckedIOException("Input is binary and unsupported"); - } + // TODO +// if (isBinary()) { +// throw new UncheckedIOException("Input is binary and unsupported"); +// } } public CharacterReader(Reader input) { @@ -459,7 +460,7 @@ boolean containsIgnoreCase(String seq) { * Heuristic to determine if the current buffer looks like binary content. Reader will already hopefully be * decoded correctly, so a bunch of NULLs indicates a binary file */ - boolean isBinary() { + private boolean isBinary() { int nullsSeen = 0; for (int i = bufPos; i < bufLength; i++) { diff --git a/zhttp/src/main/java/com/zpj/http/parser/html/nodes/Element.java b/zhttp/src/main/java/com/zpj/http/parser/html/nodes/Element.java index 1c763af..2bfd610 100644 --- a/zhttp/src/main/java/com/zpj/http/parser/html/nodes/Element.java +++ b/zhttp/src/main/java/com/zpj/http/parser/html/nodes/Element.java @@ -46,6 +46,10 @@ public class Element extends Node { private Attributes attributes; private String baseUri; + protected Element() { + + } + /** * Create a new, standalone element. * @param tag tag name @@ -174,6 +178,10 @@ public boolean isBlock() { return tag.isBlock(); } + public boolean isNull() { + return this instanceof NullElement; + } + /** * Get the {@code id} attribute of this element. * @@ -227,7 +235,7 @@ public Map dataset() { } @Override - public final Element parent() { + public Element parent() { return (Element) parentNode; } @@ -361,18 +369,22 @@ public List dataNodes() { *

  • {@code el.select("a[href*=example.com]")} - finds links pointing to example.com (loosely) * *

    - * See the query syntax documentation in {@link com.zpj.http.parser.html.select.Selector}. + * See the query syntax documentation in {@link Selector}. *

    * * @param cssQuery a {@link Selector} CSS-like query * @return elements that match the query (empty if none match) - * @see com.zpj.http.parser.html.select.Selector + * @see Selector * @throws Selector.SelectorParseException (unchecked) on an invalid CSS query. */ public Elements select(String cssQuery) { return Selector.select(cssQuery, this); } + public boolean has(String cssQuery) { + return !(selectFirst(cssQuery) instanceof NullElement); + } + /** * Find the first Element that matches the {@link Selector} CSS query, with this element as the starting context. *

    This is effectively the same as calling {@code element.select(query).first()}, but is more efficient as query @@ -609,6 +621,13 @@ public Element after(Node node) { return (Element) super.after(node); } + public void remove(String cssQuery) { + Elements elements = select(cssQuery); + for (Element element : elements) { + element.remove(); + } + } + /** * Remove all of the element's child nodes. Any attributes are left as-is. * @return this element diff --git a/zhttp/src/main/java/com/zpj/http/parser/html/nodes/Node.java b/zhttp/src/main/java/com/zpj/http/parser/html/nodes/Node.java index 81bf7d2..d4b1d5d 100644 --- a/zhttp/src/main/java/com/zpj/http/parser/html/nodes/Node.java +++ b/zhttp/src/main/java/com/zpj/http/parser/html/nodes/Node.java @@ -1,6 +1,7 @@ package com.zpj.http.parser.html.nodes; import com.zpj.http.exception.SerializationException; +import com.zpj.http.parser.html.select.Elements; import com.zpj.http.utils.StringUtil; import com.zpj.http.utils.Validate; import com.zpj.http.parser.html.select.NodeFilter; @@ -527,7 +528,7 @@ public Node previousSibling() { * Get the list index of this node in its node sibling list. I.e. if this is the first node * sibling, returns 0. * @return position in node sibling list - * @see com.zpj.http.parser.html.nodes.Element#elementSiblingIndex() + * @see Element#elementSiblingIndex() */ public int siblingIndex() { return siblingIndex; diff --git a/zhttp/src/main/java/com/zpj/http/parser/html/nodes/NullElement.java b/zhttp/src/main/java/com/zpj/http/parser/html/nodes/NullElement.java new file mode 100644 index 0000000..faab62e --- /dev/null +++ b/zhttp/src/main/java/com/zpj/http/parser/html/nodes/NullElement.java @@ -0,0 +1,993 @@ +package com.zpj.http.parser.html.nodes; + +import com.zpj.http.parser.html.Tag; +import com.zpj.http.parser.html.select.Elements; +import com.zpj.http.parser.html.select.Evaluator; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * NullElement 继承自Element。当Elements get(int index)方法参数index大于等于size()时返回NullElement,不用判断Elements是否为空, + * 避免IndexOutOfBoundsException + * @author Z-P-J + */ +public class NullElement extends Element { + private static final List EMPTY_NODES = Collections.emptyList(); + + public NullElement() { + + } + + /** + * Create a new, standalone element. + * @param tag tag name + */ + private NullElement(String tag) { } + + /** + * Create a new, standalone Element. (Standalone in that is has no parent.) + * + * @param tag tag of this element + * @param baseUri the base URI + * @param attributes initial attributes + * @see #appendChild(Node) + * @see #appendElement(String) + */ + private NullElement(Tag tag, String baseUri, Attributes attributes) { } + + private NullElement(Tag tag, String baseUri) { } + + protected List ensureChildNodes() { + return EMPTY_NODES; + } + + @Override + protected boolean hasAttributes() { + return false; + } + + @Override + public Attributes attributes() { + return new Attributes(); + } + + @Override + public String baseUri() { + return null; + } + + @Override + protected void doSetBaseUri(String baseUri) { } + + @Override + public int childNodeSize() { + return 0; + } + + @Override + public String nodeName() { + return null; + } + + public String tagName() { + return null; + } + + /** + * Get the normalized name of this Element's tag. This will always be the lowercased version of the tag, regardless + * of the tag case preserving setting of the parser. + * @return + */ + public String normalName() { + return null; + } + + /** + * Change the tag of this element. For example, convert a {@code } to a {@code

    } with + * {@code el.tagName("div");}. + * + * @param tagName new tag name for this element + * @return this element, for chaining + */ + public NullElement tagName(String tagName) { + return this; + } + + /** + * Get the Tag for this element. + * + * @return the tag object + */ + public Tag tag() { + return null; + } + + /** + * Test if this element is a block-level element. (E.g. {@code
    == true} or an inline element + * {@code

    == false}). + * + * @return true if block, false if not (and thus inline) + */ + public boolean isBlock() { + return false; + } + + /** + * Get the {@code id} attribute of this element. + * + * @return The id attribute, if present, or an empty string if not. + */ + public String id() { + return EmptyString; + } + + /** + * Set an attribute value on this element. If this element already has an attribute with the + * key, its value is updated; otherwise, a new attribute is added. + * + * @return this element + */ + public NullElement attr(String attributeKey, String attributeValue) { + return this; + } + + /** + * Set a boolean attribute value on this element. Setting to true sets the attribute value to "" and + * marks the attribute as boolean so no value is written out. Setting to false removes the attribute + * with the same key if it exists. + * + * @param attributeKey the attribute key + * @param attributeValue the attribute value + * + * @return this element + */ + public NullElement attr(String attributeKey, boolean attributeValue) { + return this; + } + + /** + * Get this element's HTML5 custom data attributes. Each attribute in the element that has a key + * starting with "data-" is included the dataset. + *

    + * E.g., the element {@code

    ...} has the dataset + * {@code package=jsoup, language=java}. + *

    + * This map is a filtered view of the element's attribute map. Changes to one map (add, remove, update) are reflected + * in the other map. + *

    + * You can find elements that have data attributes using the {@code [^data-]} attribute key prefix selector. + * @return a map of {@code key=value} custom data attributes. + */ + public Map dataset() { + return attributes().dataset(); + } + + @Override + public Element parent() { + return new NullElement(null); + } + + /** + * Get this element's parent and ancestors, up to the document root. + * @return this element's stack of parents, closest first. + */ + public Elements parents() { + return new Elements(); + } + + /** + * Get a child element of this element, by its 0-based index number. + *

    + * Note that an element can have both mixed Nodes and Elements as children. This method inspects + * a filtered list of children that are elements, and the index is based on that filtered list. + *

    + * + * @param index the index number of the element to retrieve + * @return the child element, if it exists, otherwise throws an {@code IndexOutOfBoundsException} + * @see #childNode(int) + */ + public Element child(int index) { + return new NullElement(); + } + + /** + * Get this element's child elements. + *

    + * This is effectively a filter on {@link #childNodes()} to get Element nodes. + *

    + * @return child elements. If this element has no children, returns an empty list. + * @see #childNodes() + */ + public Elements children() { + return new Elements(new ArrayList(0)); + } + + /** + * Maintains a shadow copy of this element's child elements. If the nodelist is changed, this cache is invalidated. + * TODO - think about pulling this out as a helper as there are other shadow lists (like in Attributes) kept around. + * @return a list of child elements + */ + private List childElementsList() { + return new ArrayList<>(0); + } + + /** + * Clears the cached shadow child elements. + */ + @Override + void nodelistChanged() { + super.nodelistChanged(); + } + + /** + * Get this element's child text nodes. The list is unmodifiable but the text nodes may be manipulated. + *

    + * This is effectively a filter on {@link #childNodes()} to get Text nodes. + * @return child text nodes. If this element has no text nodes, returns an + * empty list. + *

    + * For example, with the input HTML: {@code

    One Two Three
    Four

    } with the {@code p} element selected: + *
      + *
    • {@code p.text()} = {@code "One Two Three Four"}
    • + *
    • {@code p.ownText()} = {@code "One Three Four"}
    • + *
    • {@code p.children()} = {@code Elements[,
      ]}
    • + *
    • {@code p.childNodes()} = {@code List["One ", , " Three ",
      , " Four"]}
    • + *
    • {@code p.textNodes()} = {@code List["One ", " Three ", " Four"]}
    • + *
    + */ + public List textNodes() { + return new ArrayList<>(0); + } + + /** + * Get this element's child data nodes. The list is unmodifiable but the data nodes may be manipulated. + *

    + * This is effectively a filter on {@link #childNodes()} to get Data nodes. + *

    + * @return child data nodes. If this element has no data nodes, returns an + * empty list. + * @see #data() + */ + public List dataNodes() { + return new ArrayList<>(0); + } + + public Elements select(String cssQuery) { + return new Elements(); + } + + public NullElement selectFirst(String cssQuery) { + return new NullElement(); + } + + public boolean is(String cssQuery) { + return false; + } + + /** + * Check if this element matches the given evaluator. + * @param evaluator an element evaluator + * @return if this element matches + */ + public boolean is(Evaluator evaluator) { + return false; + } + + /** + * Add a node child node to this element. + * + * @param child node to add. + * @return this element, so that you can add more child nodes or elements. + */ + public NullElement appendChild(Node child) { + return this; + } + + /** + * Add this element to the supplied parent element, as its next child. + * + * @param parent element to which this element will be appended + * @return this element, so that you can continue modifying the element + */ + public NullElement appendTo(NullElement parent) { + return this; + } + + /** + * Add a node to the start of this element's children. + * + * @param child node to add. + * @return this element, so that you can add more child nodes or elements. + */ + public NullElement prependChild(Node child) { + return this; + } + + + /** + * Inserts the given child nodes into this element at the specified index. Current nodes will be shifted to the + * right. The inserted nodes will be moved from their current parent. To prevent moving, copy the nodes first. + * + * @param index 0-based index to insert children at. Specify {@code 0} to insert at the start, {@code -1} at the + * end + * @param children child nodes to insert + * @return this element, for chaining. + */ + public NullElement insertChildren(int index, Collection children) { + return this; + } + + /** + * Inserts the given child nodes into this element at the specified index. Current nodes will be shifted to the + * right. The inserted nodes will be moved from their current parent. To prevent moving, copy the nodes first. + * + * @param index 0-based index to insert children at. Specify {@code 0} to insert at the start, {@code -1} at the + * end + * @param children child nodes to insert + * @return this element, for chaining. + */ + public NullElement insertChildren(int index, Node... children) { + return this; + } + + /** + * Create a new element by tag name, and add it as the last child. + * + * @param tagName the name of the tag (e.g. {@code div}). + * @return the new element, to allow you to add content to it, e.g.: + * {@code parent.appendElement("h1").attr("id", "header").text("Welcome");} + */ + public NullElement appendElement(String tagName) { + return this; + } + + /** + * Create a new element by tag name, and add it as the first child. + * + * @param tagName the name of the tag (e.g. {@code div}). + * @return the new element, to allow you to add content to it, e.g.: + * {@code parent.prependElement("h1").attr("id", "header").text("Welcome");} + */ + public NullElement prependElement(String tagName) { + return this; + } + + /** + * Create and append a new TextNode to this element. + * + * @param text the unencoded text to add + * @return this element + */ + public NullElement appendText(String text) { + return this; + } + + /** + * Create and prepend a new TextNode to this element. + * + * @param text the unencoded text to add + * @return this element + */ + public NullElement prependText(String text) { + return this; + } + + /** + * Add inner HTML to this element. The supplied HTML will be parsed, and each node appended to the end of the children. + * @param html HTML to add inside this element, after the existing HTML + * @return this element + * @see #html(String) + */ + public NullElement append(String html) { + return this; + } + + /** + * Add inner HTML into this element. The supplied HTML will be parsed, and each node prepended to the start of the element's children. + * @param html HTML to add inside this element, before the existing HTML + * @return this element + * @see #html(String) + */ + public NullElement prepend(String html) { + return this; + } + + /** + * Insert the specified HTML into the DOM before this element (as a preceding sibling). + * + * @param html HTML to add before this element + * @return this element, for chaining + * @see #after(String) + */ + @Override + public NullElement before(String html) { + return (NullElement) super.before(html); + } + + /** + * Insert the specified node into the DOM before this node (as a preceding sibling). + * @param node to add before this element + * @return this Element, for chaining + * @see #after(Node) + */ + @Override + public NullElement before(Node node) { + return (NullElement) super.before(node); + } + + /** + * Insert the specified HTML into the DOM after this element (as a following sibling). + * + * @param html HTML to add after this element + * @return this element, for chaining + * @see #before(String) + */ + @Override + public NullElement after(String html) { + return (NullElement) super.after(html); + } + + /** + * Insert the specified node into the DOM after this node (as a following sibling). + * @param node to add after this element + * @return this element, for chaining + * @see #before(Node) + */ + @Override + public NullElement after(Node node) { + return (NullElement) super.after(node); + } + + /** + * Remove all of the element's child nodes. Any attributes are left as-is. + * @return this element + */ + public NullElement empty() { + childNodes.clear(); + return this; + } + + /** + * Wrap the supplied HTML around this element. + * + * @param html HTML to wrap around this element, e.g. {@code
    }. Can be arbitrarily deep. + * @return this element, for chaining. + */ + @Override + public NullElement wrap(String html) { + return (NullElement) super.wrap(html); + } + + /** + * Get a CSS selector that will uniquely select this element. + *

    + * If the element has an ID, returns #id; + * otherwise returns the parent (if any) CSS selector, followed by {@literal '>'}, + * followed by a unique selector for the element (tag.class.class:nth-child(n)). + *

    + * + * @return the CSS Path that can be used to retrieve the element in a selector. + */ + public String cssSelector() { + return null; + } + + /** + * Get sibling elements. If the element has no sibling elements, returns an empty list. An element is not a sibling + * of itself, so will not be included in the returned list. + * @return sibling elements + */ + public Elements siblingElements() { + return new Elements(0); + } + + /** + * Gets the next sibling element of this element. E.g., if a {@code div} contains two {@code p}s, + * the {@code nextElementSibling} of the first {@code p} is the second {@code p}. + *

    + * This is similar to {@link #nextSibling()}, but specifically finds only Elements + *

    + * @return the next element, or null if there is no next element + * @see #previousElementSibling() + */ + public NullElement nextElementSibling() { + return new NullElement(null); + } + + /** + * Get each of the sibling elements that come after this element. + * + * @return each of the element siblings after this element, or an empty list if there are no next sibling elements + */ + public Elements nextElementSiblings() { + return new Elements(); + } + + /** + * Gets the previous element sibling of this element. + * @return the previous element, or null if there is no previous element + * @see #nextElementSibling() + */ + public NullElement previousElementSibling() { + return new NullElement(); + } + + /** + * Get each of the element siblings before this element. + * + * @return the previous element siblings, or an empty list if there are none. + */ + public Elements previousElementSiblings() { + return new Elements(); + } + + private Elements nextElementSiblings(boolean next) { + return new Elements(); + } + + /** + * Gets the first element sibling of this element. + * @return the first sibling that is an element (aka the parent's first element child) + */ + public NullElement firstElementSibling() { + return new NullElement(); + } + + /** + * Get the list index of this element in its element sibling list. I.e. if this is the first element + * sibling, returns 0. + * @return position in element sibling list + */ + @Override + public int elementSiblingIndex() { + return -1; + } + + /** + * Gets the last element sibling of this element + * @return the last sibling that is an element (aka the parent's last element child) + */ + public NullElement lastElementSibling() { + return new NullElement(); + } + + // DOM type methods + + /** + * Finds elements, including and recursively under this element, with the specified tag name. + * @param tagName The tag name to search for (case insensitively). + * @return a matching unmodifiable list of elements. Will be empty if this element and none of its children match. + */ + public Elements getElementsByTag(String tagName) { + return new Elements(); + } + + /** + * Find an element by ID, including or under this element. + *

    + * Note that this finds the first matching ID, starting with this element. If you search down from a different + * starting point, it is possible to find a different element by ID. For unique element by ID within a Document, + * use {@link Document#getElementById(String)} + * @param id The ID to search for. + * @return The first matching element by ID, starting with this element, or null if none found. + */ + public NullElement getElementById(String id) { + return new NullElement(); + } + + /** + * Find elements that have this class, including or under this element. Case insensitive. + *

    + * Elements can have multiple classes (e.g. {@code

    }. This method + * checks each class, so you can find the above with {@code el.getElementsByClass("header");}. + * + * @param className the name of the class to search for. + * @return elements with the supplied class name, empty if none + * @see #hasClass(String) + * @see #classNames() + */ + public Elements getElementsByClass(String className) { + return new Elements(); + } + + /** + * Find elements that have a named attribute set. Case insensitive. + * + * @param key name of the attribute, e.g. {@code href} + * @return elements that have this attribute, empty if none + */ + public Elements getElementsByAttribute(String key) { + return new Elements(); + } + + /** + * Find elements that have an attribute name starting with the supplied prefix. Use {@code data-} to find elements + * that have HTML5 datasets. + * @param keyPrefix name prefix of the attribute e.g. {@code data-} + * @return elements that have attribute names that start with with the prefix, empty if none. + */ + public Elements getElementsByAttributeStarting(String keyPrefix) { + return new Elements(); + } + + /** + * Find elements that have an attribute with the specific value. Case insensitive. + * + * @param key name of the attribute + * @param value value of the attribute + * @return elements that have this attribute with this value, empty if none + */ + public Elements getElementsByAttributeValue(String key, String value) { + return new Elements(); + } + + /** + * Find elements that either do not have this attribute, or have it with a different value. Case insensitive. + * + * @param key name of the attribute + * @param value value of the attribute + * @return elements that do not have a matching attribute + */ + public Elements getElementsByAttributeValueNot(String key, String value) { + return new Elements(); + } + + /** + * Find elements that have attributes that start with the value prefix. Case insensitive. + * + * @param key name of the attribute + * @param valuePrefix start of attribute value + * @return elements that have attributes that start with the value prefix + */ + public Elements getElementsByAttributeValueStarting(String key, String valuePrefix) { + return new Elements(); + } + + /** + * Find elements that have attributes that end with the value suffix. Case insensitive. + * + * @param key name of the attribute + * @param valueSuffix end of the attribute value + * @return elements that have attributes that end with the value suffix + */ + public Elements getElementsByAttributeValueEnding(String key, String valueSuffix) { + return new Elements(); + } + + /** + * Find elements that have attributes whose value contains the match string. Case insensitive. + * + * @param key name of the attribute + * @param match substring of value to search for + * @return elements that have attributes containing this text + */ + public Elements getElementsByAttributeValueContaining(String key, String match) { + return new Elements(); + } + + /** + * Find elements that have attributes whose values match the supplied regular expression. + * @param key name of the attribute + * @param pattern compiled regular expression to match against attribute values + * @return elements that have attributes matching this regular expression + */ + public Elements getElementsByAttributeValueMatching(String key, Pattern pattern) { + return new Elements(); + + } + + /** + * Find elements that have attributes whose values match the supplied regular expression. + * @param key name of the attribute + * @param regex regular expression to match against attribute values. You can use embedded flags (such as (?i) and (?m) to control regex options. + * @return elements that have attributes matching this regular expression + */ + public Elements getElementsByAttributeValueMatching(String key, String regex) { + return new Elements(); + } + + /** + * Find elements whose sibling index is less than the supplied index. + * @param index 0-based index + * @return elements less than index + */ + public Elements getElementsByIndexLessThan(int index) { + return new Elements(); + } + + /** + * Find elements whose sibling index is greater than the supplied index. + * @param index 0-based index + * @return elements greater than index + */ + public Elements getElementsByIndexGreaterThan(int index) { + return new Elements(); + } + + /** + * Find elements whose sibling index is equal to the supplied index. + * @param index 0-based index + * @return elements equal to index + */ + public Elements getElementsByIndexEquals(int index) { + return new Elements(); + } + + /** + * Find elements that contain the specified string. The search is case insensitive. The text may appear directly + * in the element, or in any of its descendants. + * @param searchText to look for in the element's text + * @return elements that contain the string, case insensitive. + * @see NullElement#text() + */ + public Elements getElementsContainingText(String searchText) { + return new Elements(); + } + + /** + * Find elements that directly contain the specified string. The search is case insensitive. The text must appear directly + * in the element, not in any of its descendants. + * @param searchText to look for in the element's own text + * @return elements that contain the string, case insensitive. + * @see NullElement#ownText() + */ + public Elements getElementsContainingOwnText(String searchText) { + return new Elements(); + } + + /** + * Find elements whose text matches the supplied regular expression. + * @param pattern regular expression to match text against + * @return elements matching the supplied regular expression. + * @see NullElement#text() + */ + public Elements getElementsMatchingText(Pattern pattern) { + return new Elements(); + } + + /** + * Find elements whose text matches the supplied regular expression. + * @param regex regular expression to match text against. You can use embedded flags (such as (?i) and (?m) to control regex options. + * @return elements matching the supplied regular expression. + * @see NullElement#text() + */ + public Elements getElementsMatchingText(String regex) { + return new Elements(); + } + + /** + * Find elements whose own text matches the supplied regular expression. + * @param pattern regular expression to match text against + * @return elements matching the supplied regular expression. + * @see NullElement#ownText() + */ + public Elements getElementsMatchingOwnText(Pattern pattern) { + return new Elements(); + } + + /** + * Find elements whose text matches the supplied regular expression. + * @param regex regular expression to match text against. You can use embedded flags (such as (?i) and (?m) to control regex options. + * @return elements matching the supplied regular expression. + * @see NullElement#ownText() + */ + public Elements getElementsMatchingOwnText(String regex) { + return new Elements(); + } + + /** + * Find all elements under this element (including self, and children of children). + * + * @return all elements + */ + public Elements getAllElements() { + return new Elements(); + } + + /** + * Gets the combined text of this element and all its children. Whitespace is normalized and trimmed. + *

    + * For example, given HTML {@code

    Hello there now!

    }, {@code p.text()} returns {@code "Hello there now!"} + * + * @return unencoded, normalized text, or empty string if none. + * @see #wholeText() if you don't want the text to be normalized. + * @see #ownText() + * @see #textNodes() + */ + public String text() { + return null; + } + + /** + * Get the (unencoded) text of all children of this element, including any newlines and spaces present in the + * original. + * + * @return unencoded, un-normalized text + * @see #text() + */ + public String wholeText() { + return null; + } + + /** + * Gets the text owned by this element only; does not get the combined text of all children. + *

    + * For example, given HTML {@code

    Hello there now!

    }, {@code p.ownText()} returns {@code "Hello now!"}, + * whereas {@code p.text()} returns {@code "Hello there now!"}. + * Note that the text within the {@code b} element is not returned, as it is not a direct child of the {@code p} element. + * + * @return unencoded text, or empty string if none. + * @see #text() + * @see #textNodes() + */ + public String ownText() { + return null; + } + + /** + * Set the text of this element. Any existing contents (text or elements) will be cleared + * @param text unencoded text + * @return this element + */ + public NullElement text(String text) { + return this; + } + + /** + Test if this element has any text content (that is not just whitespace). + @return true if element has non-blank text content. + */ + public boolean hasText() { + return false; + } + + /** + * Get the combined data of this element. Data is e.g. the inside of a {@code script} tag. Note that data is NOT the + * text of the element. Use {@link #text()} to get the text that would be visible to a user, and {@link #data()} + * for the contents of scripts, comments, CSS styles, etc. + * + * @return the data, or empty string if none + * + * @see #dataNodes() + */ + public String data() { + return null; + } + + /** + * Gets the literal value of this element's "class" attribute, which may include multiple class names, space + * separated. (E.g. on <div class="header gray"> returns, "header gray") + * @return The literal class attribute, or empty string if no class attribute set. + */ + public String className() { + return null; + } + + /** + * Get all of the element's class names. E.g. on element {@code
    }, + * returns a set of two elements {@code "header", "gray"}. Note that modifications to this set are not pushed to + * the backing {@code class} attribute; use the {@link #classNames(Set)} method to persist them. + * @return set of classnames, empty if no class attribute + */ + public Set classNames() { + return new LinkedHashSet<>(0); + } + + /** + Set the element's {@code class} attribute to the supplied class names. + @param classNames set of classes + @return this element, for chaining + */ + public NullElement classNames(Set classNames) { + return this; + } + + /** + * Tests if this element has a class. Case insensitive. + * @param className name of class to check for + * @return true if it does, false if not + */ + // performance sensitive + public boolean hasClass(String className) { + return false; + } + + /** + Add a class name to this element's {@code class} attribute. + @param className class name to add + @return this element + */ + public NullElement addClass(String className) { + return this; + } + + /** + Remove a class name from this element's {@code class} attribute. + @param className class name to remove + @return this element + */ + public NullElement removeClass(String className) { + return this; + } + + /** + Toggle a class name on this element's {@code class} attribute: if present, remove it; otherwise add it. + @param className class name to toggle + @return this element + */ + public NullElement toggleClass(String className) { + return this; + } + + /** + * Get the value of a form element (input, textarea, etc). + * @return the value of the form element, or empty string if not set. + */ + public String val() { + return null; + } + + /** + * Set the value of a form element (input, textarea, etc). + * @param value value to set + * @return this element (for chaining) + */ + public NullElement val(String value) { + return this; + } + + void outerHtmlHead(final Appendable accum, int depth, final Document.OutputSettings out) throws IOException { } + + void outerHtmlTail(Appendable accum, int depth, Document.OutputSettings out) throws IOException { } + + /** + * Retrieves the element's inner HTML. E.g. on a {@code
    } with one empty {@code

    }, would return + * {@code

    }. (Whereas {@link #outerHtml()} would return {@code

    }.) + * + * @return String of HTML. + * @see #outerHtml() + */ + public String html() { + return null; + } + + @Override + public T html(T appendable) { + return appendable; + } + + /** + * Set this element's inner HTML. Clears the existing HTML first. + * @param html HTML to parse and set into this element + * @return this element + * @see #append(String) + */ + public NullElement html(String html) { + return this; + } + + @Override + public NullElement clone() { + return (NullElement) super.clone(); + } + + @Override + public NullElement shallowClone() { + // simpler than implementing a clone version with no child copy + return new NullElement(); + } + + @Override + protected NullElement doClone(Node parent) { + return (NullElement) super.doClone(parent); + } + +} diff --git a/zhttp/src/main/java/com/zpj/http/parser/html/select/Collector.java b/zhttp/src/main/java/com/zpj/http/parser/html/select/Collector.java index f5dfc30..dc2388b 100644 --- a/zhttp/src/main/java/com/zpj/http/parser/html/select/Collector.java +++ b/zhttp/src/main/java/com/zpj/http/parser/html/select/Collector.java @@ -2,6 +2,7 @@ import com.zpj.http.parser.html.nodes.Element; import com.zpj.http.parser.html.nodes.Node; +import com.zpj.http.parser.html.nodes.NullElement; import static com.zpj.http.parser.html.select.NodeFilter.FilterResult.CONTINUE; import static com.zpj.http.parser.html.select.NodeFilter.FilterResult.STOP; @@ -60,7 +61,7 @@ public static Element findFirst(Evaluator eval, Element root) { private static class FirstFinder implements NodeFilter { private final Element root; - private Element match = null; + private Element match = new NullElement(); private final Evaluator eval; FirstFinder(Element root, Evaluator eval) { diff --git a/zhttp/src/main/java/com/zpj/http/parser/html/select/Elements.java b/zhttp/src/main/java/com/zpj/http/parser/html/select/Elements.java index bae85f1..45765ae 100644 --- a/zhttp/src/main/java/com/zpj/http/parser/html/select/Elements.java +++ b/zhttp/src/main/java/com/zpj/http/parser/html/select/Elements.java @@ -1,5 +1,6 @@ package com.zpj.http.parser.html.select; +import com.zpj.http.parser.html.nodes.NullElement; import com.zpj.http.utils.StringUtil; import com.zpj.http.utils.Validate; import com.zpj.http.parser.html.nodes.Element; @@ -640,4 +641,12 @@ public List forms() { return forms; } + @Override + public Element get(int index) { + if (index >= size() || index < 0) { + return new NullElement(); + } + return super.get(index); + } + } diff --git a/zhttp/src/main/java/com/zpj/http/utils/W3CDom.java b/zhttp/src/main/java/com/zpj/http/utils/W3CDom.java index e777e52..a5a50ca 100644 --- a/zhttp/src/main/java/com/zpj/http/utils/W3CDom.java +++ b/zhttp/src/main/java/com/zpj/http/utils/W3CDom.java @@ -1,4 +1,4 @@ -//package com.zpj.http.helper; +package com.zpj.http.utils;//package com.zpj.http.helper; // //import com.zpj.http.utils.StringUtil; //import com.zpj.http.parser.html.nodes.Attribute;