diff --git a/.gitignore b/.gitignore deleted file mode 100755 index 473209c5a3a7825d3445f439231a16f2f884fbbd..0000000000000000000000000000000000000000 --- a/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea -.DS_Store -/build -/captures -.externalNativeBuild -app/release diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..5e3adcb2d83c7f3d134b37b9cf31d8b0fe94dafe --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1-SNAPSHOT +ohos 实现文件传输功能 + diff --git a/README.OPENSOURCE b/README.OPENSOURCE new file mode 100644 index 0000000000000000000000000000000000000000..d38217aeff3564abb7411e0ff2c2b15582949e10 --- /dev/null +++ b/README.OPENSOURCE @@ -0,0 +1,20 @@ +[ + + { + + "Name": "FileTransfer", + + "License": "GNU GENERAL PUBLIC License", + + "License File": "FileTransfer/README.md", + + "Version Number": "Master", + + "Upstream URL": "https://github.com/CPPAlien/FileTransfer", + + "Description": " web端与app端文件传输 " + + + } + +] \ No newline at end of file diff --git a/README.md b/README.md index 756c4b3d0a9cbefe713822323db2181d8166e249..fe5d01aadc1e323f18a097dc0a1a694ffb4e1998 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,77 @@ # FileTransfer -![](https://github.com/CPPAlien/FileTransfer/raw/master/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png) -Transfer any files from PC to your phone easily. +#### 项目介绍 -https://www.youtube.com/watch?v=NUNaORa1YzM +- 项目名称:FileTransfer +- 所属系列:openharmony的第三方组件适配移植 +- 功能:web端与app端文件传输 +- 项目移植状态:完成 +- 调用差异:因网络库限制 暂不支持断点续传功能以及文件传输速度无法控制。 -[![](http://7xq276.com2.z0.glb.qiniucdn.com/google_play.png)](https://play.google.com/store/apps/details?id=me.pengtao.filetransfer) +#### 使用说明: -**If you are banned by GFW. You can watch the video or download apk by ways below:** +```java +Server server = AndServer.webServer(context) + .port(8080) + .timeout(10, TimeUnit.SECONDS) + .build(); -Video: https://v.qq.com/x/page/x0618rp8w9x.html +// 开启服务. +server.startup(); -APK: http://d.6short.com/transfer +... -### What Next -1, Two-way transfer between PC and Phone. +// 关闭服务 +server.shutdown(); +``` + +http 动态接口的示例如下: + +```java +@RestController +@RequestMapping(path = "/user") +public class UserController { + + @PostMapping(path = "/upload", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + String upload(@RequestParam(name = "avatar") MultipartFile file) throws IOException { + File localFile = ...FileUtils.createRandomFile(file); + + return localFile.getAbsolutePath(); + } + + @PostMapping(path = "/delete", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + String delete(@RequestParam(name = "name") @RequestBody String name) throws IOException { + File file = new File(MainAbilitySlice.path + "/" + name); + + return file; + } + + @RequestMapping( + path = "/fileList", + method = {RequestMethod.PUT, RequestMethod.POST, RequestMethod.GET}, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE)ResponseBody download(HttpRequest request, HttpResponse response) throws IOException { + + return new StreamBody(FileUtils.fileToinputstream(fileEntity)); + + } +} +``` +#### 测试信息 + +CodeCheck代码测试无异常 -2, Support plain text send. +CloudTest代码测试无异常 -### We Need you -If you have any ideas for this app, welcome to submit an issue. +火绒安全病毒安全检测通过 -If you want to add some features, welcome to pull request. +当前版本demo功能与原组件基本无差异 -## Thanks -The idea and some code of this app are from [WifiTransfer](https://github.com/baidusoso/WifiTransfer). -Thanks for [baidusoso](https://github.com/baidusoso). +#### 版本迭代 + +- v1.0.0 + +#### 版权和许可信息 -## LICENSE ``` Copyright (c) 2018 CPPAlien @@ -43,4 +87,3 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` - diff --git a/app/.gitignore b/annotation/.gitignore old mode 100755 new mode 100644 similarity index 100% rename from app/.gitignore rename to annotation/.gitignore diff --git a/annotation/build.gradle b/annotation/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..88b4e2c921ce2c1e8ae92434257b7a78457799f0 --- /dev/null +++ b/annotation/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'java-library' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) +} + +sourceCompatibility = "8" +targetCompatibility = "8" + +ohos { + compileSdkVersion = 5 +} diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/Addition.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/Addition.java new file mode 100644 index 0000000000000000000000000000000000000000..16430b9718eb794ed2fe6bd3c6f684b9ba29ff9a --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/Addition.java @@ -0,0 +1,79 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 2018/9/9. + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.SOURCE) +public @interface Addition { + + /** + * Alias for {@link #stringType()}. + */ + String[] value() default {}; + + /** + * The added value of the String type. + */ + String[] stringType() default {}; + + /** + * The added value of the boolean type. + */ + boolean[] booleanType() default {}; + + /** + * The added value of the int type. + */ + int[] intTypeType() default {}; + + /** + * The added value of the long type. + */ + long[] longType() default {}; + + /** + * The added value of the short type. + */ + short[] shortType() default {}; + + /** + * The added value of the float type. + */ + float[] floatType() default {}; + + /** + * The added value of the double type. + */ + double[] doubleType() default {}; + + /** + * The added value of the byte type. + */ + byte[] byteType() default {}; + + /** + * The added value of the char type. + */ + char[] charType() default {}; +} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/AppInfo.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/AppInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..fa9dffacb4d55c694273096ddb9fba195476e36c --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/AppInfo.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 4/11/20. + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.SOURCE) +public @interface AppInfo { + + /** + * Application Id. + */ + String value() default ""; + +} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/Config.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/Config.java new file mode 100644 index 0000000000000000000000000000000000000000..adb04ee4d09e2c4245ab750ac79cf51ae9247bdf --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/Config.java @@ -0,0 +1,48 @@ +/* + * Copyright © 2019 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 2018/9/9. + *
+ * @Config
+ * public class AppConfig implements WebConfig {
+ *
+ *     @Override
+ *     public void onConfig(Context context, Delegate delegate) {
+ *         Website website = ...;
+ *         delegate.addWebsite(website);
+ *
+ *         Multipart multipart = Multipart.newBuilder()...build();
+ *         delegate.setMultipart(multipart);
+ *     }
+ * }
+ * 
+ */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.SOURCE) +public @interface Config { + + /** + * Group name. + */ + String value() default "default"; +} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/Controller.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/Controller.java new file mode 100644 index 0000000000000000000000000000000000000000..52cec7ef9e97d13f04df5fc36ac1f3a06e5ad567 --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/Controller.java @@ -0,0 +1,34 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 2018/6/3. + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.SOURCE) +public @interface Controller { + + /** + * Group name. + */ + String value() default "default"; +} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/Converter.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/Converter.java new file mode 100644 index 0000000000000000000000000000000000000000..13b0e06d8294ba2a96b65386aeb7b26f653febe8 --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/Converter.java @@ -0,0 +1,34 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 2018/9/11. + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.SOURCE) +public @interface Converter { + + /** + * Group name. + */ + String value() default "default"; +} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/CookieValue.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/CookieValue.java new file mode 100644 index 0000000000000000000000000000000000000000..c61278d3f7a168de3c3ed16ec8549c671a2b027f --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/CookieValue.java @@ -0,0 +1,56 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 2018/6/3. + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.SOURCE) +public @interface CookieValue { + + /** + * Alias for {@link #name()}. + */ + String value() default ""; + + /** + * The name of the cookie to bind to. + */ + String name() default ""; + + /** + * Whether the cookie is required. + * + *

Defaults to {@code true}, leading to an exception being thrown if the cookie is missing in the request. Switch + * this to {@code false} if you prefer a {@code null} value if the cookie is not present in the request. + * + *

Alternatively, provide a {@link #defaultValue()}, which implicitly sets this flag to {@code false}. + */ + boolean required() default true; + + /** + * The default value to use as a fallback. + * + *

Supplying a default value implicitly sets {@link #required()} to {@code false}. + */ + String defaultValue() default ""; +} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/CrossOrigin.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/CrossOrigin.java new file mode 100644 index 0000000000000000000000000000000000000000..c7acfadb503a6a4796e9ab28f225a892cc8f1550 --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/CrossOrigin.java @@ -0,0 +1,105 @@ +/* + * Copyright 2020 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 10/10/20. + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.SOURCE) +public @interface CrossOrigin { + + /** + * Alias for {@link #origins()}. + */ + String[] value() default {}; + + /** + * List of allowed origins, e.g. {@code "http://domain1.com"}. + * + *

These values are placed in the {@code Access-Control-Allow-Origin} + * header of both the pre-flight response and the actual response. + * {@code "*"} means that all origins are allowed. + * + *

If undefined, all origins are allowed. + * + * @see #value() + */ + String[] origins() default {}; + + /** + * List of request headers that can be used during the actual request. + * + *

This property controls the value of the pre-flight response's + * {@code Access-Control-Allow-Headers} header. + * {@code "*"} means that all headers requested by the client are allowed. + * + *

If undefined, all requested headers are allowed. + */ + String[] allowedHeaders() default {}; + + /** + * List of response headers that the user-agent will allow the client to access. + * + *

This property controls the value of actual response's + * {@code Access-Control-Expose-Headers} header. + * + *

If undefined, an empty exposed header list is used. + */ + String[] exposedHeaders() default {}; + + /** + * List of supported HTTP request methods, e.g. + * {@code "{RequestMethod.GET, RequestMethod.POST}"}. + * + *

Methods specified here override those specified via {@code RequestMapping}. + * + *

If undefined, methods defined by {@link RequestMapping} annotation are used. + */ + RequestMethod[] methods() default {}; + + /** + * Whether the browser should include any cookies associated with the + * domain of the request being annotated. + * + *

Set to {@code "false"} if such cookies should not included. + * An empty string ({@code ""}) means undefined. + * {@code "true"} means that the pre-flight response will include the header + * {@code Access-Control-Allow-Credentials=true}. + * + *

If undefined, credentials are allowed. + */ + String allowCredentials() default ""; + + /** + * The maximum age (in seconds) of the cache duration for pre-flight responses. + * + *

This property controls the value of the {@code Access-Control-Max-Age} + * header in the pre-flight response. + * + *

Setting this to a reasonable value can reduce the number of pre-flight + * request/response interactions required by the browser. + * A negative value means undefined. + * + *

If undefined, max age is set to {@code 1800} seconds (i.e., 30 minutes). + */ + long maxAge() default -1; +} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/DeleteMapping.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/DeleteMapping.java new file mode 100644 index 0000000000000000000000000000000000000000..a1d878498bc719fe709557a259fed42382c986cc --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/DeleteMapping.java @@ -0,0 +1,59 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 2018/6/4. + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.SOURCE) +public @interface DeleteMapping { + + /** + * Alias for {@link RequestMapping#value()}. + */ + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path()}. + */ + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params()}. + */ + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers()}. + */ + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes()}. + */ + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces()}. + */ + String[] produces() default {}; +} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/FormPart.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/FormPart.java new file mode 100644 index 0000000000000000000000000000000000000000..9c32976ba13fccd05358d7e60181d2103f44db2b --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/FormPart.java @@ -0,0 +1,37 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +/** + * Created by Zhenjie Yan on 2018/9/13. + */ +public @interface FormPart { + + /** + * Alias for {@link #name()}. + */ + String value() default ""; + + /** + * The name of the request parameter to bind to. + */ + String name() default ""; + + /** + * Whether the parameter is required. + */ + boolean required() default true; +} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/GetMapping.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/GetMapping.java new file mode 100644 index 0000000000000000000000000000000000000000..441b0baa1e7c9c262727512638178582d1fb4e9e --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/GetMapping.java @@ -0,0 +1,59 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 2018/6/4. + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.SOURCE) +public @interface GetMapping { + + /** + * Alias for {@link RequestMapping#value()}. + */ + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path()}. + */ + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params()}. + */ + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers()}. + */ + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes()}. + */ + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces()}. + */ + String[] produces() default {}; +} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/Interceptor.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/Interceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..4c3834f216b07edb6896e200e5747510c381ad3a --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/Interceptor.java @@ -0,0 +1,34 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 2018/9/11. + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.SOURCE) +public @interface Interceptor { + + /** + * Group name. + */ + String value() default "default"; +} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/PatchMapping.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/PatchMapping.java new file mode 100644 index 0000000000000000000000000000000000000000..ed2fbe50717f062beac41a3117eb3f0ec14d742c --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/PatchMapping.java @@ -0,0 +1,59 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 2018/6/4. + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.SOURCE) +public @interface PatchMapping { + + /** + * Alias for {@link RequestMapping#value()}. + */ + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path()}. + */ + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params()}. + */ + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers()}. + */ + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes()}. + */ + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces()}. + */ + String[] produces() default {}; +} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/PathVariable.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/PathVariable.java new file mode 100644 index 0000000000000000000000000000000000000000..ae1930780f48d80cb455d4ccd9ba5aee205998fd --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/PathVariable.java @@ -0,0 +1,55 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 2018/6/3. + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.SOURCE) +public @interface PathVariable { + + /** + * Alias for {@link #name()}. + */ + String value() default ""; + + /** + * The name of the path variable to bind to. + */ + String name() default ""; + + /** + * Whether the path is required. + * + *

Defaults to {@code true}, leading to an exception being thrown if the path is missing in the request. + * + *

Alternatively, provide a {@link #defaultValue()}, which implicitly sets this flag to {@code false}. + */ + boolean required() default true; + + /** + * The default value to use as a fallback. + * + *

Supplying a default value implicitly sets {@link #required()} to {@code false}. + */ + String defaultValue() default ""; +} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/PostMapping.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/PostMapping.java new file mode 100644 index 0000000000000000000000000000000000000000..01257d58f3eb1767c0122ed7a20b497a4cb36439 --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/PostMapping.java @@ -0,0 +1,59 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 2018/6/4. + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.SOURCE) +public @interface PostMapping { + + /** + * Alias for {@link RequestMapping#value()}. + */ + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path()}. + */ + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params()}. + */ + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers()}. + */ + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes()}. + */ + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces()}. + */ + String[] produces() default {}; +} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/PutMapping.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/PutMapping.java new file mode 100644 index 0000000000000000000000000000000000000000..6bebf6da6e63e16078cc8af88dea92013fdaa43b --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/PutMapping.java @@ -0,0 +1,59 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 2018/6/4. + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.SOURCE) +public @interface PutMapping { + + /** + * Alias for {@link RequestMapping#value()}. + */ + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path()}. + */ + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params()}. + */ + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers()}. + */ + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes()}. + */ + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces()}. + */ + String[] produces() default {}; +} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/QueryParam.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/QueryParam.java new file mode 100644 index 0000000000000000000000000000000000000000..fb1d955912c29022fefc684c1975390d563c2eed --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/QueryParam.java @@ -0,0 +1,55 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 2018/9/13. + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.SOURCE) +public @interface QueryParam { + + /** + * Alias for {@link #name()}. + */ + String value() default ""; + + /** + * The name of the request parameter to bind to. + */ + String name() default ""; + + /** + * Whether the parameter is required. + * + *

Defaults to {@code true}, leading to an exception being thrown if the parameter is missing in the request. + * + *

Alternatively, provide a {@link #defaultValue()}, which implicitly sets this flag to {@code false}. + */ + boolean required() default true; + + /** + * The default value to use as a fallback. + * + *

Supplying a default value implicitly sets {@link #required()} to {@code false}. + */ + String defaultValue() default ""; +} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/RequestBody.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/RequestBody.java new file mode 100644 index 0000000000000000000000000000000000000000..2353ce056638519c0ac4474eda3dbea9f5d2f812 --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/RequestBody.java @@ -0,0 +1,36 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 2018/6/3. + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.SOURCE) +public @interface RequestBody { + + /** + * Whether body content is required. + * + *

Default is {@code true}, leading to an exception thrown in case there is no body content. + */ + boolean required() default true; +} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/RequestHeader.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/RequestHeader.java new file mode 100644 index 0000000000000000000000000000000000000000..a02df95c8714ee68800dff0919687463bd2d11f7 --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/RequestHeader.java @@ -0,0 +1,55 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 2018/6/3. + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.SOURCE) +public @interface RequestHeader { + + /** + * Alias for {@link #name()}. + */ + String value() default ""; + + /** + * The name of the request header to bind to. + */ + String name() default ""; + + /** + * Whether the header is required. + * + *

Defaults to {@code true}, leading to an exception being thrown if the header is missing in the request. + * + *

Alternatively, provide a {@link #defaultValue()}, which implicitly sets this flag to {@code false}. + */ + boolean required() default true; + + /** + * The default value to use as a fallback. + * + *

Supplying a default value implicitly sets {@link #required()} to {@code false}. + */ + String defaultValue() default ""; +} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/RequestMapping.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/RequestMapping.java new file mode 100644 index 0000000000000000000000000000000000000000..967ad9dec118f7f51c95ff9da77144dbe7812a7b --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/RequestMapping.java @@ -0,0 +1,125 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 2018/6/3. + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.SOURCE) +public @interface RequestMapping { + + /** + * Alias for {@link #path()}. + */ + String[] value() default {}; + + /** + * The primary mapping expressed by this annotation. For example {@code @RequestMapping ("/foo")} is equivalent to + * {@code @RequestMapping (path="/foo")}. + * + *

Supported at the type level as well as at the method level. When used at the type level, all + * method-level mappings inherit this primary mapping, narrowing it for a specific handler method. + */ + String[] path() default {}; + + /** + * The HTTP request methods to map to, narrowing the primary mapping: GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, + * TRACE. + * + *

Supported at the type level as well as at the method level. When used at the type level, all + * method-level mappings inherit this HTTP method restriction (i.e. the type-level restriction gets checked before the + * handler method is even resolved). + */ + RequestMethod[] method() default {}; + + /** + * The parameters of the mapped request, narrowing the primary mapping. + * + *

A sequence of "myParam=myValue" style expressions, with a request only mapped if each such parameter is found + * to have the given value. Expressions can be negated by using the "!=" operator, as in "myParam!=myValue". "myParam" + * style expressions are also supported, with such parameters having to be present in the request (allowed to have any + * value). Finally, "!myParam" style expressions indicate that the specified parameter is not supposed to be present in + * the request. + * + *

Supported at the type level as well as at the method level. When used at the type level, all + * method-level mappings inherit this parameter restriction (i.e. the type-level restriction gets checked before the + * handler method is even resolved). + */ + String[] params() default {}; + + /** + * The headers of the mapped request, narrowing the primary mapping. + * + *

A sequence of "My-Header=myValue" style expressions, with a request only mapped if each such header is found + * to have the given value. Expressions can be negated by using the "!=" operator, as in "My-Header!=myValue". + * "My-Header" style expressions are also supported, with such headers having to be present in the request (allowed to + * have any value). Finally, "!My-Header" style expressions indicate that the specified header is not supposed to + * be present in the request. + * + *

Also supports media type wildcards (*), for headers such as Accept and Content-Type. For instance, + * + *

 @RequestMapping(value = "/something", headers = "content-type=text/*") 
+ * + * will match requests with a Content-Type of "text/html", "text/plain", etc. + * + *

Supported at the type level as well as at the method level. When used at the type level, all + * method-level mappings inherit this header restriction (i .e. the type-level restriction gets checked before the + * handler method is even resolved). + */ + String[] headers() default {}; + + /** + * The consumable media types of the mapped request, narrowing the primary mapping. + * + *

The format is a single media type or a sequence of media types, with a request only mapped if the {@code + * Content-Type} matches one of these media types. Examples: + * + *

 consumes = "text/plain" consumes = {"text/plain", "application/*"} 
+ * + * Expressions can be negated by using the "!" operator, as in "!text/plain", which matches all requests with a {@code + * Content-Type} other than "text/plain". + * + *

Supported at the type level as well as at the method level. When used at the type level, all + * method-level mappings override this consumes restriction. + */ + String[] consumes() default {}; + + /** + * The producible media types of the mapped request, narrowing the primary mapping. + * + *

The format is a single media type or a sequence of media types, with a request only mapped if the {@code + * Accept} matches one of these media types. Examples: + * + *

 produces = "text/plain" produces = {"text/plain", "application/*"} produces =
+     * "application/json; charset=UTF-8" 
+ * + *

It affects the actual content type written, for example to produce a JSON response with UTF-8 encoding, {@code + * "application/json; charset=UTF-8"} should be used. + * + *

Expressions can be negated by using the "!" operator, as in "!text/plain", which matches all requests with a + * {@code Accept} other than "text/plain". + * + *

Supported at the type level as well as at the method level. When used at the type level, all + * method-level mappings override this produces restriction. + */ + String[] produces() default {}; +} \ No newline at end of file diff --git a/app/src/main/java/me/pengtao/filetransfer/receiver/PackageStateReceiver.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/RequestMethod.java similarity index 43% rename from app/src/main/java/me/pengtao/filetransfer/receiver/PackageStateReceiver.java rename to annotation/src/main/java/com/yanzhenjie/andserver/annotation/RequestMethod.java index d8913f40a3b3ce9723bcf60d0baade725fff98db..dee8d42e1418d31b04b8030e2ead7f841dab1c2b 100644 --- a/app/src/main/java/me/pengtao/filetransfer/receiver/PackageStateReceiver.java +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/RequestMethod.java @@ -1,11 +1,11 @@ /* - * Copyright (c) 2018 CPPAlien + * Copyright 2018 Zhenjie Yan. * - * Licensed under the GNU GENERAL PUBLIC LICENSE, Version 3, 29 June 2007 (the "License"); + * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://github.com/CPPAlien/FileTransfer/blob/master/LICENSE + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,22 +13,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package me.pengtao.filetransfer.receiver; +package com.yanzhenjie.andserver.annotation; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; +/** + * Created by Zhenjie Yan on 2018/6/3. + */ +public enum RequestMethod { + GET("GET"), + HEAD("HEAD"), + POST("POST"), + PUT("PUT"), + PATCH("PATCH"), + DELETE("DELETE"), + OPTIONS("OPTIONS"), + TRACE("TRACE"); -import com.hwangjr.rxbus.RxBus; + private String value; -import me.pengtao.filetransfer.Constants; + RequestMethod(String value) { + this.value = value; + } + + public String value() { + return value; + } -/** - * @author CPPAlien - */ -public class PackageStateReceiver extends BroadcastReceiver { @Override - public void onReceive(Context context, Intent intent) { - RxBus.get().post(Constants.RxBusEventType.LOAD_BOOK_LIST, 0); + public String toString() { + return value; } } \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/RequestParam.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/RequestParam.java new file mode 100644 index 0000000000000000000000000000000000000000..b4f55ebfd8d9942a0e641a2b5f6fa2954eb014ef --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/RequestParam.java @@ -0,0 +1,55 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 2018/6/3. + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.SOURCE) +public @interface RequestParam { + + /** + * Alias for {@link #name()}. + */ + String value() default ""; + + /** + * The name of the request parameter to bind to. + */ + String name() default ""; + + /** + * Whether the parameter is required. + * + *

Defaults to {@code true}, leading to an exception being thrown if the parameter is missing in the request. + * + *

Alternatively, provide a {@link #defaultValue()}, which implicitly sets this flag to {@code false}. + */ + boolean required() default true; + + /** + * The default value to use as a fallback. + * + *

Supplying a default value implicitly sets {@link #required()} to {@code false}. + */ + String defaultValue() default ""; +} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/Resolver.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/Resolver.java new file mode 100644 index 0000000000000000000000000000000000000000..2a91a118045663d58735f2aef1606db00acceda5 --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/Resolver.java @@ -0,0 +1,34 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 2018/9/11. + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.SOURCE) +public @interface Resolver { + + /** + * Group name. + */ + String value() default "default"; +} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/ResponseBody.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/ResponseBody.java new file mode 100644 index 0000000000000000000000000000000000000000..1daf53240388cb5109bfbbbd9ad8bf01d45ada55 --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/ResponseBody.java @@ -0,0 +1,28 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 2018/6/3. + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.SOURCE) +public @interface ResponseBody {} \ No newline at end of file diff --git a/annotation/src/main/java/com/yanzhenjie/andserver/annotation/RestController.java b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/RestController.java new file mode 100644 index 0000000000000000000000000000000000000000..48daff90352332f4ca6866bf39bd8bb8966933b2 --- /dev/null +++ b/annotation/src/main/java/com/yanzhenjie/andserver/annotation/RestController.java @@ -0,0 +1,34 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Zhenjie Yan on 2018/6/3. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface RestController { + + /** + * Group name. + */ + String value() default "default"; +} \ No newline at end of file diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..796b96d1c402326528b4ba3c12ee9d92d0e212e9 --- /dev/null +++ b/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/api/build.gradle b/api/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..4f80da35d252c89478ad5ff21b539769a970293e --- /dev/null +++ b/api/build.gradle @@ -0,0 +1,16 @@ +apply plugin: 'com.huawei.ohos.library' +ohos { + compileSdkVersion 5 + defaultConfig { + compatibleSdkVersion 5 + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + testCompile 'junit:junit:4.12' + api project(':annotation') + api 'com.yanzhenjie.apache:fileupload:1.4' + api 'com.yanzhenjie.apache:httpcore:4.4.13.2' + // implementation 'androidx.annotation:annotation:1.1.0' +} diff --git a/api/src/main/config.json b/api/src/main/config.json new file mode 100644 index 0000000000000000000000000000000000000000..e271453c136cb6b5056ffd31cd6ba3b9c0c2656e --- /dev/null +++ b/api/src/main/config.json @@ -0,0 +1,32 @@ +{ + "app": { + "bundleName": "com.yanzhenjie.andserver", + "vendor": "yanzhenjie", + "version": { + "code": 1, + "name": "1.0.1" + }, + "apiVersion": { + "compatible": 5, + "target": 5, + "releaseType": "Beta1" + } + }, + "deviceConfig": {}, + "module": { + "package": "com.yanzhenjie.andserver", + "deviceType": [ + "phone" + ], + "distro": { + "deliveryWithInstall": true, + "moduleName": "api", + "moduleType": "har" + }, + "reqPermissions": [ + { + "name": "ohos.permission.INTERNET" + } + ] + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/AndServer.java b/api/src/main/java/com/yanzhenjie/andserver/AndServer.java new file mode 100644 index 0000000000000000000000000000000000000000..5a1551796bdede1a959726c002cf0ee0016a2dd4 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/AndServer.java @@ -0,0 +1,81 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver; + +import com.yanzhenjie.andserver.server.ProxyServer; +import com.yanzhenjie.andserver.server.WebServer; +import ohos.agp.render.render3d.BuildConfig; +import ohos.app.Context; + +/** + * Created by Zhenjie Yan on 2018/6/9. + */ +public class AndServer { + + public static final String TAG = "AndServer"; + public static final String INFO = String.format("AndServer/%1$s", BuildConfig.VERSION_CODE); + + + /** + * Create a builder for the web server. + * + * @return {@link Server.Builder}. + */ + public static Server.Builder webServer(Context context) { + return WebServer.newBuilder(context, "default"); + } + + /** + * Create a builder for the web server. + * + * @param group group name. + * + * @return {@link Server.Builder}. + */ + + public static Server.Builder webServer( Context context, + String group) { + return WebServer.newBuilder(context, group); + } + + /** + * Create a builder for the reverse proxy server. + * + * @return {@link Server.ProxyBuilder}. + */ + + public static Server.ProxyBuilder proxyServer() { + return ProxyServer.newBuilder(); + } + + /** + * @deprecated use {@link #webServer(Context)} instead. + */ + + @Deprecated + public static Server.Builder serverBuilder( Context context) { + return webServer(context); + } + + /** + * @deprecated use {@link #webServer(Context, String)} instead. + */ + + @Deprecated + public static Server.Builder serverBuilder( Context context, String group) { + return webServer(context, group); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/ComponentRegister.java b/api/src/main/java/com/yanzhenjie/andserver/ComponentRegister.java new file mode 100644 index 0000000000000000000000000000000000000000..435a6eefd860668c82cb1747484bf2f6b89e7b2d --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/ComponentRegister.java @@ -0,0 +1,83 @@ +/* + * Copyright 2018 Zhenjie Yan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver; + +import com.yanzhenjie.andserver.register.OnRegister; +import com.yanzhenjie.andserver.register.Register; +import ohos.app.Context; +import ohos.global.resource.Entry; +import ohos.global.resource.ResourceManager; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Zhenjie Yan on 2018/9/23. + */ +public class ComponentRegister { + + private static final String ANDSERVER_REGISTER_SUFFIX = ".andserver"; + private static final String PROCESSOR_PACKAGE = ".andserver.processor.generator."; + private static final List REGISTER_LIST = new ArrayList<>(); + + static { + REGISTER_LIST.add("AdapterRegister"); + REGISTER_LIST.add("ConfigRegister"); + REGISTER_LIST.add("ConverterRegister"); + REGISTER_LIST.add("InterceptorRegister"); + REGISTER_LIST.add("ResolverRegister"); + } + + private Context mContext; + + public ComponentRegister(Context context) { + this.mContext = context; + } + + public void register(Register register, String group) + throws InstantiationException, IllegalAccessException { + ResourceManager manager = mContext.getResourceManager(); + Entry[] pathList = null; + try { + pathList = manager.getRawFileEntry("").getEntries(); + } catch (IOException e) { + e.printStackTrace(); + } + + if (pathList == null || pathList.length == 0) { + return; + } + + String packageName = "com.yanzhenjie.andserver.hos"; + for (String clazz : REGISTER_LIST) { + String className = String.format("%s%s%s", packageName, PROCESSOR_PACKAGE, clazz); + registerClass(register, group, className); + } + } + + private void registerClass(Register register, String group, String className) + throws InstantiationException, IllegalAccessException { + try { + Class clazz = Class.forName(className); + if (OnRegister.class.isAssignableFrom(clazz)) { + OnRegister load = (OnRegister) clazz.newInstance(); + load.onRegister(mContext, group, register); + } + } catch (ClassNotFoundException ignored) { + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/DispatcherHandler.java b/api/src/main/java/com/yanzhenjie/andserver/DispatcherHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..b337ad96b66e25bc648166f2536971ee913ce0b3 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/DispatcherHandler.java @@ -0,0 +1,262 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver; + +import com.yanzhenjie.andserver.error.NotFoundException; +import com.yanzhenjie.andserver.error.ServerInternalException; +import com.yanzhenjie.andserver.framework.ExceptionResolver; +import com.yanzhenjie.andserver.framework.HandlerInterceptor; +import com.yanzhenjie.andserver.framework.MessageConverter; +import com.yanzhenjie.andserver.framework.ModifiedInterceptor; +import com.yanzhenjie.andserver.framework.body.StringBody; +import com.yanzhenjie.andserver.framework.config.Multipart; +import com.yanzhenjie.andserver.framework.handler.HandlerAdapter; +import com.yanzhenjie.andserver.framework.handler.RequestHandler; +import com.yanzhenjie.andserver.framework.view.View; +import com.yanzhenjie.andserver.framework.view.ViewResolver; +import com.yanzhenjie.andserver.http.*; +import com.yanzhenjie.andserver.http.cookie.Cookie; +import com.yanzhenjie.andserver.http.multipart.MultipartRequest; +import com.yanzhenjie.andserver.http.multipart.MultipartResolver; +import com.yanzhenjie.andserver.http.multipart.StandardMultipartResolver; +import com.yanzhenjie.andserver.http.session.Session; +import com.yanzhenjie.andserver.http.session.SessionManager; +import com.yanzhenjie.andserver.http.session.StandardSessionManager; +import com.yanzhenjie.andserver.register.Register; +import com.yanzhenjie.andserver.util.Assert; +import ohos.app.Context; +import ohos.hiviewdfx.HiLog; +import ohos.hiviewdfx.HiLogLabel; +import org.apache.httpcore.protocol.HttpRequestHandler; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +/** + * Created by Zhenjie Yan on 2018/8/8. + */ +public class DispatcherHandler implements HttpRequestHandler, Register { + + private final Context mContext; + + private SessionManager mSessionManager; + private MessageConverter mConverter; + private ViewResolver mViewResolver; + private ExceptionResolver mResolver; + private Multipart mMultipart; + + private List mAdapterList = new LinkedList<>(); + private List mInterceptorList = new LinkedList<>(); + + public DispatcherHandler(Context context) { + this.mContext = context; + this.mSessionManager = new StandardSessionManager(context); + this.mViewResolver = new ViewResolver(); + this.mResolver = new ExceptionResolver.ResolverWrapper(ExceptionResolver.DEFAULT); + this.mInterceptorList.add(new ModifiedInterceptor()); + } + + @Override + public void addAdapter(HandlerAdapter adapter) { + Assert.notNull(adapter, "The adapter cannot be null."); + + if (!mAdapterList.contains(adapter)) { + mAdapterList.add(adapter); + } + } + + @Override + public void addInterceptor( HandlerInterceptor interceptor) { + Assert.notNull(interceptor, "The interceptor cannot be null."); + + if (!mInterceptorList.contains(interceptor)) { + mInterceptorList.add(interceptor); + } + } + + @Override + public void setConverter(MessageConverter converter) { + this.mConverter = converter; + this.mViewResolver = new ViewResolver(converter); + } + + @Override + public void setResolver( ExceptionResolver resolver) { + Assert.notNull(resolver, "The exceptionResolver cannot be null."); + this.mResolver = new ExceptionResolver.ResolverWrapper(resolver); + } + + @Override + public void setMultipart(Multipart multipart) { + this.mMultipart = multipart; + } + + @Override + public void handle(org.apache.httpcore.HttpRequest req, org.apache.httpcore.HttpResponse res, + org.apache.httpcore.protocol.HttpContext con) { + HttpRequest request = new StandardRequest(req, new StandardContext(con), this, mSessionManager); + HttpResponse response = new StandardResponse(res); + handle(request, response); + } + + private void handle(HttpRequest request, HttpResponse response) { + MultipartResolver multipartResolver = new StandardMultipartResolver(); + try { + if (multipartResolver.isMultipart(request)) { + configMultipart(multipartResolver); + request = multipartResolver.resolveMultipart(request); + } + + // Determine adapter for the current request. + HandlerAdapter ha = getHandlerAdapter(request); + if (ha == null) { + throw new NotFoundException(request.getPath()); + } + + // Determine handler for the current request. + RequestHandler handler = ha.getHandler(request); + if (handler == null) { + throw new NotFoundException(request.getPath()); + } + + // Pre processor, e.g. interceptor. + if (preHandle(request, response, handler)) { + return; + } + + // Actually invoke the handler. + request.setAttribute(HttpContext.ANDROID_CONTEXT, mContext); + request.setAttribute(HttpContext.HTTP_MESSAGE_CONVERTER, mConverter); + View view = handler.handle(request, response); + mViewResolver.resolve(view, request, response); + processSession(request, response); + } catch (Throwable err) { + try { + mResolver.onResolve(request, response, err); + } catch (Exception e) { + e = new ServerInternalException(e); + response.setStatus(StatusCode.SC_INTERNAL_SERVER_ERROR); + response.setBody(new StringBody(e.getMessage())); + } + processSession(request, response); + } finally { + if (request instanceof MultipartRequest) { + multipartResolver.cleanupMultipart((MultipartRequest) request); + } + } + } + + private void configMultipart(MultipartResolver multipartResolver) { + if (mMultipart != null) { + long allFileMaxSize = mMultipart.getAllFileMaxSize(); + if (allFileMaxSize == -1 || allFileMaxSize > 0) { + multipartResolver.setAllFileMaxSize(allFileMaxSize); + } + + long fileMaxSize = mMultipart.getFileMaxSize(); + if (fileMaxSize == -1 || fileMaxSize > 0) { + multipartResolver.setFileMaxSize(fileMaxSize); + } + + int maxInMemorySize = mMultipart.getMaxInMemorySize(); + if (maxInMemorySize > 0) { + multipartResolver.setMaxInMemorySize(maxInMemorySize); + } + + File uploadTempDir = mMultipart.getUploadTempDir(); + if (uploadTempDir != null) { + multipartResolver.setUploadTempDir(uploadTempDir); + } + } + } + + /** + * Return the {@link RequestHandler} for this request. + * + * @param request current HTTP request. + * + * @return the {@link RequestHandler}, or {@code null} if no handler could be found. + */ + private HandlerAdapter getHandlerAdapter(HttpRequest request) { + for (HandlerAdapter ha: mAdapterList) { + if (ha.intercept(request)) { + return ha; + } + } + return null; + } + + /** + * Intercept the execution of a handler. + * + * @param request current request. + * @param response current response. + * @param handler the corresponding handler of the current request. + * + * @return true if the interceptor has processed the request and responded. + */ + private boolean preHandle(HttpRequest request, HttpResponse response, RequestHandler handler) throws Exception { + for (HandlerInterceptor interceptor: mInterceptorList) { + if (interceptor.onIntercept(request, response, handler)) { + return true; + } + } + return false; + } + + + public RequestDispatcher getRequestDispatcher(final HttpRequest request, final String path) { + HttpRequest copyRequest = request; + while (copyRequest instanceof RequestWrapper) { + RequestWrapper wrapper = (RequestWrapper) request; + copyRequest = wrapper.getRequest(); + } + + StandardRequest newRequest = (StandardRequest) copyRequest; + newRequest.setPath(path); + + HandlerAdapter ha = getHandlerAdapter(copyRequest); + if (ha == null) { + throw new NotFoundException(request.getPath()); + } + + return new RequestDispatcher() { + @Override + public void forward( HttpRequest request, HttpResponse response) { + handle(request, response); + } + }; + } + + private void processSession(HttpRequest request, HttpResponse response) { + Object objSession = request.getAttribute(HttpContext.REQUEST_CREATED_SESSION); + if (objSession instanceof Session) { + Session session = (Session) objSession; + try { + mSessionManager.add(session); + } catch (IOException e) { + HiLog.error(new HiLogLabel(0,1, AndServer.TAG), "Session persistence failed.", e); + } + + Cookie cookie = new Cookie(HttpRequest.SESSION_NAME, session.getId()); + cookie.setPath("/"); + cookie.setHttpOnly(true); + response.addCookie(cookie); + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/ProxyHandler.java b/api/src/main/java/com/yanzhenjie/andserver/ProxyHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..4236432ec8ea1df602d2d5ee5389346cf6204a51 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/ProxyHandler.java @@ -0,0 +1,160 @@ +/* + * Copyright 2020 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver; + +import com.yanzhenjie.andserver.error.NotFoundException; +import com.yanzhenjie.andserver.server.ProxyServer; +import com.yanzhenjie.andserver.util.IOUtils; +import org.apache.httpcore.*; +import org.apache.httpcore.entity.StringEntity; +import org.apache.httpcore.impl.DefaultBHttpClientConnection; +import org.apache.httpcore.impl.DefaultConnectionReuseStrategy; +import org.apache.httpcore.protocol.*; + +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.*; + +import static com.yanzhenjie.andserver.server.ProxyServer.PROXY_CONN_CLIENT; + +/** + * Created by Zhenjie Yan on 3/7/20. + */ +public class ProxyHandler implements HttpRequestHandler { + + private static final int BUFFER = 8 * 1024; + + private final static Set HOP_BY_HOP = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + HttpHeaders.HOST, + HttpHeaders.CONTENT_LENGTH, + HttpHeaders.TRANSFER_ENCODING, + HttpHeaders.CONNECTION, + HttpHeaders.PROXY_AUTHENTICATE, + HttpHeaders.TE, + HttpHeaders.TRAILER, + HttpHeaders.UPGRADE + ))); + + private final Map mHostList; + + private final SSLSocketFactory mSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + + private final HttpRequestExecutor mHttpExecutor = new HttpRequestExecutor(); + private final HttpProcessor mRequestProcessor = new ImmutableHttpProcessor( + new RequestContent(), + new RequestTargetHost(), + new RequestConnControl(), + new RequestUserAgent(AndServer.INFO), + new RequestExpectContinue(true)); + + public ProxyHandler(Map hostList) { + this.mHostList = hostList; + } + + @Override + public void handle(HttpRequest request, HttpResponse response, HttpContext context) + throws HttpException, IOException { + String hostHeader = request.getFirstHeader(HttpHeaders.HOST).getValue(); + String hostName = HttpHost.create(hostHeader).getHostName(); + HttpHost host = mHostList.get(hostName.toLowerCase(Locale.ROOT)); + if (host == null) { + NotFoundException e = new NotFoundException(request.getRequestLine().getUri()); + response.setStatusCode(e.getStatusCode()); + response.setEntity(new StringEntity(e.getMessage())); + return; + } + + // Remove hop-by-hop headers. + for (String name: HOP_BY_HOP) { + request.removeHeaders(name); + } + + DefaultBHttpClientConnection conn = (DefaultBHttpClientConnection) context.getAttribute(PROXY_CONN_CLIENT); + if (!conn.isOpen() || conn.isStale()) { + Socket socket = createSocket(host); + conn.bind(socket); + } + + context.setAttribute(HttpCoreContext.HTTP_CONNECTION, conn); + context.setAttribute(HttpCoreContext.HTTP_TARGET_HOST, host); + + mHttpExecutor.preProcess(request, mRequestProcessor, context); + HttpResponse outResponse = mHttpExecutor.execute(request, conn, context); + mHttpExecutor.postProcess(response, mRequestProcessor, context); + + // Remove hop-by-hop headers. + for (String name: HOP_BY_HOP) { + outResponse.removeHeaders(name); + } + + response.setStatusLine(outResponse.getStatusLine()); + response.setHeaders(outResponse.getAllHeaders()); + response.setEntity(outResponse.getEntity()); + + boolean keepAlive = DefaultConnectionReuseStrategy.INSTANCE.keepAlive(response, context); + context.setAttribute(ProxyServer.PROXY_CONN_ALIVE, keepAlive); + } + + private Socket createSocket(HttpHost host) throws IOException { + Socket socket = new Socket(); + socket.setSoTimeout(60 * 1000); + socket.setReuseAddress(true); + socket.setTcpNoDelay(true); + socket.setKeepAlive(true); + socket.setReceiveBufferSize(BUFFER); + socket.setSendBufferSize(BUFFER); + socket.setSoLinger(true, 0); + + String scheme = host.getSchemeName(); + String hostName = host.getHostName(); + int port = host.getPort(); + + InetSocketAddress address = resolveAddress(scheme, hostName, port); + socket.connect(address, 10 * 1000); + + if ("https".equalsIgnoreCase(scheme)) { + SSLSocket sslSocket = (SSLSocket) mSocketFactory.createSocket(socket, hostName, port, true); + try { + sslSocket.startHandshake(); + final SSLSession session = sslSocket.getSession(); + if (session == null) { + throw new SSLHandshakeException("SSL session not available."); + } + } catch (final IOException ex) { + IOUtils.closeQuietly(sslSocket); + throw ex; + } + return sslSocket; + } + return socket; + } + + private InetSocketAddress resolveAddress(String scheme, String hostName, int port) { + if (port < 0) { + if ("http".equalsIgnoreCase(scheme)) { + port = 80; + } else if ("https".equalsIgnoreCase(scheme)) { + port = 443; + } + } + return new InetSocketAddress(hostName, port); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/SSLSocketInitializer.java b/api/src/main/java/com/yanzhenjie/andserver/SSLSocketInitializer.java new file mode 100644 index 0000000000000000000000000000000000000000..3aea58c697d4a2afa6a1d8cc243196a00869643a --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/SSLSocketInitializer.java @@ -0,0 +1,29 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver; + + + +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLServerSocket; + +/** + * Created by Zhenjie Yan on 2018/9/10. + */ +public interface SSLSocketInitializer { + + void onCreated(SSLServerSocket socket) throws SSLException; +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/Server.java b/api/src/main/java/com/yanzhenjie/andserver/Server.java new file mode 100644 index 0000000000000000000000000000000000000000..b5fb156835d178650752b3787656e6b841065dfe --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/Server.java @@ -0,0 +1,178 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver; + +import javax.net.ServerSocketFactory; +import javax.net.ssl.SSLContext; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.TimeUnit; + +/** + * Created by Zhenjie Yan on 2018/9/10. + */ +public interface Server { + + /** + * Server running status. + * + * @return return true, not return false. + */ + boolean isRunning(); + + /** + * Start the server. + */ + void startup(); + + /** + * Quit the server. + */ + void shutdown(); + + /** + * Get the local address of this server socket. + * + * @return {@link InetAddress}. + * + * @throws IllegalStateException if the server is not started, an IllegalStateException is thrown. + * @see ServerSocket#getInetAddress() + */ + InetAddress getInetAddress(); + + /** + * Returns the port number on which this socket is listening. + * + * @return the local port number to which this socket is bound or -1 if the socket is not bound yet. + * + * @throws IllegalStateException if the server is not started, an IllegalStateException is thrown. + * @see Socket#getLocalPort() + */ + int getPort(); + + interface Builder { + + /** + * Specified server need to monitor the ip address. + */ + T inetAddress(InetAddress inetAddress); + + /** + * Specify the port on which the server listens. + */ + T port(int port); + + /** + * Connection and response timeout. + */ + T timeout(int timeout, TimeUnit timeUnit); + + /** + * Assigns {@link ServerSocketFactory} instance. + */ + T serverSocketFactory(ServerSocketFactory factory); + + /** + * Assigns {@link SSLContext} instance. + */ + T sslContext(SSLContext sslContext); + + /** + * Assigns {@link SSLSocketInitializer} instance. + */ + T sslSocketInitializer(SSLSocketInitializer initializer); + + /** + * Set the server listener. + */ + T listener(ServerListener listener); + + /** + * Create a server. + */ + S build(); + } + + interface ProxyBuilder { + + /** + * Add host address to proxy. + * + * @param hostName such as: {@code www.example.com}, {@code api.example.com}, {@code 192.168.1.111}. + * @param proxyHost such as: {@code http://127.0.0.1:8080}, {@code http://localhost:8181} + */ + T addProxy(String hostName, String proxyHost); + + /** + * Specified server need to monitor the ip address. + */ + T inetAddress(InetAddress inetAddress); + + /** + * Specify the port on which the server listens. + */ + T port(int port); + + /** + * Connection and response timeout. + */ + T timeout(int timeout, TimeUnit timeUnit); + + /** + * Assigns {@link ServerSocketFactory} instance. + */ + T serverSocketFactory(ServerSocketFactory factory); + + /** + * Assigns {@link SSLContext} instance. + */ + T sslContext(SSLContext sslContext); + + /** + * Assigns {@link SSLSocketInitializer} instance. + */ + T sslSocketInitializer(SSLSocketInitializer initializer); + + /** + * Set the server listener. + */ + T listener(ServerListener listener); + + /** + * Create a server. + */ + S build(); + } + + interface ServerListener { + + /** + * When the server is started. + */ + void onStarted(); + + /** + * When the server stops running. + */ + void onStopped(); + + /** + * An error occurred while starting the server. + */ + void onException(Exception e); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/error/BasicException.java b/api/src/main/java/com/yanzhenjie/andserver/error/BasicException.java new file mode 100644 index 0000000000000000000000000000000000000000..c716c3b70d77e91eb702f381cc271f02f6d6cb56 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/error/BasicException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.error; + +/** + * Created by Zhenjie Yan on 11/22/20. + * + * @deprecated use {@link HttpException} instead. + */ +@Deprecated +public class BasicException extends HttpException { + + public BasicException(int statusCode, String message) { + super(statusCode, message); + } + + public BasicException(int statusCode, String message, Throwable cause) { + super(statusCode, message, cause); + } + + public BasicException(int statusCode, Throwable cause) { + super(statusCode, cause); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/error/BodyMissingException.java b/api/src/main/java/com/yanzhenjie/andserver/error/BodyMissingException.java new file mode 100644 index 0000000000000000000000000000000000000000..80f633af53e86525446b026e4b9b246eaf4599ef --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/error/BodyMissingException.java @@ -0,0 +1,36 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.error; + + +import com.yanzhenjie.andserver.http.StatusCode; + +/** + * Created by Zhenjie Yan on 2018/9/10. + */ +public class BodyMissingException extends HttpException { + + private static final String MESSAGE = "RequestBody is missing."; + + public BodyMissingException() { + super(StatusCode.SC_BAD_REQUEST, MESSAGE); + } + + public BodyMissingException(Throwable cause) { + super(StatusCode.SC_BAD_REQUEST, MESSAGE, cause); + } + +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/error/ContentNotAcceptableException.java b/api/src/main/java/com/yanzhenjie/andserver/error/ContentNotAcceptableException.java new file mode 100644 index 0000000000000000000000000000000000000000..80ac03ba3ffdaa3648060b0fc26e4c4e9f45008f --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/error/ContentNotAcceptableException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.error; + +import com.yanzhenjie.andserver.http.StatusCode; + +/** + * Created by Zhenjie Yan on 2018/9/8. + */ +public class ContentNotAcceptableException extends HttpException { + + private static final String MESSAGE = "Could not find acceptable representation."; + + public ContentNotAcceptableException() { + super(StatusCode.SC_NOT_ACCEPTABLE, MESSAGE); + } + + public ContentNotAcceptableException(String message, Throwable cause) { + super(StatusCode.SC_NOT_ACCEPTABLE, message, cause); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/error/ContentNotSupportedException.java b/api/src/main/java/com/yanzhenjie/andserver/error/ContentNotSupportedException.java new file mode 100644 index 0000000000000000000000000000000000000000..504e396cdfd43b0ac45d47aab2e97809d0c5bda7 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/error/ContentNotSupportedException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.error; + +import com.yanzhenjie.andserver.http.StatusCode; +import com.yanzhenjie.andserver.util.MediaType; + +/** + * Created by Zhenjie Yan on 2018/9/8. + */ +public class ContentNotSupportedException extends HttpException { + + private static final String MESSAGE = "The content type [%s] is not supported."; + + public ContentNotSupportedException(MediaType mediaType) { + super(StatusCode.SC_UNSUPPORTED_MEDIA_TYPE, String.format(MESSAGE, mediaType)); + } + + public ContentNotSupportedException(MediaType mediaType, Throwable cause) { + super(StatusCode.SC_UNSUPPORTED_MEDIA_TYPE, String.format(MESSAGE, mediaType), cause); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/error/CookieMissingException.java b/api/src/main/java/com/yanzhenjie/andserver/error/CookieMissingException.java new file mode 100644 index 0000000000000000000000000000000000000000..abcf6ac2e2598944e299a83cd7d86898dcad05f0 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/error/CookieMissingException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.error; +import com.yanzhenjie.andserver.http.StatusCode; + +/** + * Created by Zhenjie Yan on 2018/9/9. + */ +public class CookieMissingException extends HttpException { + + private static final String MESSAGE = "Missing cookie [%s] for method parameter."; + + public CookieMissingException(String name) { + super(StatusCode.SC_BAD_REQUEST, String.format(MESSAGE, name)); + } + + public CookieMissingException(String name, Throwable cause) { + super(StatusCode.SC_BAD_REQUEST, String.format(MESSAGE, name), cause); + } + + public CookieMissingException(Throwable cause) { + super(StatusCode.SC_BAD_REQUEST, String.format(MESSAGE, ""), cause); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/error/HeaderMissingException.java b/api/src/main/java/com/yanzhenjie/andserver/error/HeaderMissingException.java new file mode 100644 index 0000000000000000000000000000000000000000..28eeff8d01bb3f267cf31a6113c841640ac88d32 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/error/HeaderMissingException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.error; + +import com.yanzhenjie.andserver.http.StatusCode; + +/** + * Created by Zhenjie Yan on 2018/9/9. + */ +public class HeaderMissingException extends HttpException { + + private static final String MESSAGE = "Missing header [%s] for method parameter."; + + public HeaderMissingException(String name) { + super(StatusCode.SC_BAD_REQUEST, String.format(MESSAGE, name)); + } + + public HeaderMissingException(String name, Throwable cause) { + super(StatusCode.SC_BAD_REQUEST, String.format(MESSAGE, name), cause); + } + + public HeaderMissingException(Throwable cause) { + super(StatusCode.SC_BAD_REQUEST, String.format(MESSAGE, ""), cause); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/error/HeaderValidateException.java b/api/src/main/java/com/yanzhenjie/andserver/error/HeaderValidateException.java new file mode 100644 index 0000000000000000000000000000000000000000..a1cb3b73ed5bc7b7aa17ee46a76476873c737148 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/error/HeaderValidateException.java @@ -0,0 +1,36 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.error; + +import com.yanzhenjie.andserver.http.StatusCode; + +/** + * Created by Zhenjie Yan on 2018/9/9. + */ +public class HeaderValidateException extends HttpException { + + public HeaderValidateException(String message) { + super(StatusCode.SC_FORBIDDEN, message); + } + + public HeaderValidateException(String message, Throwable cause) { + super(StatusCode.SC_FORBIDDEN, message, cause); + } + + public HeaderValidateException(Throwable cause) { + super(StatusCode.SC_FORBIDDEN, cause); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/error/HttpException.java b/api/src/main/java/com/yanzhenjie/andserver/error/HttpException.java new file mode 100644 index 0000000000000000000000000000000000000000..8ab17be8e9e9d6c0d5502cec2ea729851e0bbe38 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/error/HttpException.java @@ -0,0 +1,46 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.error; + +/** + * Created by Zhenjie Yan on 2018/7/19. + */ +public class HttpException extends RuntimeException { + + /** + * Status code. + */ + private int mStatusCode; + + public HttpException(int statusCode, String message) { + super(message); + this.mStatusCode = statusCode; + } + + public HttpException(int statusCode, String message, Throwable cause) { + super(message, cause); + this.mStatusCode = statusCode; + } + + public HttpException(int statusCode, Throwable cause) { + super(cause); + this.mStatusCode = statusCode; + } + + public int getStatusCode() { + return mStatusCode; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/error/InvalidMediaTypeException.java b/api/src/main/java/com/yanzhenjie/andserver/error/InvalidMediaTypeException.java new file mode 100644 index 0000000000000000000000000000000000000000..28a37a0d31aa49348d2af4dfa8f2074e11208661 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/error/InvalidMediaTypeException.java @@ -0,0 +1,51 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.error; + +/** + * Created by Zhenjie Yan on 2018/7/10. + */ +public class InvalidMediaTypeException extends IllegalArgumentException { + + private String mMediaType; + + /** + * Create a new InvalidMediaTypeException for the given media type. + * + * @param mediaType the offending media type. + * @param message a detail message indicating the invalid part. + */ + public InvalidMediaTypeException(String mediaType, String message) { + super("Invalid media type \"" + mediaType + "\": " + message); + this.mMediaType = mediaType; + } + + /** + * Constructor that allows wrapping {@link InvalidMimeTypeException}. + */ + public InvalidMediaTypeException(InvalidMimeTypeException ex) { + super(ex.getMessage(), ex); + this.mMediaType = ex.getMimeType(); + } + + /** + * Return the offending media type. + */ + public String getMediaType() { + return this.mMediaType; + } + +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/error/InvalidMimeTypeException.java b/api/src/main/java/com/yanzhenjie/andserver/error/InvalidMimeTypeException.java new file mode 100644 index 0000000000000000000000000000000000000000..86dc4f52ef657392364de9b855aeb12926ff4cdc --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/error/InvalidMimeTypeException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.error; + +/** + * Created by Zhenjie Yan on 2018/7/10. + */ +public class InvalidMimeTypeException extends IllegalArgumentException { + + private final String mMimeType; + + /** + * Create a new InvalidContentTypeException for the given content type. + * + * @param mimeType the offending media type. + * @param message a detail message indicating the invalid part. + */ + public InvalidMimeTypeException(String mimeType, String message) { + super("Invalid mime type \"" + mimeType + "\": " + message); + this.mMimeType = mimeType; + } + + /** + * Return the offending content type. + */ + public String getMimeType() { + return this.mMimeType; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/error/MaxUploadSizeExceededException.java b/api/src/main/java/com/yanzhenjie/andserver/error/MaxUploadSizeExceededException.java new file mode 100644 index 0000000000000000000000000000000000000000..beda78dfdb0cdfc106dd9020e9e5adef85510b1f --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/error/MaxUploadSizeExceededException.java @@ -0,0 +1,52 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.error; +import com.yanzhenjie.andserver.http.StatusCode; + +/** + * Created by Zhenjie Yan on 2018/8/9. + */ +public class MaxUploadSizeExceededException extends HttpException { + + private final long mMaxSize; + + /** + * Constructor for MaxUploadSizeExceededException. + * + * @param maxUploadSize the maximum upload size allowed. + */ + public MaxUploadSizeExceededException(long maxUploadSize) { + this(maxUploadSize, null); + } + + /** + * Constructor for MaxUploadSizeExceededException. + * + * @param maxSize the maximum upload size allowed. + * @param ex root cause from multipart parsing API in use. + */ + public MaxUploadSizeExceededException(long maxSize, Throwable ex) { + super(StatusCode.SC_REQUEST_ENTITY_TOO_LARGE, "Maximum upload size of " + maxSize + " bytes exceeded", ex); + this.mMaxSize = maxSize; + } + + /** + * Return the maximum upload size allowed. + */ + public long getMaxSize() { + return this.mMaxSize; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/error/MethodNotSupportException.java b/api/src/main/java/com/yanzhenjie/andserver/error/MethodNotSupportException.java new file mode 100644 index 0000000000000000000000000000000000000000..392670c1685257c51a8393100ada054841160548 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/error/MethodNotSupportException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.error; + + +import com.yanzhenjie.andserver.http.HttpMethod; +import com.yanzhenjie.andserver.http.StatusCode; + +import java.util.List; + +/** + * Created by Zhenjie Yan on 2018/7/19. + */ +public class MethodNotSupportException extends HttpException { + + private static final String MESSAGE = "The request method [%s] is not supported."; + + private List mMethods; + + public MethodNotSupportException(HttpMethod method) { + super(StatusCode.SC_METHOD_NOT_ALLOWED, String.format(MESSAGE, method.value())); + } + + public MethodNotSupportException(HttpMethod method, Throwable cause) { + super(StatusCode.SC_METHOD_NOT_ALLOWED, String.format(MESSAGE, method.value()), cause); + } + + public List getMethods() { + return mMethods; + } + + public void setMethods(List methods) { + mMethods = methods; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/error/MultipartException.java b/api/src/main/java/com/yanzhenjie/andserver/error/MultipartException.java new file mode 100644 index 0000000000000000000000000000000000000000..cf6bcb18eb542d8470dede7b1824ed313ad24216 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/error/MultipartException.java @@ -0,0 +1,44 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.error; + + +import com.yanzhenjie.andserver.http.StatusCode; + +/** + * Created by Zhenjie Yan on 2018/8/9. + */ +public class MultipartException extends HttpException { + + /** + * Constructor for MultipartException. + * + * @param msg the detail message. + */ + public MultipartException(String msg) { + super(StatusCode.SC_BAD_REQUEST, msg); + } + + /** + * Constructor for MultipartException. + * + * @param msg the detail message + * @param cause the root cause from the multipart parsing API in use + */ + public MultipartException(String msg, Throwable cause) { + super(StatusCode.SC_BAD_REQUEST, msg, cause); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/error/NotFoundException.java b/api/src/main/java/com/yanzhenjie/andserver/error/NotFoundException.java new file mode 100644 index 0000000000000000000000000000000000000000..3c6de0d960b55ffbcfe53facfa892e6b5ea9e8a8 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/error/NotFoundException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.error; + +import com.yanzhenjie.andserver.http.StatusCode; + +/** + * Created by Zhenjie Yan on 2018/7/19. + */ +public class NotFoundException extends HttpException { + + private static final String MESSAGE = "The resource [%s] is not found."; + + public NotFoundException() { + super(StatusCode.SC_NOT_FOUND, String.format(MESSAGE, "")); + } + + public NotFoundException(String path) { + super(StatusCode.SC_NOT_FOUND, String.format(MESSAGE, path)); + } + + public NotFoundException(String path, Throwable cause) { + super(StatusCode.SC_NOT_FOUND, String.format(MESSAGE, path), cause); + } + + public NotFoundException(Throwable cause) { + super(StatusCode.SC_NOT_FOUND, String.format(MESSAGE, ""), cause); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/error/ParamMissingException.java b/api/src/main/java/com/yanzhenjie/andserver/error/ParamMissingException.java new file mode 100644 index 0000000000000000000000000000000000000000..5dad959d833c95cf087ff0da6dda805b6225aef5 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/error/ParamMissingException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.error; + +import com.yanzhenjie.andserver.http.StatusCode; + +/** + * Created by Zhenjie Yan on 2018/9/9. + */ +public class ParamMissingException extends HttpException { + + private static final String MESSAGE = "Missing param [%s] for method parameter."; + + public ParamMissingException(String name) { + super(StatusCode.SC_BAD_REQUEST, String.format(MESSAGE, name)); + } + + public ParamMissingException(String name, Throwable cause) { + super(StatusCode.SC_BAD_REQUEST, String.format(MESSAGE, name), cause); + } + + public ParamMissingException(Throwable cause) { + super(StatusCode.SC_BAD_REQUEST, String.format(MESSAGE, ""), cause); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/error/ParamValidateException.java b/api/src/main/java/com/yanzhenjie/andserver/error/ParamValidateException.java new file mode 100644 index 0000000000000000000000000000000000000000..2f8246fff4750cf29e59250adf2c128e1b80ae7b --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/error/ParamValidateException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.error; +import com.yanzhenjie.andserver.http.StatusCode; + +/** + * Created by Zhenjie Yan on 2018/9/8. + */ +public class ParamValidateException extends HttpException { + + public ParamValidateException(String message) { + super(StatusCode.SC_FORBIDDEN, message); + } + + public ParamValidateException(String message, Throwable cause) { + super(StatusCode.SC_FORBIDDEN, message, cause); + } + + public ParamValidateException(Throwable cause) { + super(StatusCode.SC_FORBIDDEN, cause); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/error/PathMissingException.java b/api/src/main/java/com/yanzhenjie/andserver/error/PathMissingException.java new file mode 100644 index 0000000000000000000000000000000000000000..c0405a81924d6e454f182121c80560d0c80c3244 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/error/PathMissingException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.error; + +import com.yanzhenjie.andserver.http.StatusCode; + +/** + * Created by Zhenjie Yan on 2018/9/10. + */ +public class PathMissingException extends HttpException { + + private static final String MESSAGE = "Missing param [%s] for path parameter."; + + public PathMissingException(String name) { + super(StatusCode.SC_BAD_REQUEST, String.format(MESSAGE, name)); + } + + public PathMissingException(String name, Throwable cause) { + super(StatusCode.SC_BAD_REQUEST, String.format(MESSAGE, name), cause); + } + + public PathMissingException(Throwable cause) { + super(StatusCode.SC_BAD_REQUEST, String.format(MESSAGE, ""), cause); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/error/ServerInternalException.java b/api/src/main/java/com/yanzhenjie/andserver/error/ServerInternalException.java new file mode 100644 index 0000000000000000000000000000000000000000..058004b30d01b9f21646856cf8a679a9ba83a56d --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/error/ServerInternalException.java @@ -0,0 +1,38 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.error; + +import com.yanzhenjie.andserver.http.StatusCode; + +/** + * Created by Zhenjie Yan on 2018/9/4. + */ +public class ServerInternalException extends HttpException { + + private static final String MESSAGE = "Server internal error"; + + public ServerInternalException(String subMessage) { + super(StatusCode.SC_INTERNAL_SERVER_ERROR, String.format("%s, %s.", MESSAGE, subMessage)); + } + + public ServerInternalException(String subMessage, Throwable cause) { + super(StatusCode.SC_INTERNAL_SERVER_ERROR, String.format("%s, %s.", MESSAGE, subMessage), cause); + } + + public ServerInternalException(Throwable cause) { + super(StatusCode.SC_INTERNAL_SERVER_ERROR, String.format("%s.", MESSAGE), cause); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/ETag.java b/api/src/main/java/com/yanzhenjie/andserver/framework/ETag.java new file mode 100644 index 0000000000000000000000000000000000000000..74491a37c199cdc60e00e2a41a1a070856122eaf --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/ETag.java @@ -0,0 +1,32 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework; + + +import com.yanzhenjie.andserver.http.HttpRequest; + +/** + * Created by Zhenjie Yan on 2018/8/31. + */ +public interface ETag { + + /** + * Get the {@code ETag} requesting the specified resource. + * + *

Can simply return {@code null} if there's no support. + */ + String getETag(HttpRequest request) throws Throwable; +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/ExceptionResolver.java b/api/src/main/java/com/yanzhenjie/andserver/framework/ExceptionResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..e59e2a99d53fbe6f376da078264fd110545e0468 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/ExceptionResolver.java @@ -0,0 +1,73 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework; + + +import com.yanzhenjie.andserver.error.HttpException; +import com.yanzhenjie.andserver.error.MethodNotSupportException; +import com.yanzhenjie.andserver.framework.body.StringBody; +import com.yanzhenjie.andserver.http.*; +import com.yanzhenjie.andserver.util.TextUtils; + +import java.util.List; + +/** + * Created by Zhenjie Yan on 2018/8/8. + */ +public interface ExceptionResolver { + + class ResolverWrapper implements ExceptionResolver { + + private final ExceptionResolver mResolver; + + public ResolverWrapper(ExceptionResolver resolver) { + this.mResolver = resolver; + } + + @Override + public void onResolve(HttpRequest request, HttpResponse response, Throwable e) { + if (e instanceof MethodNotSupportException) { + List methods = ((MethodNotSupportException) e).getMethods(); + if (methods != null && methods.size() > 0) { + response.setHeader(HttpHeaders.ALLOW, TextUtils.join(", ", methods)); + } + } + mResolver.onResolve(request, response, e); + } + } + + ExceptionResolver DEFAULT = new ExceptionResolver() { + @Override + public void onResolve( HttpRequest request, HttpResponse response, Throwable e) { + if (e instanceof HttpException) { + HttpException ex = (HttpException) e; + response.setStatus(ex.getStatusCode()); + } else { + response.setStatus(StatusCode.SC_INTERNAL_SERVER_ERROR); + } + response.setBody(new StringBody(e.getMessage())); + } + }; + + /** + * Resolve exceptions that occur in the program, replacing the default output information for the exception. + * + * @param request current request. + * @param response current response. + * @param e an exception occurred in the program. + */ + void onResolve(HttpRequest request, HttpResponse response, Throwable e); +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/HandlerInterceptor.java b/api/src/main/java/com/yanzhenjie/andserver/framework/HandlerInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..ed8aff069b6584ba3b91751597582a7a8d510f44 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/HandlerInterceptor.java @@ -0,0 +1,39 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework; + + +import com.yanzhenjie.andserver.framework.handler.RequestHandler; +import com.yanzhenjie.andserver.http.HttpRequest; +import com.yanzhenjie.andserver.http.HttpResponse; + +/** + * Created by Zhenjie Yan on 2018/8/8. + */ +public interface HandlerInterceptor { + + /** + * Intercept the execution of a handler. + * + * @param request current request. + * @param response current response. + * @param handler the corresponding handler of the current request. + * + * @return true if the interceptor has processed the request and responded. + */ + boolean onIntercept(HttpRequest request, HttpResponse response, RequestHandler handler) + throws Exception; +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/LastModified.java b/api/src/main/java/com/yanzhenjie/andserver/framework/LastModified.java new file mode 100644 index 0000000000000000000000000000000000000000..7a6c366524a886d694218adc9e6c7bcd10ddebae --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/LastModified.java @@ -0,0 +1,36 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework; + +import com.yanzhenjie.andserver.http.HttpRequest; + +/** + * Created by Zhenjie Yan on 2018/8/29. + */ +public interface LastModified { + + /** + * The return value will be sent to the HTTP client as {@code Last-Modified} header, and compared with {@code + * If-Modified-Since} headers that the client sends back. The content will only get regenerated if there has been a + * modification. + * + * @param request current request + * + * @return the time the underlying resource was last modified, or -1 meaning that the content must always be + * regenerated. + */ + long getLastModified(HttpRequest request) throws Throwable; +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/MessageConverter.java b/api/src/main/java/com/yanzhenjie/andserver/framework/MessageConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..3c5637722da1073e7bedfc2450e021ff8f28bc95 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/MessageConverter.java @@ -0,0 +1,55 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework; + + + +import com.yanzhenjie.andserver.framework.view.ViewResolver; +import com.yanzhenjie.andserver.http.ResponseBody; +import com.yanzhenjie.andserver.util.MediaType; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Type; + +/** + * Created by Zhenjie Yan on 2018/9/6. + */ +public interface MessageConverter { + + /** + * Convert a specific output to the response body. Some of the return values of handlers that cannot be recognized + * by + * {@link ViewResolver} require a message converter to be converted to a response body. + * + * @param output output of handle. + * @param mediaType the content media type specified by the handler. + */ + ResponseBody convert(Object output, MediaType mediaType); + + /** + * Convert RequestBody to a object. + * + * @param stream {@link InputStream}. + * @param mediaType he content media type. + * @param type type of object. + * @param type of object. + * + * @return object. + */ + + T convert(InputStream stream, MediaType mediaType, Type type) throws IOException; +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/ModifiedInterceptor.java b/api/src/main/java/com/yanzhenjie/andserver/framework/ModifiedInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..d98e54448174f3b25a219b20d7229eef56c8c031 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/ModifiedInterceptor.java @@ -0,0 +1,55 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework; + + +import com.yanzhenjie.andserver.AndServer; +import com.yanzhenjie.andserver.framework.handler.RequestHandler; +import com.yanzhenjie.andserver.http.HttpMethod; +import com.yanzhenjie.andserver.http.HttpRequest; +import com.yanzhenjie.andserver.http.HttpResponse; +import com.yanzhenjie.andserver.http.Modified; +import ohos.hiviewdfx.HiLog; +import ohos.hiviewdfx.HiLogLabel; + +/** + * Created by Zhenjie Yan on 2018/9/14. + */ +public class ModifiedInterceptor implements HandlerInterceptor { + + @Override + public boolean onIntercept(HttpRequest request, HttpResponse response, + RequestHandler handler) { + // Process cache header, if supported by the handler. + HttpMethod method = request.getMethod(); + if (method == HttpMethod.GET || method == HttpMethod.HEAD) { + String eTag = null; + try { + eTag = handler.getETag(request); + } catch (Throwable e) { + HiLog.warn(new HiLogLabel(0, 1, AndServer.TAG),"", e); + } + long lastModified = -1; + try { + lastModified = handler.getLastModified(request); + } catch (Throwable e) { + HiLog.warn(new HiLogLabel(0, 1, AndServer.TAG),"", e); + } + return new Modified(request, response).process(eTag, lastModified); + } + return false; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/body/FileBody.java b/api/src/main/java/com/yanzhenjie/andserver/framework/body/FileBody.java new file mode 100644 index 0000000000000000000000000000000000000000..3272edb7edff41c29e8ef3188e3c46a1c364cc1b --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/body/FileBody.java @@ -0,0 +1,60 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.body; + +import com.yanzhenjie.andserver.http.ResponseBody; +import com.yanzhenjie.andserver.util.IOUtils; +import com.yanzhenjie.andserver.util.MediaType; + +import java.io.*; + +/** + * Created by Zhenjie Yan on 2018/8/6. + */ +public class FileBody implements ResponseBody { + + private File mBody; + + public FileBody(File body) { + if (body == null) { + throw new IllegalArgumentException("The file cannot be null."); + } + this.mBody = body; + } + + @Override + public boolean isRepeatable() { + return true; + } + + @Override + public long contentLength() { + return mBody.length(); + } + + + @Override + public MediaType contentType() { + return MediaType.getFileMediaType(mBody.getName()); + } + + @Override + public void writeTo(OutputStream output) throws IOException { + InputStream is = new FileInputStream(mBody); + IOUtils.write(is, output); + IOUtils.closeQuietly(is); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/body/JsonBody.java b/api/src/main/java/com/yanzhenjie/andserver/framework/body/JsonBody.java new file mode 100644 index 0000000000000000000000000000000000000000..756f06aac5d475f75fec7b34bdb8df95575bb186 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/body/JsonBody.java @@ -0,0 +1,39 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.body; + + +import com.yanzhenjie.andserver.util.MediaType; +import ohos.utils.zson.ZSONObject; + +/** + * Created by Zhenjie Yan on 2018/8/8. + */ +public class JsonBody extends StringBody { + + public JsonBody(String body) { + super(body); + } + + public JsonBody(ZSONObject object) { + super(object.toString()); + } + + @Override + public MediaType contentType() { + return MediaType.APPLICATION_JSON_UTF8; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/body/StreamBody.java b/api/src/main/java/com/yanzhenjie/andserver/framework/body/StreamBody.java new file mode 100644 index 0000000000000000000000000000000000000000..a64a4739986b39bbf5633f68545aeb93b2c36d91 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/body/StreamBody.java @@ -0,0 +1,86 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.body; + + + + +import com.yanzhenjie.andserver.http.ResponseBody; +import com.yanzhenjie.andserver.util.IOUtils; +import com.yanzhenjie.andserver.util.MediaType; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Created by Zhenjie Yan on 2018/9/7. + */ +public class StreamBody implements ResponseBody { + + private InputStream mStream; + private long mLength; + private MediaType mMediaType; + + public StreamBody(InputStream stream) { + this(stream, MediaType.APPLICATION_OCTET_STREAM); + } + + public StreamBody(InputStream stream, long length) { + this(stream, length, MediaType.APPLICATION_OCTET_STREAM); + } + + public StreamBody(InputStream stream, MediaType mediaType) { + this(stream, 0, mediaType); + } + + public StreamBody(InputStream stream, long length, MediaType mediaType) { + this.mStream = stream; + this.mLength = length; + this.mMediaType = mediaType; + } + + @Override + public boolean isRepeatable() { + return false; + } + + @Override + public long contentLength() { + if (mLength == 0 && mStream instanceof FileInputStream) { + try { + mLength = ((FileInputStream) mStream).getChannel().size(); + return mLength; + } catch (IOException e) { + e.printStackTrace(); + } + } + return mLength; + } + + + @Override + public MediaType contentType() { + return mMediaType; + } + + @Override + public void writeTo(OutputStream output) throws IOException { + IOUtils.write(mStream, output); + IOUtils.closeQuietly(mStream); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/body/StringBody.java b/api/src/main/java/com/yanzhenjie/andserver/framework/body/StringBody.java new file mode 100644 index 0000000000000000000000000000000000000000..95dda0a44f68dc88588788c1a639bc2f97775dec --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/body/StringBody.java @@ -0,0 +1,82 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.body; + +import com.yanzhenjie.andserver.http.ResponseBody; +import com.yanzhenjie.andserver.util.IOUtils; +import com.yanzhenjie.andserver.util.MediaType; +import org.apache.commons.io.Charsets; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * Created by Zhenjie Yan on 2018/8/6. + */ +public class StringBody implements ResponseBody { + + private byte[] mBody; + private MediaType mMediaType; + + public StringBody(String body) { + this(body, MediaType.TEXT_PLAIN); + } + + public StringBody(String body, MediaType mediaType) { + if (body == null) { + throw new IllegalArgumentException("The content cannot be null."); + } + + this.mMediaType = mediaType; + if (mMediaType == null) { + mMediaType = new MediaType(MediaType.TEXT_PLAIN, StandardCharsets.UTF_8); + } + + Charset charset = mMediaType.getCharset(); + if (charset == null) { + charset = StandardCharsets.UTF_8; + } + this.mBody = body.getBytes(charset); + } + + @Override + public boolean isRepeatable() { + return true; + } + + @Override + public long contentLength() { + return mBody.length; + } + + + @Override + public MediaType contentType() { + Charset charset = mMediaType.getCharset(); + if (charset == null) { + charset = StandardCharsets.UTF_8; + return new MediaType(mMediaType.getType(), mMediaType.getSubtype(), charset); + } + return mMediaType; + } + + @Override + public void writeTo(OutputStream output) throws IOException { + IOUtils.write(output, mBody); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/config/Delegate.java b/api/src/main/java/com/yanzhenjie/andserver/framework/config/Delegate.java new file mode 100644 index 0000000000000000000000000000000000000000..7f4c837f138237f2d6da6dd3364a73481c622759 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/config/Delegate.java @@ -0,0 +1,57 @@ +/* + * Copyright © 2019 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.config; + + +import com.yanzhenjie.andserver.framework.website.Website; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Zhenjie Yan on 2019-06-30. + */ +public class Delegate implements WebConfig.Delegate { + + public static Delegate newInstance() { + return new Delegate(); + } + + private Multipart mMultipart; + private List mWebsites; + + private Delegate() { + mWebsites = new ArrayList<>(); + } + + public Multipart getMultipart() { + return mMultipart; + } + + @Override + public void setMultipart(Multipart multipart) { + mMultipart = multipart; + } + + public List getWebsites() { + return mWebsites; + } + + @Override + public void addWebsite(Website website) { + mWebsites.add(website); + } +} diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/config/Multipart.java b/api/src/main/java/com/yanzhenjie/andserver/framework/config/Multipart.java new file mode 100644 index 0000000000000000000000000000000000000000..fed0090ec1e1df67660248bc80ab05a63e839942 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/config/Multipart.java @@ -0,0 +1,119 @@ +/* + * Copyright © 2019 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.config; + +import org.apache.commons.fileupload.FileUpload; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; + +import java.io.File; + +/** + * Created by Zhenjie Yan on 2019-06-28. + */ +public class Multipart { + + public static Builder newBuilder() { + return new Builder(); + } + + private final long allFileMaxSize; + private final long fileMaxSize; + private final int maxInMemorySize; + private final File uploadTempDir; + + private Multipart(Builder builder) { + this.allFileMaxSize = builder.allFileMaxSize; + this.fileMaxSize = builder.fileMaxSize; + this.maxInMemorySize = builder.maxInMemorySize; + this.uploadTempDir = builder.uploadTempDir; + } + + public long getAllFileMaxSize() { + return allFileMaxSize; + } + + public long getFileMaxSize() { + return fileMaxSize; + } + + public int getMaxInMemorySize() { + return maxInMemorySize; + } + + public File getUploadTempDir() { + return uploadTempDir; + } + + public static class Builder { + + private long allFileMaxSize; + private long fileMaxSize; + private int maxInMemorySize; + private File uploadTempDir; + + private Builder() { + } + + /** + * Set the maximum size (in bytes) allowed for uploading. -1 indicates no limit (the default). + * + * @param allFileMaxSize the maximum upload size allowed. + * + * @see FileUpload#setSizeMax(long) + */ + public Builder allFileMaxSize(long allFileMaxSize) { + this.allFileMaxSize = allFileMaxSize; + return this; + } + + /** + * Set the maximum size (in bytes) allowed for each individual file. -1 indicates no limit (the default). + * + * @param fileMaxSize the maximum upload size per file. + * + * @see FileUpload#setFileSizeMax(long) + */ + public Builder fileMaxSize(long fileMaxSize) { + this.fileMaxSize = fileMaxSize; + return this; + } + + /** + * Set the maximum allowed size (in bytes) before uploads are written to disk, default is 10240. + * + * @param maxInMemorySize the maximum in memory size allowed. + * + * @see DiskFileItemFactory#setSizeThreshold(int) + */ + public Builder maxInMemorySize(int maxInMemorySize) { + this.maxInMemorySize = maxInMemorySize; + return this; + } + + /** + * Set the temporary directory where uploaded files get stored. + */ + public Builder uploadTempDir(File uploadTempDir) { + this.uploadTempDir = uploadTempDir; + return this; + } + + public Multipart build() { + return new Multipart(this); + } + + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/config/WebConfig.java b/api/src/main/java/com/yanzhenjie/andserver/framework/config/WebConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..fb1a92bb09eca8dad9d2139ae86f7fef18d9a190 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/config/WebConfig.java @@ -0,0 +1,54 @@ +/* + * Copyright © 2019 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.config; + +import com.yanzhenjie.andserver.framework.website.Website; +import ohos.app.Context; + +/** + * Created by Zhenjie Yan on 2019-06-28. + *

+ * @Config
+ * public class AppConfig implements WebConfig {
+ *
+ *     @Override
+ *     public void onConfig(Context context, Delegate delegate) {
+ *         Website website = ...;
+ *         delegate.addWebsite(website);
+ *
+ *         Multipart multipart = Multipart.newBuilder()...build();
+ *         delegate.setMultipart(multipart);
+ *     }
+ * }
+ * 
+ */ +public interface WebConfig { + + void onConfig(Context context, Delegate delegate); + + interface Delegate { + + /** + * + */ + void setMultipart(Multipart multipart); + + /** + * + */ + void addWebsite(Website website); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/cross/CrossOrigin.java b/api/src/main/java/com/yanzhenjie/andserver/framework/cross/CrossOrigin.java new file mode 100644 index 0000000000000000000000000000000000000000..69209b264aa3a3c49d0633e2a96b6af437d0269e --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/cross/CrossOrigin.java @@ -0,0 +1,89 @@ +/* + * Copyright 2020 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.cross; + + +import com.yanzhenjie.andserver.http.HttpMethod; + +/** + * Created by Zhenjie Yan on 10/16/20. + */ +public class CrossOrigin { + + private String[] origins; + private String[] allowedHeaders; + private String[] exposedHeaders; + private HttpMethod[] methods; + private boolean allowCredentials; + private long maxAge; + + public CrossOrigin() { + } + + + public String[] getOrigins() { + return origins; + } + + public void setOrigins(String[] origins) { + this.origins = origins; + } + + + public String[] getAllowedHeaders() { + return allowedHeaders; + } + + public void setAllowedHeaders(String[] allowedHeaders) { + this.allowedHeaders = allowedHeaders; + } + + + public String[] getExposedHeaders() { + return exposedHeaders; + } + + public void setExposedHeaders(String[] exposedHeaders) { + this.exposedHeaders = exposedHeaders; + } + + + public HttpMethod[] getMethods() { + return methods; + } + + public void setMethods(HttpMethod[] methods) { + this.methods = methods; + } + + + public boolean isAllowCredentials() { + return allowCredentials; + } + + public void setAllowCredentials(boolean allowCredentials) { + this.allowCredentials = allowCredentials; + } + + + public long getMaxAge() { + return maxAge; + } + + public void setMaxAge(long maxAge) { + this.maxAge = maxAge; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/handler/HandlerAdapter.java b/api/src/main/java/com/yanzhenjie/andserver/framework/handler/HandlerAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..301566dda44fe3eb8fe8c059b9cc93935860df42 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/handler/HandlerAdapter.java @@ -0,0 +1,45 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.handler; + + +import com.yanzhenjie.andserver.http.HttpRequest; + +/** + * Created by Zhenjie Yan on 2018/9/4. + */ +public interface HandlerAdapter { + + /** + * Whether to intercept the current request. + * + * @param request current request. + * + * @return returns true, otherwise false. + */ + boolean intercept(HttpRequest request); + + /** + * Get the handler that handles the current request. + * + * @param request current request. + * + * @return the handler to handle current request. + * + * @throws if current request cannot find the corresponding handler. + */ + RequestHandler getHandler(HttpRequest request); +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/handler/MappingAdapter.java b/api/src/main/java/com/yanzhenjie/andserver/framework/handler/MappingAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..9d57f6ea01e9e04aa8cd3673c50f6b00c3562a0e --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/handler/MappingAdapter.java @@ -0,0 +1,330 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.handler; + + +import com.yanzhenjie.andserver.error.*; +import com.yanzhenjie.andserver.framework.mapping.Mapping; +import com.yanzhenjie.andserver.framework.mapping.Mime; +import com.yanzhenjie.andserver.framework.mapping.Pair; +import com.yanzhenjie.andserver.framework.mapping.Path; +import com.yanzhenjie.andserver.http.HttpContext; +import com.yanzhenjie.andserver.http.HttpMethod; +import com.yanzhenjie.andserver.http.HttpRequest; +import com.yanzhenjie.andserver.util.MediaType; +import com.yanzhenjie.andserver.util.Patterns; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Created by Zhenjie Yan on 2018/9/8. + */ +public abstract class MappingAdapter implements HandlerAdapter, Patterns { + + @Override + public boolean intercept(HttpRequest request) { + List pathSegments = Path.pathToList(request.getPath()); + + List mappings = getExactMappings(pathSegments); + if (mappings.isEmpty()) { + mappings = getBlurredMappings(pathSegments); + } + if (mappings.isEmpty()) { + return false; + } + + HttpMethod method = request.getMethod(); + if (method.equals(HttpMethod.OPTIONS)) { + return true; + } + + Mapping mapping = MappingAdapter.findMappingByMethod(mappings, method); + if (mapping == null) { + MethodNotSupportException exception = new MethodNotSupportException(method); + List methods = MappingAdapter.findSupportMethods(mappings); + exception.setMethods(methods); + throw exception; + } + + Pair param = mapping.getParam(); + if (param != null) { + validateParams(param, request); + } + + Pair header = mapping.getHeader(); + if (header != null) { + validateHeaders(header, request); + } + + Mime consume = mapping.getConsume(); + if (consume != null) { + validateConsume(consume, request); + } + + Mime produce = mapping.getProduce(); + if (produce != null) { + validateProduce(produce, request); + } + + return true; + } + + + @Override + public RequestHandler getHandler( HttpRequest request) { + List pathSegments = Path.pathToList(request.getPath()); + + List mappings = getExactMappings(pathSegments); + if (mappings.isEmpty()) { + mappings = getBlurredMappings(pathSegments); + } + + HttpMethod method = request.getMethod(); + Mapping mapping = MappingAdapter.findMappingByMethod(mappings, method); + + if (method.equals(HttpMethod.OPTIONS) && mapping == null) { + return new OptionsHandler(request, mappings, getMappingMap()); + } + + if (mapping == null) { + return null; + } + + Mime mime = mapping.getProduce(); + if (mime != null) { + List produces = mime.getRuleList(); + MediaType mediaType = null; + for (Mime.Rule produce: produces) { + String text = produce.toString(); + if (!text.startsWith("!")) { + mediaType = produce; + break; + } + } + request.setAttribute(HttpContext.RESPONSE_PRODUCE_TYPE, mediaType); + } + + return getMappingMap().get(mapping); + } + + private List getExactMappings(List httpSegments) { + List mappings = new ArrayList<>(); + + Map mappingMap = getMappingMap(); + for (Mapping mapping: mappingMap.keySet()) { + Path path = mapping.getPath(); + List rules = path.getRuleList(); + for (Path.Rule rule: rules) { + if (matchExactPath(rule.getSegments(), httpSegments)) { + mappings.add(mapping); + } + } + } + return mappings; + } + + private boolean matchExactPath(List segments, List httpSegments) { + if (httpSegments.size() != segments.size()) { + return false; + } + + if (Path.listToPath(segments).equals(Path.listToPath(httpSegments))) { + return true; + } + return false; + } + + private List getBlurredMappings(List httpSegments) { + List mappings = new ArrayList<>(); + + Map mappingMap = getMappingMap(); + for (Mapping mapping: mappingMap.keySet()) { + Path path = mapping.getPath(); + List rules = path.getRuleList(); + for (Path.Rule rule: rules) { + if (matchBlurredPath(rule.getSegments(), httpSegments)) { + mappings.add(mapping); + } + } + } + return mappings; + } + + private boolean matchBlurredPath(List segments, List httpSegments) { + if (httpSegments.size() != segments.size()) { + return false; + } + + for (int i = 0; i < segments.size(); i++) { + Path.Segment segment = segments.get(i); + if (!segment.equals(httpSegments.get(i)) && !segment.isBlurred()) { + return false; + } + } + return true; + } + + private void validateParams(Pair param, HttpRequest request) { + List rules = param.getRuleList(); + for (Pair.Rule rule: rules) { + String key = rule.getKey(); + List keys = request.getParameterNames(); + String value = rule.getValue(); + List values = request.getParameters(key); + if (rule.isNoKey()) { + if (keys.contains(key)) { + throw new ParamValidateException(String.format("The parameter [%s] is not allowed.", key)); + } + } else if (rule.isNoValue()) { + if (values.contains(value)) { + throw new ParamValidateException( + String.format("The value of parameter %s cannot be %s.", key, value)); + } + } else if (!key.isEmpty() && !value.isEmpty()) { + if (!keys.contains(key) || !values.contains(value)) { + throw new ParamValidateException( + String.format("The value of parameter %s is missing or wrong.", key)); + } + } else if (!key.isEmpty() && value.isEmpty()) { + if (!keys.contains(key)) { + throw new ParamValidateException(String.format("The parameter %s is missing.", key)); + } + } + } + } + + private void validateHeaders(Pair header, HttpRequest request) { + List rules = header.getRuleList(); + for (Pair.Rule rule: rules) { + String key = rule.getKey(); + List keys = request.getHeaderNames(); + String value = rule.getValue(); + List values = request.getHeaders(key); + if (rule.isNoKey()) { + if (keys.contains(key)) { + throw new HeaderValidateException(String.format("The header [%s] is not allowed.", key)); + } + } else if (rule.isNoValue()) { + if (values.contains(value)) { + throw new HeaderValidateException( + String.format("The value of header %s cannot be %s.", key, value)); + } + } else if (!key.isEmpty() && !value.isEmpty() && + (!keys.contains(key) || !values.contains(value))) { + throw new HeaderValidateException(String.format("The value of header %s is missing or wrong.", key)); + } else if (!key.isEmpty() && value.isEmpty()) { + if (!keys.contains(key)) { + throw new HeaderValidateException(String.format("The header %s is missing.", key)); + } + } + } + } + + private void validateConsume(Mime mime, HttpRequest request) { + List rules = mime.getRuleList(); + MediaType contentType = request.getContentType(); + + List includeType = new ArrayList<>(); + for (Mime.Rule rule: rules) { + String type = rule.getType(); + boolean nonContent = type.startsWith("!"); + if (nonContent) { + type = type.substring(1); + } + MediaType consume = new MediaType(type, rule.getSubtype()); + + if (nonContent) { + if (consume.equalsExcludeParameter(contentType)) { + throw new ContentNotSupportedException(contentType); + } + } else { + includeType.add(consume); + } + } + + boolean included = false; + for (MediaType mediaType: includeType) { + if (mediaType.includes(contentType)) { + included = true; + break; + } + } + if (!included) { + throw new ContentNotSupportedException(contentType); + } + } + + private void validateProduce(Mime mime, HttpRequest request) { + List rules = mime.getRuleList(); + List accepts = request.getAccepts(); + for (Mime.Rule rule: rules) { + String type = rule.getType(); + boolean nonContent = type.startsWith("!"); + if (nonContent) { + type = type.substring(1); + } + MediaType produce = new MediaType(type, rule.getSubtype()); + + boolean exclude = false; + for (MediaType accept: accepts) { + if (accept.includes(produce)) { + exclude = true; + } + } + if (nonContent && exclude) { + throw new ContentNotAcceptableException(); + } + if (!nonContent && !exclude) { + throw new ContentNotAcceptableException(); + } + } + } + + /** + * Get all the mappings for this adapter. + * + * @return all mappings, non-null, non-empty. + */ + + protected abstract Map getMappingMap(); + + /** + * Get the host of the {@code HandlerAdapter}. + * + * @return the host of the adapter. + */ + + protected abstract Object getHost(); + + public static List findSupportMethods(List mappings) { + List methods = new ArrayList<>(); + for (Mapping child: mappings) { + methods.addAll(child.getMethod().getRuleList()); + } + return methods; + } + + public static Mapping findMappingByMethod(List mappings, HttpMethod method) { + for (Mapping child: mappings) { + if (child.getMethod().getRuleList().contains(method)) { + return child; + } + } + return null; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/handler/MappingHandler.java b/api/src/main/java/com/yanzhenjie/andserver/framework/handler/MappingHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..bd344f0a1abea964deb23d6260cae5bfc92b854e --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/handler/MappingHandler.java @@ -0,0 +1,178 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.handler; + +import com.yanzhenjie.andserver.framework.ETag; +import com.yanzhenjie.andserver.framework.LastModified; +import com.yanzhenjie.andserver.framework.body.StringBody; +import com.yanzhenjie.andserver.framework.cross.CrossOrigin; +import com.yanzhenjie.andserver.framework.mapping.Addition; +import com.yanzhenjie.andserver.framework.mapping.Mapping; +import com.yanzhenjie.andserver.framework.mapping.Path; +import com.yanzhenjie.andserver.framework.view.BodyView; +import com.yanzhenjie.andserver.framework.view.View; +import com.yanzhenjie.andserver.http.HttpHeaders; +import com.yanzhenjie.andserver.http.HttpMethod; +import com.yanzhenjie.andserver.http.HttpRequest; +import com.yanzhenjie.andserver.http.HttpResponse; +import com.yanzhenjie.andserver.util.StringUtils; +import com.yanzhenjie.andserver.util.TextUtils; +import org.apache.httpcore.HttpStatus; + +import java.util.*; + +/** + * Created by Zhenjie Yan on 2018/9/9. + */ +public abstract class MappingHandler implements MethodHandler { + + private final Object mHost; + private final Mapping mMapping; + private final Addition mAddition; + private final CrossOrigin mCrossOrigin; + + public MappingHandler(Object host, Mapping mapping, Addition addition, + CrossOrigin crossOrigin) { + this.mHost = host; + this.mMapping = mapping; + this.mAddition = addition; + this.mCrossOrigin = crossOrigin; + } + + @Override + public String getETag( HttpRequest request) throws Throwable { + Object o = getHost(); + if (o instanceof ETag) { + return ((ETag) o).getETag(request); + } + return null; + } + + @Override + public long getLastModified( HttpRequest request) throws Throwable { + Object o = getHost(); + if (o instanceof LastModified) { + return ((LastModified) o).getLastModified(request); + } + return -1; + } + + + @Override + public Addition getAddition() { + return mAddition; + } + + + @Override + public CrossOrigin getCrossOrigin() { + return mCrossOrigin; + } + + + @Override + public Mapping getMapping() { + return mMapping; + } + + + protected Object getHost() { + return mHost; + } + + /** + * Get the path to match the request. + * + * @param httpPath http path. + * + * @return the path of handler. + */ + + protected Map getPathVariable( String httpPath) { + List httpSegments = Path.pathToList(httpPath); + List ruleList = mMapping.getPath().getRuleList(); + for (Path.Rule rule: ruleList) { + List segments = rule.getSegments(); + if (httpSegments.size() != segments.size()) { + continue; + } + + String path = Path.listToPath(segments); + if (path.equals(httpPath)) { + return Collections.emptyMap(); + } + + boolean matches = true; + boolean isBlurred = false; + for (int i = 0; i < segments.size(); i++) { + Path.Segment segment = segments.get(i); + boolean blurred = segment.isBlurred(); + isBlurred = isBlurred || blurred; + if (!segment.equals(httpSegments.get(i)) && !blurred) { + matches = false; + break; + } + } + + if (matches && isBlurred) { + Map map = new HashMap<>(); + for (int i = 0; i < segments.size(); i++) { + Path.Segment segment = segments.get(i); + if (segment.isBlurred()) { + Path.Segment httpSegment = httpSegments.get(i); + + String key = segment.getValue(); + key = key.substring(1, key.length() - 1); + map.put(key, httpSegment.getValue()); + } + } + return map; + } + } + + return Collections.emptyMap(); + } + + @Override + public View handle(HttpRequest request, HttpResponse response) throws Throwable { + String origin = request.getHeader(HttpHeaders.ORIGIN); + if (!StringUtils.isEmpty(origin) && mCrossOrigin != null) { + HttpMethod method = request.getMethod(); + + List allowMethods = Arrays.asList(mCrossOrigin.getMethods()); + if (!allowMethods.isEmpty() && !allowMethods.contains(method)) { + return invalidCORS(response); + } + + response.setHeader(HttpHeaders.Access_Control_Allow_Origin, origin); + boolean credentials = mCrossOrigin.isAllowCredentials(); + response.setHeader(HttpHeaders.Access_Control_Allow_Credentials, Boolean.toString(credentials)); + response.setHeader(HttpHeaders.VARY, HttpHeaders.ORIGIN); + } + + return onHandle(request, response); + } + + private View invalidCORS(HttpResponse response, HttpMethod... methods) { + response.setStatus(HttpStatus.SC_FORBIDDEN); + if (methods != null && methods.length > 0) { + response.setHeader(HttpHeaders.ALLOW, TextUtils.join(", ", methods)); + } + return new BodyView(new StringBody(OptionsHandler.INVALID_CORS_REQUEST)); + } + + protected abstract View onHandle(HttpRequest request, HttpResponse response) throws Throwable; +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/handler/MethodHandler.java b/api/src/main/java/com/yanzhenjie/andserver/framework/handler/MethodHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..49317b9537934234cebd87beec5001daa944dc33 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/handler/MethodHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.handler; + + +import com.yanzhenjie.andserver.framework.cross.CrossOrigin; +import com.yanzhenjie.andserver.framework.mapping.Addition; +import com.yanzhenjie.andserver.framework.mapping.Mapping; + +/** + * Created by Zhenjie Yan on 2018/6/16. + */ +public interface MethodHandler extends RequestHandler { + + /** + * Get addition configuration, addition provides some added value. + * + * @return {@link Addition}. + */ + + Addition getAddition(); + + /** + * Get cross origin information. + * + * @return {@link CrossOrigin} + */ + + CrossOrigin getCrossOrigin(); + + /** + * Get mapping configuration, mapping provides all the annotation information for this method. + * + * @return {@link Mapping}. + */ + + Mapping getMapping(); +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/handler/OptionsHandler.java b/api/src/main/java/com/yanzhenjie/andserver/framework/handler/OptionsHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..bb551407e7876ac53db0f48bfbbc35ece08f2167 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/handler/OptionsHandler.java @@ -0,0 +1,198 @@ +/* + * Copyright 2020 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.handler; + +import com.yanzhenjie.andserver.error.NotFoundException; +import com.yanzhenjie.andserver.framework.body.StringBody; +import com.yanzhenjie.andserver.framework.cross.CrossOrigin; +import com.yanzhenjie.andserver.framework.mapping.Addition; +import com.yanzhenjie.andserver.framework.mapping.Mapping; +import com.yanzhenjie.andserver.framework.view.BodyView; +import com.yanzhenjie.andserver.framework.view.View; +import com.yanzhenjie.andserver.http.HttpHeaders; +import com.yanzhenjie.andserver.http.HttpMethod; +import com.yanzhenjie.andserver.http.HttpRequest; +import com.yanzhenjie.andserver.http.HttpResponse; +import com.yanzhenjie.andserver.util.TextUtils; +import org.apache.httpcore.HttpStatus; + +import java.util.*; + +/** + * Created by Zhenjie Yan on 10/17/20. + */ +public class OptionsHandler implements MethodHandler { + + public static final String INVALID_CORS_REQUEST = "Invalid CORS request."; + + private List mMappings; + private Map mMappingMap; + + private Mapping mMapping; + private MethodHandler mHandler; + + public OptionsHandler(HttpRequest optionsRequest, List mappings, Map mappingMap) { + this.mMappings = mappings; + this.mMappingMap = mappingMap; + + mMapping = mMappings.get(0); + String requestMethod = optionsRequest.getHeader(HttpHeaders.Access_Control_Request_Method); + if (!requestMethod.isEmpty()) { + HttpMethod method = HttpMethod.reverse(requestMethod); + Mapping exactMapping = MappingAdapter.findMappingByMethod(mMappings, method); + if (exactMapping != null) { + mMapping = exactMapping; + } + } + + mHandler = (MethodHandler) mMappingMap.get(mMapping); + } + + + @Override + public Addition getAddition() { + return mHandler.getAddition(); + } + + + @Override + public CrossOrigin getCrossOrigin() { + return mHandler.getCrossOrigin(); + } + + + @Override + public Mapping getMapping() { + return mMapping; + } + + @Override + public View handle( HttpRequest request, HttpResponse response) throws Throwable { + String requestOrigin = request.getHeader(HttpHeaders.ORIGIN); + if (requestOrigin.isEmpty()) { + return invalidCORS(response); + } + + String requestMethodText = request.getHeader(HttpHeaders.Access_Control_Request_Method); + if (requestMethodText.isEmpty()) { + return invalidCORS(response); + } + + HttpMethod requestMethod = HttpMethod.reverse(requestMethodText); + Mapping mapping = MappingAdapter.findMappingByMethod(mMappings, requestMethod); + if (mapping == null) { + return invalidCORS(response); + } + + MethodHandler handler = (MethodHandler) mMappingMap.get(mapping); + if (handler == null) { + throw new NotFoundException(); + } + + CrossOrigin crossOrigin = handler.getCrossOrigin(); + if (crossOrigin == null) { + return invalidCORS(response); + } + + List allowMethods = new ArrayList<>(); + Collections.addAll(allowMethods, crossOrigin.getMethods()); + List mappingMethods = mapping.getMethod().getRuleList(); + if (allowMethods.isEmpty()) { + allowMethods.addAll(mappingMethods); + } + if (!allowMethods.contains(requestMethod)) { + return invalidCORS(response); + } + + List allowOrigins = Arrays.asList(crossOrigin.getOrigins()); + if (!allowOrigins.isEmpty() && !allowOrigins.contains("*") && !allowOrigins.contains(requestOrigin)) { + return invalidCORS(response); + } + + List allowedHeaders = Arrays.asList(crossOrigin.getAllowedHeaders()); + List outHeaders = new ArrayList<>(); + String headerHeadersText = request.getHeader(HttpHeaders.Access_Control_Request_Headers); + List requestHeaders = new ArrayList<>(); + if (!headerHeadersText.isEmpty()) { + StringTokenizer st = new StringTokenizer(headerHeadersText, ","); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + token = token.trim(); + if (token.length() > 0) { + requestHeaders.add(token); + } + } + } + if (allowedHeaders.contains("*")) { + if (requestHeaders.size() > 0) { + outHeaders.addAll(requestHeaders); + } + } else if (allowedHeaders.size() > 0) { + if (requestHeaders.size() > 0) { + for (String allowedHeader: allowedHeaders) { + for (String requestHeader: requestHeaders) { + if (allowedHeader.equalsIgnoreCase(requestHeader)) { + outHeaders.add(requestHeader); + } + } + } + if (outHeaders.isEmpty()) { + return invalidCORS(response); + } + } + } else if (requestHeaders.size() > 0) { + outHeaders.addAll(requestHeaders); + } + + String[] exposeHeaders = crossOrigin.getExposedHeaders(); + + response.setHeader(HttpHeaders.Access_Control_Allow_Origin, requestOrigin); + response.setHeader(HttpHeaders.Access_Control_Allow_Methods, TextUtils.join(", ", allowMethods)); + if (outHeaders.size() > 0) { + response.setHeader(HttpHeaders.Access_Control_Allow_Headers, TextUtils.join(", ", outHeaders)); + } + if (exposeHeaders.length > 0) { + response.setHeader(HttpHeaders.Access_Control_Expose_Headers, String.join(", ", exposeHeaders)); + } + + boolean credentials = crossOrigin.isAllowCredentials(); + response.setHeader(HttpHeaders.Access_Control_Allow_Credentials, Boolean.toString(credentials)); + + long maxAge = crossOrigin.getMaxAge(); + response.setHeader(HttpHeaders.Access_Control_Max_Age, Long.toString(maxAge)); + + response.setHeader(HttpHeaders.ALLOW, TextUtils.join(", ", HttpMethod.values())); + response.setHeader(HttpHeaders.VARY, HttpHeaders.ORIGIN); + + return new BodyView(new StringBody("OK")); + } + + private View invalidCORS(HttpResponse response) { + response.setStatus(HttpStatus.SC_FORBIDDEN); + response.setHeader(HttpHeaders.ALLOW, TextUtils.join(", ", HttpMethod.values())); + return new BodyView(new StringBody(INVALID_CORS_REQUEST)); + } + + @Override + public String getETag( HttpRequest request) throws Throwable { + return mHandler.getETag(request); + } + + @Override + public long getLastModified( HttpRequest request) throws Throwable { + return mHandler.getLastModified(request); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/handler/RequestHandler.java b/api/src/main/java/com/yanzhenjie/andserver/framework/handler/RequestHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..ead5ddce5aca8c53a49eefee9ebc12c461bb93b3 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/handler/RequestHandler.java @@ -0,0 +1,39 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.handler; + + +import com.yanzhenjie.andserver.framework.ETag; +import com.yanzhenjie.andserver.framework.LastModified; +import com.yanzhenjie.andserver.framework.view.View; +import com.yanzhenjie.andserver.http.HttpRequest; +import com.yanzhenjie.andserver.http.HttpResponse; + +/** + * Created by Zhenjie Yan on 2018/8/28. + */ +public interface RequestHandler extends ETag, LastModified { + + /** + * Use the given handler to handle this request. + * + * @param request current request. + * @param response current response. + * + * @return the impression sent to the client. + */ + View handle(HttpRequest request, HttpResponse response) throws Throwable; +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/mapping/Addition.java b/api/src/main/java/com/yanzhenjie/andserver/framework/mapping/Addition.java new file mode 100644 index 0000000000000000000000000000000000000000..750c37be09c4204c753d4082c2545b6f9a38c883 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/mapping/Addition.java @@ -0,0 +1,118 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.mapping; + + + +/** + * Created by Zhenjie Yan on 2018/9/9. + */ +public class Addition { + + private String[] stringType; + private boolean[] booleanType; + private int[] intType; + private long[] longType; + private short[] shortType; + private float[] floatType; + private double[] doubleType; + private byte[] byteType; + private char[] charType; + + public Addition() { + } + + + public String[] getStringType() { + return stringType; + } + + public void setStringType(String[] stringType) { + this.stringType = stringType; + } + + + public boolean[] getBooleanType() { + return booleanType; + } + + public void setBooleanType(boolean[] booleanType) { + this.booleanType = booleanType; + } + + + public int[] getIntType() { + return intType; + } + + public void setIntType(int[] intType) { + this.intType = intType; + } + + + public long[] getLongType() { + return longType; + } + + public void setLongType(long[] longType) { + this.longType = longType; + } + + + public short[] getShortType() { + return shortType; + } + + public void setShortType(short[] shortType) { + this.shortType = shortType; + } + + + public float[] getFloatType() { + return floatType; + } + + public void setFloatType(float[] floatType) { + this.floatType = floatType; + } + + + public double[] getDoubleType() { + return doubleType; + } + + public void setDoubleType(double[] doubleType) { + this.doubleType = doubleType; + } + + + public byte[] getByteType() { + return byteType; + } + + public void setByteType(byte[] byteType) { + this.byteType = byteType; + } + + + public char[] getCharType() { + return charType; + } + + public void setCharType(char[] charType) { + this.charType = charType; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/mapping/Mapping.java b/api/src/main/java/com/yanzhenjie/andserver/framework/mapping/Mapping.java new file mode 100644 index 0000000000000000000000000000000000000000..ca70981cfc2d807e6b5d78fead12165253b2ad33 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/mapping/Mapping.java @@ -0,0 +1,82 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.mapping; + +/** + *

Save the request mapping configuration.

+ * + * Created by Zhenjie Yan on 2018/6/13. + */ +public class Mapping { + + private Path mPath; + private Method mMethod; + private Pair mParam; + private Pair mHeader; + private Mime mConsume; + private Mime mProduce; + + public Mapping() { + } + + public Path getPath() { + return mPath; + } + + public void setPath(Path path) { + mPath = path; + } + + public Method getMethod() { + return mMethod; + } + + public void setMethod(Method method) { + mMethod = method; + } + + public Pair getParam() { + return mParam; + } + + public void setParam(Pair param) { + mParam = param; + } + + public Pair getHeader() { + return mHeader; + } + + public void setHeader(Pair header) { + mHeader = header; + } + + public Mime getConsume() { + return mConsume; + } + + public void setConsume(Mime consume) { + mConsume = consume; + } + + public Mime getProduce() { + return mProduce; + } + + public void setProduce(Mime produce) { + mProduce = produce; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/mapping/Method.java b/api/src/main/java/com/yanzhenjie/andserver/framework/mapping/Method.java new file mode 100644 index 0000000000000000000000000000000000000000..3c4f2daf15a9b620827e331c6ca8c782c73c1da7 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/mapping/Method.java @@ -0,0 +1,41 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.mapping; + +import com.yanzhenjie.andserver.http.HttpMethod; + +import java.util.LinkedList; +import java.util.List; + +/** + * Created by Zhenjie Yan on 2018/6/14. + */ +public class Method { + + private List mRuleList = new LinkedList<>(); + + public Method() { + } + + + public List getRuleList() { + return mRuleList; + } + + public void addRule( String ruleText) { + mRuleList.add(HttpMethod.reverse(ruleText)); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/mapping/Mime.java b/api/src/main/java/com/yanzhenjie/andserver/framework/mapping/Mime.java new file mode 100644 index 0000000000000000000000000000000000000000..8fd067bf4aecc931ce0048eb475e7dbf50e8dace --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/mapping/Mime.java @@ -0,0 +1,52 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.mapping; + + +import com.yanzhenjie.andserver.util.MediaType; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * Created by Zhenjie Yan on 2018/6/14. + */ +public class Mime { + + private List mRuleList = new LinkedList<>(); + + public Mime() { + } + + + public List getRuleList() { + return mRuleList; + } + + public void addRule( String ruleText) { + MediaType mimeType = MediaType.valueOf(ruleText); + Rule rule = new Rule(mimeType.getType(), mimeType.getSubtype(), mimeType.getParameters()); + mRuleList.add(rule); + } + + public static class Rule extends MediaType { + + public Rule(String type, String subtype, Map parameters) { + super(type, subtype, parameters); + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/mapping/Pair.java b/api/src/main/java/com/yanzhenjie/andserver/framework/mapping/Pair.java new file mode 100644 index 0000000000000000000000000000000000000000..717acbe71db14f2166f4f7ace6269412c06052e2 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/mapping/Pair.java @@ -0,0 +1,109 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.mapping; + + +import com.yanzhenjie.andserver.util.Patterns; + +import java.util.LinkedList; +import java.util.List; + +/** + * Created by Zhenjie Yan on 2018/6/14. + */ +public class Pair implements Patterns { + + private List mRuleList = new LinkedList<>(); + + public Pair() { + } + + + public List getRuleList() { + return mRuleList; + } + + public void addRule( String ruleText) { + if (ruleText.matches(PAIR_NO_VALUE)) { + String[] keyValue = ruleText.split("="); + Rule rule = new Rule(); + String key = keyValue[0]; + rule.setKey(key.substring(0, key.length() - 1)); + rule.setValue(keyValue[1]); + rule.setNoValue(true); + mRuleList.add(rule); + } else if (ruleText.matches(PAIR_KEY_VALUE)) { + String[] keyValue = ruleText.split("="); + + Rule rule = new Rule(); + rule.setKey(keyValue[0]); + rule.setValue(keyValue[1]); + mRuleList.add(rule); + } else if (ruleText.matches(PAIR_NO_KEY)) { + Rule rule = new Rule(); + rule.setKey(ruleText.substring(1)); + rule.setNoKey(true); + mRuleList.add(rule); + } else if (ruleText.matches(PAIR_KEY)) { + Rule rule = new Rule(); + rule.setKey(ruleText); + mRuleList.add(rule); + } + } + + public static class Rule { + + private String key; + private String value; + private boolean noKey; + private boolean noValue; + + public Rule() { + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public boolean isNoKey() { + return noKey; + } + + public void setNoKey(boolean noKey) { + this.noKey = noKey; + } + + public boolean isNoValue() { + return noValue; + } + + public void setNoValue(boolean noValue) { + this.noValue = noValue; + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/mapping/Path.java b/api/src/main/java/com/yanzhenjie/andserver/framework/mapping/Path.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa78e36a34ecfcfe9a29470d786d5a920d6c24b --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/mapping/Path.java @@ -0,0 +1,156 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.mapping; + + +import com.yanzhenjie.andserver.util.Patterns; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * Created by Zhenjie Yan on 2018/6/14. + */ +public class Path implements Patterns { + + private List mRuleList = new LinkedList<>(); + + public Path() { + } + + + public List getRuleList() { + return mRuleList; + } + + public void addRule( String ruleText) { + Rule rule = new Rule(); + rule.setSegments(pathToList(ruleText)); + mRuleList.add(rule); + } + + public static class Rule { + + private List mSegments; + + public Rule() { + } + + public List getSegments() { + return mSegments; + } + + public void setSegments(List segments) { + mSegments = segments; + } + } + + public static class Segment { + + private final String value; + private final boolean isBlurred; + + public Segment(String value, boolean isBlurred) { + this.value = value; + this.isBlurred = isBlurred; + } + + public String getValue() { + return value; + } + + public boolean isBlurred() { + return isBlurred; + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if(obj == null ){ + return false; + } + if(this == obj){ + return true ; + } + if (!(obj instanceof Segment)) { + return false; + } + return value.equals(((Segment) obj).value); + } + + @Override + public String toString() { + return value; + } + } + + + public static List pathToList( String path) { + List segmentList = new LinkedList<>(); + if (!path.isEmpty()) { + while (path.startsWith("/")) + path = path.substring(1); + while (path.endsWith("/")) + path = path.substring(0, path.length() - 1); + String[] pathArray = path.split("/"); + for (String segmentText: pathArray) { + Segment segment = new Segment(segmentText, segmentText.contains("{")); + segmentList.add(segment); + } + } + return Collections.unmodifiableList(segmentList); + } + + + public static String listToPath( List segments) { + StringBuilder builder = new StringBuilder(""); + if (segments.isEmpty()) { + builder.append("/"); + } + for (Segment segment: segments) { + builder.append("/").append(segment.getValue()); + } + return builder.toString(); + } + + public static boolean matches( String path1, String path2) { + if (path1.equals(path2)) { + return true; + } + + List segments1 = pathToList(path1); + List segments2 = pathToList(path2); + + if (segments1.size() != segments2.size()) { + return false; + } + + boolean matches = true; + for (int i = 0; i < segments1.size(); i++) { + Segment segment = segments1.get(i); + if (!segment.equals(segments2.get(i)) && !segment.isBlurred()) { + matches = false; + break; + } + } + return matches; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/view/BodyView.java b/api/src/main/java/com/yanzhenjie/andserver/framework/view/BodyView.java new file mode 100644 index 0000000000000000000000000000000000000000..21b109a0f6d81730439867190db1cff84cf66282 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/view/BodyView.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.view; + + +import com.yanzhenjie.andserver.http.ResponseBody; + +/** + * Created by Zhenjie Yan on 2018/9/7. + */ +public class BodyView implements View { + + private ResponseBody mBody; + + public BodyView( ResponseBody body) { + this.mBody = body; + } + + @Override + public boolean rest() { + return true; + } + + + @Override + public Object output() { + return mBody; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/view/ObjectView.java b/api/src/main/java/com/yanzhenjie/andserver/framework/view/ObjectView.java new file mode 100644 index 0000000000000000000000000000000000000000..e122d8ebd17eeb97e54c3d42d11e2b261daa9908 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/view/ObjectView.java @@ -0,0 +1,43 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.view; + + + +/** + * Created by Zhenjie Yan on 2018/9/9. + */ +public class ObjectView implements View { + + private final boolean isRest; + private final Object output; + + public ObjectView(boolean isRest, Object output) { + this.isRest = isRest; + this.output = output; + } + + @Override + public boolean rest() { + return isRest; + } + + + @Override + public Object output() { + return output; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/view/View.java b/api/src/main/java/com/yanzhenjie/andserver/framework/view/View.java new file mode 100644 index 0000000000000000000000000000000000000000..8cae4af6db571604d311bc673969fa5086c985d6 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/view/View.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.view; + + + +/** + * Created by Zhenjie Yan on 2018/8/29. + */ +public interface View { + + /** + * Is it a rest style view? + * + * @return true, otherwise is false. + */ + boolean rest(); + + /** + * Get the output. + * + * @return output, e.g. {@code "redirect:/user/list"}, {@code "forward:/user/list"}, {@code "/user/list"}, String, + * JSONObject, Object, Basic data type(int, short, long, double, float, byte, boolean char). + */ + + Object output(); +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/view/ViewResolver.java b/api/src/main/java/com/yanzhenjie/andserver/framework/view/ViewResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..c673d78b8ed944d0aff7fe75c38425fa586d72eb --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/view/ViewResolver.java @@ -0,0 +1,126 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.view; + + +import com.yanzhenjie.andserver.error.NotFoundException; +import com.yanzhenjie.andserver.error.ServerInternalException; +import com.yanzhenjie.andserver.framework.MessageConverter; +import com.yanzhenjie.andserver.framework.body.StringBody; +import com.yanzhenjie.andserver.http.*; +import com.yanzhenjie.andserver.util.MediaType; +import com.yanzhenjie.andserver.util.Patterns; + +/** + * Created by Zhenjie Yan on 2018/8/31. + */ +public class ViewResolver implements Patterns, StatusCode, HttpHeaders { + + private MessageConverter mConverter; + + public ViewResolver() { + } + + public ViewResolver(MessageConverter converter) { + this.mConverter = converter; + } + + /** + * Solve the view and convert the view to http package content. + * + * @param view current view. + * @param request current request. + * @param response current response. + */ + public void resolve(View view, HttpRequest request, HttpResponse response) { + if (view == null) { + return; + } + + Object output = view.output(); + + if (view.rest()) { + resolveRest(output, request, response); + } else { + resolvePath(output, request, response); + } + } + + private void resolveRest(Object output, HttpRequest request, HttpResponse response) { + if (output instanceof ResponseBody) { + response.setBody((ResponseBody) output); + } else if (mConverter != null) { + response.setBody(mConverter.convert(output, obtainProduce(request))); + } else if (output == null) { + response.setBody(new StringBody("")); + } else if (output instanceof String) { + response.setBody(new StringBody(output.toString(), obtainProduce(request))); + } else { + response.setBody(new StringBody(output.toString())); + } + } + + + private MediaType obtainProduce(HttpRequest request) { + final Object mtAttribute = request.getAttribute(HttpContext.RESPONSE_PRODUCE_TYPE); + if (mtAttribute instanceof MediaType) { + return (MediaType) mtAttribute; + } + return null; + } + + private void resolvePath(Object output, HttpRequest request, HttpResponse response) { + if (output instanceof CharSequence) { + final String action = output.toString(); + if (action.isEmpty()) { + return; + } + + // "redirect:(.)*" + if (action.matches(REDIRECT)) { + response.setStatus(SC_FOUND); + if (action.length() >= 9) { + final String path = action.substring(9); + response.setHeader(LOCATION, path); + } + } + // "forward:(.)*" + else if (action.matches(FORWARD)) { + final String path = action.substring(8); + RequestDispatcher dispatcher = request.getRequestDispatcher(path); + if (dispatcher != null) { + dispatcher.forward(request, response); + } else { + throw new NotFoundException(path); + } + } + // "/user/kevin" + else if (action.matches(PATH)) { + final String path = action + ".html"; + RequestDispatcher dispatcher = request.getRequestDispatcher(path); + if (dispatcher != null) { + dispatcher.forward(request, response); + } else { + throw new NotFoundException(path); + } + } else { + throw new NotFoundException(action); + } + } else { + throw new ServerInternalException(String.format("The return value of [%s] is not supported", output)); + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/website/AssetsWebsite.java b/api/src/main/java/com/yanzhenjie/andserver/framework/website/AssetsWebsite.java new file mode 100644 index 0000000000000000000000000000000000000000..9121a2456901d4b13df939793392808d6d17ff43 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/website/AssetsWebsite.java @@ -0,0 +1,263 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.website; + +import com.yanzhenjie.andserver.error.NotFoundException; +import com.yanzhenjie.andserver.framework.body.StreamBody; +import com.yanzhenjie.andserver.framework.body.StringBody; +import com.yanzhenjie.andserver.http.HttpRequest; +import com.yanzhenjie.andserver.http.HttpResponse; +import com.yanzhenjie.andserver.http.ResponseBody; +import com.yanzhenjie.andserver.util.*; +import ohos.app.Context; +import ohos.bundle.BundleInfo; +import ohos.bundle.IBundleManager; +import ohos.global.resource.Entry; +import ohos.global.resource.ResourceManager; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Created by Zhenjie Yan on 2018/9/7. + */ +public class AssetsWebsite extends BasicWebsite implements Patterns { + + private final AssetsReader mAssetsReader; + private final String mRootPath; + private final BundleInfo mPackageInfo; + + /** + * Create a website object. + * + * @param rootPath website root directory. + */ + public AssetsWebsite( Context context, String rootPath) { + this(context, rootPath, DEFAULT_INDEX); + } + + /** + * Create a website object. + * + * @param rootPath website root directory. + * @param indexFileName the default file name for each directory, e.g. index.html. + */ + public AssetsWebsite( Context context, String rootPath, String indexFileName) { + super(indexFileName); + Assert.isTrue(!rootPath.isEmpty(), "The rootPath cannot be empty."); + Assert.isTrue(!indexFileName.isEmpty(), "The indexFileName cannot be empty."); + + if (!rootPath.matches(PATH)) { + String message = "The format of [%s] is wrong, it should be like [/root/project] or [/root/project/]."; + String format = String.format(message, rootPath); + throw new IllegalArgumentException(format); + } + + this.mAssetsReader = new AssetsReader(context.getResourceManager()); + this.mRootPath = trimSlash(rootPath); + + IBundleManager packageManager = context.getBundleManager(); + try { + mPackageInfo = packageManager.getBundleInfo(context.getBundleName(), 0); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean intercept(HttpRequest request) { + String httpPath = request.getPath(); + InputStream stream = findPathSteam(httpPath); + IOUtils.closeQuietly(stream); + return stream != null; + } + + @Override + public String getETag(HttpRequest request) throws Throwable { + String httpPath = request.getPath(); + InputStream stream = findPathSteam(httpPath); + if (stream != null) { + try { + return DigestUtils.md5DigestAsHex(stream); + } finally { + IOUtils.closeQuietly(stream); + } + } + return null; + } + + @Override + public long getLastModified(HttpRequest request) throws Throwable { + String httpPath = request.getPath(); + InputStream stream = findPathSteam(httpPath); + IOUtils.closeQuietly(stream); + return stream != null ? mPackageInfo.getUpdateTime() : -1; + } + + + @Override + public ResponseBody getBody(HttpRequest request, HttpResponse response) throws IOException { + String httpPath = request.getPath(); + String objectPath = mRootPath + httpPath; + InputStream stream = mAssetsReader.getInputStream(objectPath); + if (stream != null) { + MediaType mediaType = MediaType.getFileMediaType(objectPath); + return new StreamBody(stream, stream.available(), mediaType); + } + + String indexPath = addEndSlash(objectPath) + getIndexFileName(); + InputStream indexStream = mAssetsReader.getInputStream(indexPath); + if (indexStream != null) { + if (!httpPath.endsWith(File.separator)) { + IOUtils.closeQuietly(indexStream); + String redirectPath = addEndSlash(httpPath); + String query = queryString(request); + response.sendRedirect(redirectPath + "?" + query); + return new StringBody(""); + } + + final MediaType mediaType = MediaType.getFileMediaType(indexPath); + return new StreamBody(indexStream, indexStream.available(), mediaType); + } + throw new NotFoundException(httpPath); + + } + + private InputStream findPathSteam(String httpPath) { + String targetPath = mRootPath + httpPath; + InputStream targetStream = mAssetsReader.getInputStream(targetPath); + if (targetStream != null) { + return targetStream; + } + + String indexPath = addEndSlash(targetPath) + getIndexFileName(); + InputStream indexStream = mAssetsReader.getInputStream(indexPath); + if (indexStream != null) { + return indexStream; + } + + return null; + } + + public static class AssetsReader { + + /** + * {@link ResourceManager}. + */ + private ResourceManager mAssetManager; + + /** + * Create {@link AssetsReader}. + * + * @param manager {@link ResourceManager}. + */ + public AssetsReader( ResourceManager manager) { + this.mAssetManager = manager; + } + + /** + * Get stream file. + * + * @param filePath assets in the absolute path. + * + * @return {@link InputStream} or null. + */ + + public InputStream getInputStream( String filePath) { + try { + return mAssetManager.getRawFileEntry(filePath).openRawFile(); + } catch (Throwable ignored) { + return null; + } + } + + /** + * Specify whether the destination is a file. + * + * @param fileName assets in the absolute path. + * + * @return true, other wise is false. + */ + public boolean isFile( String fileName) { + InputStream stream = null; + try { + stream = getInputStream(fileName); + return stream != null; + } finally { + IOUtils.closeQuietly(stream); + } + } + + /** + * Scanning subFolders and files under the specified path. + * + * @param path the specified path. + * + * @return String[] Array of strings, one for each asset. May be null. + */ + + public List list( String path) { + List fileList = new ArrayList<>(); + try { + Entry[] entries = mAssetManager.getRawFileEntry(path).getEntries(); + if(entries.length > 0){ + String[] files = new String[entries.length]; + for(int i = 0;i < entries.length;i++){ + files[i] = entries[i].getPath(); + } + Collections.addAll(fileList, files); + } + } catch (Throwable ignored) { + } + return fileList; + } + + /** + * Scan all files in the inPath. + * + * @param path path in the path. + * + * @return under inPath absolute path. + */ + + public List scanFile( String path) { + Assert.isTrue(!path.isEmpty(), "The path cannot be empty."); + + List pathList = new ArrayList<>(); + if (isFile(path)) { + pathList.add(path); + } else { + List files = list(path); + for (String file: files) { + String realPath = path + File.separator + file; + if (isFile(realPath)) { + pathList.add(realPath); + } else { + List childList = scanFile(realPath); + if (childList.size() > 0) { + pathList.addAll(childList); + } + } + } + } + return pathList; + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/website/BasicWebsite.java b/api/src/main/java/com/yanzhenjie/andserver/framework/website/BasicWebsite.java new file mode 100644 index 0000000000000000000000000000000000000000..90fb6a31b08649cc6098da104cd3463998b01262 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/website/BasicWebsite.java @@ -0,0 +1,163 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.website; + + + + +import com.yanzhenjie.andserver.http.HttpRequest; +import com.yanzhenjie.andserver.util.Assert; +import com.yanzhenjie.andserver.util.MultiValueMap; + +import java.io.File; +import java.util.List; +import java.util.Map; + +/** + * Created by Zhenjie Yan on 2018/9/6. + */ +public abstract class BasicWebsite extends Website { + + public static final String DEFAULT_INDEX = "index.html"; + + private final String mIndexFileName; + + public BasicWebsite() { + this(DEFAULT_INDEX); + } + + /** + * Create a website object. + * + * @param indexFileName the default file name for each directory, e.g. index.html. + */ + public BasicWebsite( String indexFileName) { + Assert.isTrue(!indexFileName.isEmpty(), "The indexFileName cannot be empty."); + this.mIndexFileName = indexFileName; + } + + @Override + public String getETag( HttpRequest request) throws Throwable { + return null; + } + + @Override + public long getLastModified( HttpRequest request) throws Throwable { + return -1; + } + + /** + * Get the name of the indexFile. + * + * @return file name, does not include the path. + */ + + protected final String getIndexFileName() { + return mIndexFileName; + } + + /** + * Add the '/' to the beginning. + * + * @param target target string. + * + * @return rule result. + */ + protected String addStartSlash( String target) { + if (!target.startsWith(File.separator)) { + target = File.separator + target; + } + return target; + } + + /** + * Add '/' at the ending. + * + * @param target target string. + * + * @return rule result. + */ + protected String addEndSlash( String target) { + if (!target.endsWith(File.separator)) { + target = target + File.separator; + } + return target; + } + + /** + * Remove '/' at the beginning. + * + * @param target target string. + * + * @return rule result. + */ + protected String trimStartSlash( String target) { + while (target.startsWith(File.separator)) + target = target.substring(1); + return target; + } + + /** + * Remove '/' at the ending. + * + * @param target target string. + * + * @return rule result. + */ + protected String trimEndSlash( String target) { + while (target.endsWith(File.separator)) + target = target.substring(0, target.length() - 1); + return target; + } + + /** + * Remove the '/' at the beginning and ending. + * + * @param target target string. + * + * @return rule result. + */ + protected String trimSlash( String target) { + target = trimStartSlash(target); + target = trimEndSlash(target); + return target; + } + + protected String queryString(HttpRequest request) { + MultiValueMap query = request.getQuery(); + if (query.isEmpty()) { + return ""; + } + + StringBuilder queryString = new StringBuilder(); + for (Map.Entry> entry: query.entrySet()) { + String key = entry.getKey(); + List values = entry.getValue(); + if (values != null && !values.isEmpty()) { + for (int i = 0; i < values.size(); i++) { + queryString.append("&") + .append(key) + .append("=") + .append(values.get(i)); + } + } + } + if (queryString.length() > 0) { + queryString.deleteCharAt(0); + } + return queryString.toString(); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/website/FileBrowser.java b/api/src/main/java/com/yanzhenjie/andserver/framework/website/FileBrowser.java new file mode 100644 index 0000000000000000000000000000000000000000..2457546e42506e3f2bc322d8b91bdf09bfd7a4b4 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/website/FileBrowser.java @@ -0,0 +1,155 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.website; + + +import com.yanzhenjie.andserver.error.NotFoundException; +import com.yanzhenjie.andserver.framework.body.FileBody; +import com.yanzhenjie.andserver.framework.body.StringBody; +import com.yanzhenjie.andserver.http.HttpRequest; +import com.yanzhenjie.andserver.http.HttpResponse; +import com.yanzhenjie.andserver.http.ResponseBody; +import com.yanzhenjie.andserver.util.*; +import org.apache.commons.io.Charsets; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +/** + * Created by Zhenjie Yan on 2018/9/7. + */ +public class FileBrowser extends BasicWebsite implements Patterns { + + private static final String FOLDER_HTML_PREFIX = " " + + "%1$s

%2$s

    "; + private static final String FOLDER_ITEM = "
  • %2$s
  • "; + private static final String FOLDER_HTML_SUFFIX = "
"; + + private final String mRootPath; + + public FileBrowser(String rootPath) { + Assert.isTrue(!rootPath.isEmpty(), "The rootPath cannot be empty."); + Assert.isTrue(rootPath.matches(PATH), "The format of [%s] is wrong, it should be like [/root/project]."); + this.mRootPath = rootPath; + } + + @Override + public boolean intercept(HttpRequest request) { + String httpPath = request.getPath(); + File file = findPathFile(httpPath); + return file != null; + } + + @Override + public String getETag(HttpRequest request) throws Throwable { + String httpPath = request.getPath(); + File file = findPathFile(httpPath); + if (file != null) { + String tag = file.getAbsolutePath() + file.lastModified(); + return DigestUtils.md5DigestAsHex(tag); + } + return null; + } + + @Override + public long getLastModified( HttpRequest request) throws Throwable { + String httpPath = request.getPath(); + File file = findPathFile(httpPath); + if (file != null) { + return file.lastModified(); + } + return -1; + } + + + @Override + public ResponseBody getBody(HttpRequest request, HttpResponse response) throws IOException { + String httpPath = request.getPath(); + File file = new File(mRootPath, httpPath); + if (!file.exists()) { + throw new NotFoundException(httpPath); + } + + if (file.isDirectory()) { + if (!httpPath.endsWith(File.separator)) { + String redirectPath = addEndSlash(httpPath); + response.sendRedirect(redirectPath); + return new StringBody(""); + } + + File tempFile = File.createTempFile("file_browser", ".html"); + OutputStream outputStream = new FileOutputStream(tempFile); + + String folderName = file.getName(); + String prefix = String.format(FOLDER_HTML_PREFIX, folderName, folderName); + outputStream.write(prefix.getBytes("utf-8")); + + File[] children = file.listFiles(); + if (children != null && children.length > 0) { + for (File child: children) { + String filePath = child.getAbsolutePath(); + int rootIndex = filePath.indexOf(mRootPath); + String subHttpPath = filePath.substring(rootIndex + mRootPath.length()); + subHttpPath = addStartSlash(subHttpPath); + String fileItem = String.format(FOLDER_ITEM, subHttpPath, child.getName()); + outputStream.write(fileItem.getBytes("utf-8")); + } + } + + outputStream.write(FOLDER_HTML_SUFFIX.getBytes("utf-8")); + IOUtils.closeQuietly(outputStream); + + return new FileBody(tempFile) { + + @Override + public MediaType contentType() { + MediaType mimeType = super.contentType(); + if (mimeType != null) { + mimeType = new MediaType(mimeType.getType(), mimeType.getSubtype(), StandardCharsets.UTF_8); + } + return mimeType; + } + }; + } else { + return new FileBody(file); + } + } + + /** + * Find the path specified resource. + * + * @param httpPath path. + * + * @return return if the file is found. + */ + private File findPathFile( String httpPath) { + File targetFile = new File(mRootPath, httpPath); + if (targetFile.exists()) { + return targetFile; + } + return null; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/website/StorageWebsite.java b/api/src/main/java/com/yanzhenjie/andserver/framework/website/StorageWebsite.java new file mode 100644 index 0000000000000000000000000000000000000000..d8072e6760cf4ce87e76df6db566efb8e669adbd --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/website/StorageWebsite.java @@ -0,0 +1,132 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.website; + +import com.yanzhenjie.andserver.error.NotFoundException; +import com.yanzhenjie.andserver.framework.body.FileBody; +import com.yanzhenjie.andserver.framework.body.StringBody; +import com.yanzhenjie.andserver.http.HttpRequest; +import com.yanzhenjie.andserver.http.HttpResponse; +import com.yanzhenjie.andserver.http.ResponseBody; +import com.yanzhenjie.andserver.util.Assert; +import com.yanzhenjie.andserver.util.DigestUtils; +import com.yanzhenjie.andserver.util.Patterns; +import com.yanzhenjie.andserver.util.StringUtils; + +import java.io.File; +import java.io.IOException; + +/** + * Created by Zhenjie Yan on 2018/9/7. + */ +public class StorageWebsite extends BasicWebsite implements Patterns { + + private final String mRootPath; + + /** + * Create a website object. + * + * @param rootPath website root directory. + */ + public StorageWebsite( String rootPath) { + this(rootPath, DEFAULT_INDEX); + } + + /** + * Create a website object. + * + * @param rootPath website root directory. + * @param indexFileName the default file name for each directory, e.g. index.html. + */ + public StorageWebsite( String rootPath, String indexFileName) { + super(indexFileName); + Assert.isTrue(!StringUtils.isEmpty(rootPath), "The rootPath cannot be empty."); + Assert.isTrue(rootPath.matches(PATH), "The format of [%s] is wrong, it should be like [/root/project]."); + + this.mRootPath = rootPath; + } + + @Override + public boolean intercept(HttpRequest request) { + String httpPath = request.getPath(); + File file = findPathFile(httpPath); + return file != null; + } + + @Override + public String getETag( HttpRequest request) throws Throwable { + String httpPath = request.getPath(); + File file = findPathFile(httpPath); + if (file != null) { + String tag = file.getAbsolutePath() + file.lastModified(); + return DigestUtils.md5DigestAsHex(tag); + } + return null; + } + + @Override + public long getLastModified( HttpRequest request) throws Throwable { + String httpPath = request.getPath(); + File file = findPathFile(httpPath); + if (file != null) { + return file.lastModified(); + } + return -1; + } + + + @Override + public ResponseBody getBody(HttpRequest request, HttpResponse response) throws IOException { + String httpPath = request.getPath(); + File targetFile = new File(mRootPath, httpPath); + if (targetFile.exists() && targetFile.isFile()) { + return new FileBody(targetFile); + } + + File indexFile = new File(targetFile, getIndexFileName()); + if (indexFile.exists() && indexFile.isFile()) { + if (!httpPath.endsWith(File.separator)) { + String redirectPath = addEndSlash(httpPath); + String query = queryString(request); + response.sendRedirect(redirectPath + "?" + query); + return new StringBody(""); + } + + return new FileBody(indexFile); + } + throw new NotFoundException(httpPath); + } + + /** + * Find the path specified resource. + * + * @param httpPath path. + * + * @return return if the file is found. + */ + private File findPathFile( String httpPath) { + File targetFile = new File(mRootPath, httpPath); + if (targetFile.exists() && targetFile.isFile()) { + return targetFile; + } + + File indexFile = new File(targetFile, getIndexFileName()); + if (indexFile.exists() && indexFile.isFile()) { + return indexFile; + } + return null; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/framework/website/Website.java b/api/src/main/java/com/yanzhenjie/andserver/framework/website/Website.java new file mode 100644 index 0000000000000000000000000000000000000000..36ace6f165ecc68964c7656b3963aee131a8ab3b --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/framework/website/Website.java @@ -0,0 +1,73 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.framework.website; + + + +import com.yanzhenjie.andserver.framework.ETag; +import com.yanzhenjie.andserver.framework.LastModified; +import com.yanzhenjie.andserver.framework.handler.HandlerAdapter; +import com.yanzhenjie.andserver.framework.handler.RequestHandler; +import com.yanzhenjie.andserver.framework.view.BodyView; +import com.yanzhenjie.andserver.framework.view.View; +import com.yanzhenjie.andserver.http.HttpRequest; +import com.yanzhenjie.andserver.http.HttpResponse; +import com.yanzhenjie.andserver.http.ResponseBody; + +import java.io.IOException; + +/** + * Created by Zhenjie Yan on 2018/9/4. + */ +public abstract class Website implements HandlerAdapter, ETag, LastModified { + + + @Override + public String getETag( HttpRequest request) throws Throwable { + return null; + } + + @Override + public long getLastModified( HttpRequest request) throws Throwable { + return 0; + } + + + @Override + public RequestHandler getHandler(HttpRequest request) { + return new RequestHandler() { + + @Override + public String getETag( HttpRequest request) throws Throwable { + return Website.this.getETag(request); + } + + @Override + public long getLastModified( HttpRequest request) throws Throwable { + return Website.this.getLastModified(request); + } + + @Override + public View handle(HttpRequest request, HttpResponse response) throws Throwable { + return new BodyView(getBody(request, response)); + } + }; + } + + + public abstract ResponseBody getBody(HttpRequest request, HttpResponse response) + throws IOException; +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/FAcceptLanguage.java b/api/src/main/java/com/yanzhenjie/andserver/http/FAcceptLanguage.java new file mode 100644 index 0000000000000000000000000000000000000000..f6dc642d967eff0942f8dfc93a9e186bc1ca02d7 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/FAcceptLanguage.java @@ -0,0 +1,72 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http; + +import com.yanzhenjie.andserver.util.StringUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * Created by Zhenjie Yan on 2018/8/7. + */ +public class FAcceptLanguage { + + private final Locale locale; + private final double quality; + + protected FAcceptLanguage(Locale locale, double quality) { + this.locale = locale; + this.quality = quality; + } + + public Locale getLocale() { + return locale; + } + + public double getQuality() { + return quality; + } + + public static List parse(String input) { + if (StringUtils.isEmpty(input)) { + return Collections.emptyList(); + } + + String[] segments = input.split(","); + if (segments.length == 0) { + return Collections.emptyList(); + } + + List list = new ArrayList<>(); + for (String segment: segments) { + String[] values = segment.split(";"); + if (values.length == 2 && values[1].length() > 2 && values[1].charAt(0) == 'q' && + values[1].charAt(1) == '=') { + String q = values[1].substring(2); + try { + list.add(new FAcceptLanguage(new Locale(values[1]), Double.parseDouble(q))); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + } + + return list; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/HttpContext.java b/api/src/main/java/com/yanzhenjie/andserver/http/HttpContext.java new file mode 100644 index 0000000000000000000000000000000000000000..a60f03b1d0a6834c087a68fb43ecffd9c44f7cdb --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/HttpContext.java @@ -0,0 +1,61 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http; + + + + +/** + * Created by Zhenjie Yan on 2018/8/31. + */ +public interface HttpContext { + + String RESPONSE_PRODUCE_TYPE = "http.response.Produce"; + + String REQUEST_CREATED_SESSION = "http.request.Session"; + + String HTTP_MESSAGE_CONVERTER = "http.message.converter"; + + String ANDROID_CONTEXT = "android.context"; + + /** + * Obtains attribute with the given name. + * + * @param id the attribute name. + * + * @return attribute value, or {@code null} if not set. + */ + + Object getAttribute(String id); + + /** + * Sets value of the attribute with the given name. + * + * @param id the attribute name. + * @param obj the attribute value. + */ + void setAttribute(String id, Object obj); + + /** + * Removes attribute with the given name from the context. + * + * @param id the attribute name. + * + * @return attribute value, or {@code null} if not set. + */ + + Object removeAttribute(String id); +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/HttpHeaders.java b/api/src/main/java/com/yanzhenjie/andserver/http/HttpHeaders.java new file mode 100644 index 0000000000000000000000000000000000000000..2944a5b625afdd15e533e41b47bd0dea7436463f --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/HttpHeaders.java @@ -0,0 +1,352 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http; + +/** + * Created by Zhenjie Yan on 2018/9/7. + */ +public interface HttpHeaders { + + /** + * RFC 2616 (HTTP/1.1) Section 14.1 + */ + String ACCEPT = "Accept"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.2 + */ + String ACCEPT_CHARSET = "Accept-Charset"; + + /** + * Access-Control-Allow-Credentials. + */ + String Access_Control_Allow_Credentials = "Access-Control-Allow-Credentials"; + + /** + * Access-Control-Allow-Headers. + */ + String Access_Control_Allow_Headers = "Access-Control-Allow-Headers"; + + /** + * Access-Control-Allow-Methods. + */ + String Access_Control_Allow_Methods = "Access-Control-Allow-Methods"; + + /** + * Access-Control-Allow-Origin. + */ + String Access_Control_Allow_Origin = "Access-Control-Allow-Origin"; + + /** + * Access-Control-Expose-Headers. + */ + String Access_Control_Expose_Headers = "Access-Control-Expose-Headers"; + + /** + * Access-Control-Max-Age. + */ + String Access_Control_Max_Age = "Access-Control-Max-Age"; + + /** + * Access-Control-Request-Headers. + */ + String Access_Control_Request_Headers = "Access-Control-Request-Headers"; + + /** + * Access-Control-Request-Method. + */ + String Access_Control_Request_Method = "Access-Control-Request-Method"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.3 + */ + String ACCEPT_ENCODING = "Accept-Encoding"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.4 + */ + String ACCEPT_LANGUAGE = "Accept-Language"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.5 + */ + String ACCEPT_RANGES = "Accept-Ranges"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.6 + */ + String AGE = "Age"; + + /** + * RFC 1945 (HTTP/1.0) Section 10.1, RFC 2616 (HTTP/1.1) Section 14.7 + */ + String ALLOW = "Allow"; + + /** + * RFC 1945 (HTTP/1.0) Section 10.2, RFC 2616 (HTTP/1.1) Section 14.8 + */ + String AUTHORIZATION = "Authorization"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.9 + */ + String CACHE_CONTROL = "Cache-Control"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.10 + */ + String CONNECTION = "Connection"; + + /** + * RFC 1945 (HTTP/1.0) Section 10.3, RFC 2616 (HTTP/1.1) Section 14.11 + */ + String CONTENT_ENCODING = "Content-Encoding"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.12 + */ + String CONTENT_LANGUAGE = "Content-Language"; + + /** + * RFC 1945 (HTTP/1.0) Section 10.4, RFC 2616 (HTTP/1.1) Section 14.13 + */ + String CONTENT_LENGTH = "Content-Length"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.14 + */ + String CONTENT_LOCATION = "Content-Location"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.15 + */ + String CONTENT_MD5 = "Content-MD5"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.16 + */ + String CONTENT_RANGE = "Content-Range"; + + /** + * RFC 1945 (HTTP/1.0) Section 10.5, RFC 2616 (HTTP/1.1) Section 14.17 + */ + String CONTENT_TYPE = "Content-Type"; + + /** + * RFC 6265 (HTTP/1.0) Section 4.2 + */ + String COOKIE = "Cookie"; + + /** + * RFC 1945 (HTTP/1.0) Section 10.6, RFC 2616 (HTTP/1.1) Section 14.18 + */ + String DATE = "Date"; + + /** + * RFC 2518 (WevDAV) Section 9.1 + */ + String DAV = "Dav"; + + /** + * RFC 2518 (WevDAV) Section 9.2 + */ + String DEPTH = "Depth"; + + /** + * RFC 2518 (WevDAV) Section 9.3 + */ + String DESTINATION = "Destination"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.19 + */ + String ETAG = "ETag"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.20 + */ + String EXPECT = "Expect"; + + /** + * RFC 1945 (HTTP/1.0) Section 10.7, RFC 2616 (HTTP/1.1) Section 14.21 + */ + String EXPIRES = "Expires"; + + /** + * RFC 1945 (HTTP/1.0) Section 10.8, RFC 2616 (HTTP/1.1) Section 14.22 + */ + String FROM = "From"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.23 + */ + String HOST = "Host"; + + /** + * RFC 2518 (WevDAV) Section 9.4 + */ + String IF = "If"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.24 + */ + String IF_MATCH = "If-Match"; + + /** + * RFC 1945 (HTTP/1.0) Section 10.9, RFC 2616 (HTTP/1.1) Section 14.25 + */ + String IF_MODIFIED_SINCE = "If-Modified-Since"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.26 + */ + String IF_NONE_MATCH = "If-None-Match"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.27 + */ + String IF_RANGE = "If-Range"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.28 + */ + String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; + + /** + * RFC 1945 (HTTP/1.0) Section 10.10, RFC 2616 (HTTP/1.1) Section 14.29 + */ + String LAST_MODIFIED = "Last-Modified"; + + /** + * RFC 1945 (HTTP/1.0) Section 10.11, RFC 2616 (HTTP/1.1) Section 14.30 + */ + String LOCATION = "Location"; + + /** + * RFC 2518 (WevDAV) Section 9.5 + */ + String LOCK_TOKEN = "Lock-Token"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.31 + */ + String MAX_FORWARDS = "Max-Forwards"; + + /** + * Origin. + */ + String ORIGIN = "Origin"; + + /** + * RFC 2518 (WevDAV) Section 9.6 + */ + String OVERWRITE = "Overwrite"; + + /** + * RFC 1945 (HTTP/1.0) Section 10.12, RFC 2616 (HTTP/1.1) Section 14.32 + */ + String PRAGMA = "Pragma"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.33 + */ + String PROXY_AUTHENTICATE = "Proxy-Authenticate"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.34 + */ + String PROXY_AUTHORIZATION = "Proxy-Authorization"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.35 + */ + String RANGE = "Range"; + + /** + * RFC 1945 (HTTP/1.0) Section 10.13, RFC 2616 (HTTP/1.1) Section 14.36 + */ + String REFERER = "Referer"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.37 + */ + String RETRY_AFTER = "Retry-After"; + + /** + * RFC 1945 (HTTP/1.0) Section 10.14, RFC 2616 (HTTP/1.1) Section 14.38 + */ + String SERVER = "Server"; + + /** + * RFC 6265 (HTTP/1.0) Section 4.1, RFC 2109 (Http/1.1) Section 4.2.2 + */ + String SET_COOKIE = "Set-Cookie"; + + /** + * RFC 2518 (WevDAV) Section 9.7 + */ + String STATUS_URI = "Status-URI"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.39 + */ + String TE = "TE"; + + /** + * RFC 2518 (WevDAV) Section 9.8 + */ + String TIMEOUT = "Timeout"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.40 + */ + String TRAILER = "Trailer"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.41 + */ + String TRANSFER_ENCODING = "Transfer-Encoding"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.42 + */ + String UPGRADE = "Upgrade"; + + /** + * RFC 1945 (HTTP/1.0) Section 10.15, RFC 2616 (HTTP/1.1) Section 14.43 + */ + String USER_AGENT = "User-Agent"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.44 + */ + String VARY = "Vary"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.45 + */ + String VIA = "Via"; + + /** + * RFC 2616 (HTTP/1.1) Section 14.46 + */ + String WARNING = "Warning"; + + /** + * RFC 1945 (HTTP/1.0) Section 10.16, RFC 2616 (HTTP/1.1) Section 14.47 + */ + String WWW_AUTHENTICATE = "WWW-Authenticate"; +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/HttpMethod.java b/api/src/main/java/com/yanzhenjie/andserver/http/HttpMethod.java new file mode 100644 index 0000000000000000000000000000000000000000..0000f7dc1bccccbd5f3a47ff8837b01cab107d24 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/HttpMethod.java @@ -0,0 +1,106 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http; + +import java.util.Locale; + +/** + * Created by Zhenjie Yan on 2018/8/29. + */ +public enum HttpMethod { + + GET("GET"), + HEAD("HEAD"), + POST("POST"), + PUT("PUT"), + PATCH("PATCH"), + DELETE("DELETE"), + OPTIONS("OPTIONS"), + TRACE("TRACE"); + + private String value; + + HttpMethod(String value) { + this.value = value; + } + + public String value() { + return value; + } + + /** + * Whether to allow the body to be transmitted. + * + * @return true, otherwise is false. + */ + public boolean allowBody() { + switch (this) { + case POST: + case PUT: + case PATCH: + case DELETE: + return true; + default: + return false; + } + } + + /** + * Reverse the text for the request value. + * + * @param method value text, such as: GET, POST. + * + * @return {@link HttpMethod}. + */ + public static HttpMethod reverse(String method) { + method = method.toUpperCase(Locale.ENGLISH); + switch (method) { + case "GET": { + return GET; + } + case "HEAD": { + return HEAD; + } + case "POST": { + return POST; + } + case "PUT": { + return PUT; + } + case "PATCH": { + return PATCH; + } + case "DELETE": { + return DELETE; + } + case "OPTIONS": { + return OPTIONS; + } + case "TRACE": { + return TRACE; + } + default: { + String message = String.format("The value %1$s is not supported.", method); + throw new UnsupportedOperationException(message); + } + } + } + + @Override + public String toString() { + return value; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/HttpRequest.java b/api/src/main/java/com/yanzhenjie/andserver/http/HttpRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..978243fd092b12c35f0e2bdc7c1c556f75618d84 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/HttpRequest.java @@ -0,0 +1,351 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http; + +import com.yanzhenjie.andserver.http.cookie.Cookie; +import com.yanzhenjie.andserver.http.session.Session; +import com.yanzhenjie.andserver.util.MediaType; +import com.yanzhenjie.andserver.util.MultiValueMap; + +import java.util.Date; +import java.util.List; +import java.util.Locale; + +/** + * Created by Zhenjie Yan on 2018/6/12. + */ +public interface HttpRequest extends HttpContext, HttpHeaders { + + String SESSION_NAME = "ASESSIONID"; + + /** + * Returns {@link HttpMethod} with which this request was made. + */ + + HttpMethod getMethod(); + + /** + * Returns the part of this request's URL from the protocol name up to the query string in the first line of the HTTP + * request. The web container does not decode this String. + * + *

E.g. + *
First line of HTTP requestReturned Value
POST /some/path.html + * HTTP/1.1/some/path.html
GET http://foo.bar/a.html HTTP/1.0/a.html
HEAD /xyz?a=b HTTP/1.1/xyz
+ */ + + String getURI(); + + /** + * Get the path to this request. + */ + + String getPath(); + + /** + * Returns an {@link List} of {@code String} objects containing the names of the query parameters contained in url of + * this request, or empty {@link List} if there are no query parameters. + * + * @return an {@link List} of {@code String}. + */ + + List getQueryNames(); + + /** + * Returns the value of a request parameter as a {@code String} object, or {@code null} if the parameter does not + * exist. + * + *

You should only use this method when you are sure the query parameter has only one value. If the query + * parameter might have more than one value, use {@link #getQueries(String)}. + * + * @param name a {@code String} specifying the name of the query parameter. + * + * @return the single value of the query parameter. + * + * @see #getQueries(String) + */ + + String getQuery(String name); + + /** + * Returns an {@link List} of {@code String} objects containing all of the values the given query parameter name, or + * empty {@link List} if the query parameter does not exist. + * + * @param name the name of the query parameter. + * + * @return an {@link List} of {@code String} containing the parameter's values. + * + * @see #getQuery(String) + */ + + List getQueries(String name); + + /** + * Returns {@link MultiValueMap} of the query parameters of this request, or empty {@link MultiValueMap} if there are no + * query parameters. + * + * @return a {@link MultiValueMap} containing query parameter names as keys and query parameter values as map values. + */ + + MultiValueMap getQuery(); + + /** + * Returns an {@link List} of all the header names this request contains, or empty {@link List} if the request has no + * headers. + */ + + List getHeaderNames(); + + /** + * Returns the value of the specified request header as a {@code String}. If the request did not include a header of the + * specified name, this method returns {@code null}. If there are multiple headers with the same name, this method + * returns the first head in the request. + * + * @param name a {@code String} specifying the header name. + * + * @see #getHeaders(String) + */ + + String getHeader(String name); + + /** + * Returns all the values of the specified request header as an {@link List} of {@code String}. + * + *

Some headers, such as {@code Accept-Language} can be sent by clients as several headers each with a different + * value rather than sending the header as a comma separated list. + * + *

If the request did not include any headers of the specified name, this method returns an empty {@link List}. + * + * @param name a {@code String} specifying the header name. + */ + + List getHeaders(String name); + + /** + * Returns the value of the specified request header as a {@code long} value that represents a {@link Date} object. Use + * this method with headers that contain dates, e.g. {@code If-Modified-Since}. + * + *

The date is returned as the number of milliseconds since January 1, 1970 GMT. The header name is case + * insensitive. + * + *

If the request did not have a header of the specified name, this method returns -1. If the header can't be + * converted to a date, the method throws an {@link IllegalStateException}. + * + * @param name a {@code String} specifying the name of the header. + */ + long getDateHeader(String name); + + /** + * Returns the value of the specified request header as an {@code int}. If the request does not have a header of the + * specified name, this method returns -1. If the header cannot be converted to an integer, this method throws a {@link + * IllegalStateException}. + * + * @param name a {@code String} specifying the name of a request header. + */ + int getIntHeader(String name); + + /** + * Returns the preferred {@link MediaType} that the client will accept content in, based on the Accept header. + */ + + MediaType getAccept(); + + /** + * Returns a {@link List} of {@link MediaType} objects indicating, in decreasing order starting with the preferred + * MediaType, the media types that are acceptable to the client based on the {@code Accept} header. If the client + * request doesn't provide an {@code Accept} header, this method returns a empty {@link List}. + * + * @return a {@link List} of {@link MediaType} objects indicating. + */ + + List getAccepts(); + + /** + * Returns the preferred {@link Locale} that the client will accept content in, based on the {@code Accept-Language} + * header. If the client request doesn't provide an {@code Accept-Language} header, this method returns the default + * locale for the server. + * + * @return the preferred {@link Locale} for the client. + */ + + Locale getAcceptLanguage(); + + /** + * Returns a {@link List} of {@link Locale} objects indicating, in decreasing order starting with the preferred {@link + * Locale}, the locales that are acceptable to the client based on the {@code Accept-Language} header. If the client + * request doesn't provide an {@code Accept-Language} header, this method returns a {@link List} containing one {@link + * Locale}, the default locale for the server. + * + * @return an {@link List} of preferred {@link Locale} objects for the client. + */ + + List getAcceptLanguages(); + + /** + * Gets the value of the cookie with the specified name. + * + * @param name the name of value. + * + * @return the value or null if the cookie with the specified name does not exist. + */ + + String getCookieValue(String name); + + /** + * Return a {@link Cookie} object, if there is no cookie corresponding to this name, it returns {@code null}. + * + * @param name cookie name. + * + * @return a {@link Cookie} object or null if there is no cookie corresponding to this name . + */ + + Cookie getCookie(String name); + + /** + * Returns an {@link List} containing all of the {@link Cookie} objects the client sent with this request. This method + * returns {@code null} if no cookies were sent. + */ + + List getCookies(); + + /** + * Returns the length, in bytes, of the request body and made available by the input stream, or -1 if the length is not + * known. + * + * @return a long containing the length of the request body or -1L if the length is not known. + */ + long getContentLength(); + + /** + * Returns the MIME type of the body of the request, or {@code null} if the type is not known. + * + * @return a {@code String} containing the name of the MIME type of the request, or null if the type is not known. + */ + + MediaType getContentType(); + + /** + * Returns an {@link List} of {@code String} containing the names of the parameters contained in this request. If the + * request has no parameters, the method returns an empty {@link List}. + * + * @return an {@link List} of {@code String} objects. + */ + + List getParameterNames(); + + /** + * Returns the value of a request parameter as a {@code String}, or {@code null} if the parameter does not exist. + * + *

You should only use this method when you are sure the parameter has only one value. If the parameter might + * have more than one value, use {@link #getParameters(String)}. + * + * @param name a {@code String} specifying the name of the parameter. + * + * @return the single value of the parameter. + * + * @see #getParameters(String) + */ + + String getParameter(String name); + + /** + * Returns an array of {@code String} objects containing all of the values the given request parameter has, or {@code + * null} if the parameter does not exist. + * + * @param name a {@code String} containing the name of the parameter whose value is requested. + * + * @return an array of {@code String} objects containing the parameter's values. + * + * @see #getParameter(String) + */ + + List getParameters(String name); + + /** + * Returns {@link MultiValueMap} of the parameters of this request, or empty {@link MultiValueMap} if there are no + * parameters. + * + * @return a {@link MultiValueMap} containing parameter names as keys and parameter values as map values. + */ + + MultiValueMap getParameter(); + + /** + * Get the response body. + * + * @return {@link RequestBody}. + * + * @throws UnsupportedOperationException if the request method does not allow the body to be sent. + */ + + RequestBody getBody(); + + /** + * Returns the current {@code Session} associated with this request or, if there is no current session returns a new + * session. + * + * @return the session associated with this request or a new session. + * + * @see #getSession() + */ + + Session getValidSession(); + + /** + * Returns the current session associated with this request, or if the request does not have a session, creates one. + * + * @see #getValidSession() + */ + + Session getSession(); + + /** + * Change the session id of the current session associated with this request and return the new session id, if there is + * no session associated with the request, an {@link IllegalStateException} is thrown. + * + * @return the new session id. + * + * @see #getSession() + * @see #getValidSession() + */ + + String changeSessionId(); + + /** + * Checks whether the requested session ID is still valid. + * + *

If the client did not specify any session ID, this method returns {@code false}. + * + * @see #getSession() + * @see #getValidSession() + */ + boolean isSessionValid(); + + /** + * Returns a {@link RequestDispatcher} object that acts as a wrapper for the resource located at the given path. + * + * @return a {@link RequestDispatcher} object or null if the handler corresponding to the path is not + * found. + */ + + RequestDispatcher getRequestDispatcher(String path); + + /** + * Get the http context of this request. + * + * @return {@link HttpContext}. + */ + HttpContext getContext(); +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/HttpResponse.java b/api/src/main/java/com/yanzhenjie/andserver/http/HttpResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..9074c6d0d56dc16e059463c5d2e1a07ab94eec2c --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/HttpResponse.java @@ -0,0 +1,158 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http; + + +import com.yanzhenjie.andserver.http.cookie.Cookie; + +import java.util.Enumeration; +import java.util.List; + +/** + * Created by Zhenjie Yan on 2018/6/12. + */ +public interface HttpResponse extends StatusCode, HttpHeaders { + + /** + * Sets the status code for this response. + * + *

This method preserves any cookies and other response headers. + * + *

Valid status codes are those in the 2XX, 3XX, 4XX, and 5XX ranges. Other status codes will be considered to be + * specific. + */ + void setStatus(int sc); + + /** + * Gets the current status code of this response. + * + * @return status code. + */ + int getStatus(); + + /** + * Sets a response header with the given name and value. If the header had already been set, the new value overwrites + * the previous one. The {@link #containsHeader(String)} method can be used to test for the presence of a header before + * setting its value. + * + * @see #containsHeader(String) + * @see #addHeader(String, String) + */ + void setHeader(String name, String value); + + /** + * Adds a response header with the given name and value. This method allows response headers to have multiple values. + * + * @see #setHeader(String, String) + */ + void addHeader(String name, String value); + + /** + * Gets the value of the response header with the given name. + * + *

If a response header with the given name exists and contains multiple values, the value that was added first + * will be returned. + * + * @param name the name of the response header. + * + * @return the value of the response header with the given name, or {@code null} if no header with the given name has + * been set on this response. + */ + + String getHeader(String name); + + /** + * Sets a response header with the given name and date-value. The date is specified in terms of milliseconds since the + * epoch. + * + *

If the header had already been set, the new value overwrites the previous one. The {@link + * #containsHeader(String)} method can be used to test for the presence of a header before setting its value. + * + * @see #containsHeader(String) + * @see #addDateHeader(String, long) + */ + void setDateHeader(String name, long date); + + /** + * Adds a response header with the given name and date-value. The date is specified in terms of milliseconds since the + * epoch. This method allows response headers to have multiple values. + * + * @see #setDateHeader(String, long) + */ + void addDateHeader(String name, long date); + + /** + * Sets a response header with the given name and integer value. If the header had already been set, the new value + * overwrites the previous one. The {@link #containsHeader(String)} method can be used to test for the presence of a + * header before setting its value. + * + * @see #containsHeader(String) + * @see #addIntHeader(String, int) + */ + void setIntHeader(String name, int value); + + /** + * Adds a response header with the given name and integer value. This method allows response headers to have multiple + * values. + * + * @see #setIntHeader(String, int) + */ + void addIntHeader(String name, int value); + + /** + * Returns a boolean indicating whether the named response header has already been set. + */ + boolean containsHeader(String name); + + /** + * Gets the values of the response header with the given name. + * + * @param name the name of the response header. + * + * @return a {@link List} of the values of the response header with the given name. + */ + + List getHeaders(String name); + + /** + * Gets the names of the headers of this response. + * + * @return a {@link Enumeration} of the names of the headers of this response. + */ + + List getHeaderNames(); + + /** + * Adds the specified cookie to the response. This method can be called multiple times to set more than one cookie. + * + * @param cookie the cookie to return to the client. + */ + void addCookie(Cookie cookie); + + /** + * Sends a temporary redirect response to the client using the specified redirect location URL and clears the buffer. + * The buffer will be replaced with the data set by this method. Calling this method sets the status code to {@link + * StatusCode#SC_FOUND}. + */ + void sendRedirect(String location); + + /** + * Set the response body. + * + * @param body write the message content sent to the client. + */ + void setBody(ResponseBody body); +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/Modified.java b/api/src/main/java/com/yanzhenjie/andserver/http/Modified.java new file mode 100644 index 0000000000000000000000000000000000000000..4cb9ced59c97ff7af90dd0149024c5e56e59ab5f --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/Modified.java @@ -0,0 +1,206 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http; + +import com.yanzhenjie.andserver.util.StringUtils; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.yanzhenjie.andserver.util.HttpDateFormat.parseDate; + +/** + * Created by Zhenjie Yan on 2018/8/29. + */ +public class Modified implements HttpHeaders { + + private static final Pattern ETAG_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?"); + + private HttpRequest mRequest; + private HttpResponse mResponse; + + private boolean isNotModified; + + public Modified( HttpRequest request, HttpResponse response) { + this.mRequest = request; + this.mResponse = response; + } + + /** + * Process {@code Modified} according to given the supplied {@code Last-Modified}. + * + * @param lastModified the last-modified timestamp in milliseconds of the resource. + * + * @return true if the request does not require further processing. + */ + public boolean process(long lastModified) { + return process(null, lastModified); + } + + /** + * Process {@code Modified} according to given the supplied {@code ETag}. + * + * @param eTag the tag of the resource. + * + * @return true if the request does not require further processing. + */ + public boolean process(String eTag) { + return process(eTag, -1); + } + + /** + * Process {@code Modified} according to given the supplied {@code Last-Modified} and {@code ETag}. + * + * @param eTag the tag of the resource. + * @param lastModified he last-modified timestamp in milliseconds of the resource. + * + * @return true if the request does not require further processing. + */ + public boolean process(String eTag, long lastModified) { + if (isNotModified) { + return true; + } + + // See https://tools.ietf.org/html/rfc7232#section-6 + if (validateIfUnmodifiedSince(lastModified)) { + if (!isNotModified) { + mResponse.setStatus(StatusCode.SC_LENGTH_REQUIRED); + } + return isNotModified; + } + + // First, prioritized. + boolean validated = validateIfNoneMatch(eTag); + // Second. + if (!validated) { + validateIfModifiedSince(lastModified); + } + + // Update response + HttpMethod method = mRequest.getMethod(); + boolean isGetHead = (method == HttpMethod.GET || method == HttpMethod.HEAD); + if (isNotModified) { + mResponse.setStatus(isGetHead ? StatusCode.SC_NOT_MODIFIED : StatusCode.SC_LENGTH_REQUIRED); + } + if (isGetHead) { + if (lastModified > 0 && mResponse.getHeader(LAST_MODIFIED) == null) { + mResponse.setDateHeader(LAST_MODIFIED, lastModified); + } + if (!StringUtils.isEmpty(eTag) && mResponse.getHeader(ETAG) == null) { + mResponse.setHeader(ETAG, padETagIfNecessary(eTag)); + } + mResponse.setHeader(CACHE_CONTROL, "private"); + } + return isNotModified; + } + + private boolean validateIfNoneMatch(String eTag) { + if (StringUtils.isEmpty(eTag)) { + return false; + } + + List ifNoneMatch = mRequest.getHeaders(IF_NONE_MATCH); + if (ifNoneMatch.isEmpty()) { + return false; + } + + // We will perform this validation... + eTag = padETagIfNecessary(eTag); + for (String clientETags: ifNoneMatch) { + Matcher eTagMatcher = ETAG_PATTERN.matcher(clientETags); + // Compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3 + while (eTagMatcher.find()) { + if (!StringUtils.isEmpty(eTagMatcher.group()) && + eTag.replaceFirst("^W/", "").equals(eTagMatcher.group(3))) { + isNotModified = true; + break; + } + } + } + return true; + } + + private String padETagIfNecessary(String eTag) { + if (StringUtils.isEmpty(eTag)) { + return eTag; + } + if ((eTag.startsWith("\"") || eTag.startsWith("W/\"")) && eTag.endsWith("\"")) { + return eTag; + } + return "\"" + eTag + "\""; + } + + private boolean validateIfModifiedSince(long lastModifiedTimestamp) { + if (lastModifiedTimestamp < 0) { + return false; + } + + long ifModifiedSince = parseDateHeader(IF_MODIFIED_SINCE); + if (ifModifiedSince == -1) { + return false; + } + + // We will perform this validation... + isNotModified = ifModifiedSince >= lastModifiedTimestamp; + return true; + } + + private boolean validateIfUnmodifiedSince(long lastModifiedTimestamp) { + if (lastModifiedTimestamp < 0) { + return false; + } + long ifUnmodifiedSince = parseDateHeader(IF_UNMODIFIED_SINCE); + if (ifUnmodifiedSince == -1) { + return false; + } + // We will perform this validation... + isNotModified = ifUnmodifiedSince >= lastModifiedTimestamp; + return true; + } + + private long parseDateHeader(String headerName) { + long dateValue = -1; + try { + dateValue = mRequest.getDateHeader(headerName); + } catch (IllegalStateException ex) { + String headerValue = mRequest.getHeader(headerName); + if (StringUtils.isEmpty(headerValue)) { + return -1; + } + // Possibly an IE 10 style value: "Wed, 09 Apr 2014 09:57:42 GMT; length=13774" + int separatorIndex = headerValue.indexOf(';'); + if (separatorIndex != -1) { + String datePart = headerValue.substring(0, separatorIndex); + dateValue = parseDateValue(datePart); + } + } + return dateValue; + } + + private long parseDateValue(String headerValue) { + if (headerValue == null) { + return -1; + } + + if (headerValue.length() >= 3) { + // Short "0" or "-1" like values are never valid HTTP date headers... + // Let's only bother with SimpleDateFormat parsing for long enough values. + return parseDate(headerValue); + } + return -1; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/RequestBody.java b/api/src/main/java/com/yanzhenjie/andserver/http/RequestBody.java new file mode 100644 index 0000000000000000000000000000000000000000..20dc7185004608d45ed6f5b52ba1e0ae6b30b71b --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/RequestBody.java @@ -0,0 +1,70 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http; + + +import com.yanzhenjie.andserver.util.MediaType; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Created by Zhenjie Yan on 2018/8/9. + */ +public interface RequestBody { + + /** + * Retrieve the character encoding for the request. + * + * @return the character encoding for the request. + */ + String contentEncoding(); + + /** + * Get the {@code Content-Length} of the message body, if the length is unknown, return a negative value. + * + * @return message length. + */ + long length(); + + /** + * Get the {@code Content-Type} of the message body, including charset. + * + * @return e.g. {@code application/json; charset=utf-8}, or {@code null} if the content type is unknown. + */ + + MediaType contentType(); + + /** + * Returns a content stream of this body. + * + * @return content stream of this body. + * + * @throws IOException if the stream could not be created. + */ + + InputStream stream() throws IOException; + + /** + * Convert the request body to a String. + * + * @return string. + * + * @throws IOException if the stream could not be created. + */ + + String string() throws IOException; +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/RequestDispatcher.java b/api/src/main/java/com/yanzhenjie/andserver/http/RequestDispatcher.java new file mode 100644 index 0000000000000000000000000000000000000000..04c2dd51cd568b346faa2c34c6341fc3bcc6fd61 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/RequestDispatcher.java @@ -0,0 +1,32 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http; + + + +/** + * Created by Zhenjie Yan on 2018/8/31. + */ +public interface RequestDispatcher { + + /** + * Forwards a request from a handler to another handler on the server. + * + * @param request the current request. + * @param response the current response. + */ + void forward(HttpRequest request, HttpResponse response); +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/RequestWrapper.java b/api/src/main/java/com/yanzhenjie/andserver/http/RequestWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..e05297969f71ece54cbc2ec95f83868d3f46e0c3 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/RequestWrapper.java @@ -0,0 +1,250 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http; + + +import com.yanzhenjie.andserver.http.cookie.Cookie; +import com.yanzhenjie.andserver.http.session.Session; +import com.yanzhenjie.andserver.util.MediaType; +import com.yanzhenjie.andserver.util.MultiValueMap; + +import java.util.List; +import java.util.Locale; + +/** + * Created by Zhenjie Yan on 2018/9/6. + */ +public class RequestWrapper implements HttpRequest { + + private HttpRequest mRequest; + + public RequestWrapper(HttpRequest request) { + this.mRequest = request; + } + + /** + * Get the original request object. + * + * @return {@link HttpRequest}. + */ + public HttpRequest getRequest() { + return mRequest; + } + + + @Override + public HttpMethod getMethod() { + return mRequest.getMethod(); + } + + + @Override + public String getURI() { + return mRequest.getURI(); + } + + + @Override + public String getPath() { + return mRequest.getPath(); + } + + + @Override + public List getQueryNames() { + return mRequest.getQueryNames(); + } + + + @Override + public String getQuery( String name) { + return mRequest.getQuery(name); + } + + + @Override + public List getQueries( String name) { + return mRequest.getQueries(name); + } + + + @Override + public MultiValueMap getQuery() { + return mRequest.getQuery(); + } + + + @Override + public List getHeaderNames() { + return mRequest.getHeaderNames(); + } + + + @Override + public String getHeader( String name) { + return mRequest.getHeader(name); + } + + + @Override + public List getHeaders( String name) { + return mRequest.getHeaders(name); + } + + @Override + public long getDateHeader( String name) { + return mRequest.getDateHeader(name); + } + + @Override + public int getIntHeader( String name) { + return mRequest.getIntHeader(name); + } + + + @Override + public MediaType getAccept() { + return mRequest.getAccept(); + } + + + @Override + public List getAccepts() { + return mRequest.getAccepts(); + } + + + @Override + public Locale getAcceptLanguage() { + return mRequest.getAcceptLanguage(); + } + + + @Override + public List getAcceptLanguages() { + return mRequest.getAcceptLanguages(); + } + + + @Override + public String getCookieValue(String name) { + return mRequest.getCookieValue(name); + } + + + @Override + public Cookie getCookie(String name) { + return mRequest.getCookie(name); + } + + + @Override + public List getCookies() { + return mRequest.getCookies(); + } + + @Override + public long getContentLength() { + return mRequest.getContentLength(); + } + + + @Override + public MediaType getContentType() { + return mRequest.getContentType(); + } + + + @Override + public List getParameterNames() { + return mRequest.getParameterNames(); + } + + + @Override + public String getParameter( String name) { + return mRequest.getParameter(name); + } + + + @Override + public List getParameters( String name) { + return mRequest.getParameters(name); + } + + + @Override + public MultiValueMap getParameter() { + return mRequest.getParameter(); + } + + + @Override + public RequestBody getBody() throws UnsupportedOperationException { + return mRequest.getBody(); + } + + + @Override + public Session getValidSession() { + return mRequest.getValidSession(); + } + + + @Override + public Session getSession() { + return mRequest.getSession(); + } + + + @Override + public String changeSessionId() { + return mRequest.changeSessionId(); + } + + @Override + public boolean isSessionValid() { + return mRequest.isSessionValid(); + } + + + @Override + public RequestDispatcher getRequestDispatcher( String path) { + return mRequest.getRequestDispatcher(path); + } + + @Override + public HttpContext getContext() { + return mRequest.getContext(); + } + + + @Override + public Object getAttribute( String id) { + return mRequest.getAttribute(id); + } + + @Override + public void setAttribute( String id, Object obj) { + mRequest.setAttribute(id, obj); + } + + + @Override + public Object removeAttribute( String id) { + return mRequest.removeAttribute(id); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/ResponseBody.java b/api/src/main/java/com/yanzhenjie/andserver/http/ResponseBody.java new file mode 100644 index 0000000000000000000000000000000000000000..fede7df0e548b458a4b237eff56037dd79fc3ef1 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/ResponseBody.java @@ -0,0 +1,58 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http; + + +import com.yanzhenjie.andserver.util.MediaType; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Created by Zhenjie Yan on 2018/8/3. + */ +public interface ResponseBody { + + /** + * Can it be reused? + * + * @return true, otherwise is false. + */ + boolean isRepeatable(); + + /** + * Get the content-length of the message body, if the length is unknown, return a negative value. + * + * @return message length. + */ + long contentLength(); + + /** + * Get the content-type of the message body, including charset. + * + * @return e.g. {@code application/json; charset=utf-8}. + */ + + MediaType contentType(); + + /** + * Write the body to the output stream. + * + * @param output the output stream to write the body. + */ + void writeTo(OutputStream output) throws IOException; + +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/ResponseWrapper.java b/api/src/main/java/com/yanzhenjie/andserver/http/ResponseWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b00f5d434e4bce975f3041991da77f64049f6d7e --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/ResponseWrapper.java @@ -0,0 +1,120 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http; + + +import com.yanzhenjie.andserver.http.cookie.Cookie; + +import java.util.List; + +/** + * Created by Zhenjie Yan on 2018/9/6. + */ +public class ResponseWrapper implements HttpResponse { + + private HttpResponse mResponse; + + public ResponseWrapper(HttpResponse response) { + this.mResponse = response; + } + + /** + * Get the original response object. + * + * @return {@link HttpResponse}. + */ + public HttpResponse getResponse() { + return mResponse; + } + + @Override + public void setStatus(int sc) { + mResponse.setStatus(sc); + } + + @Override + public int getStatus() { + return mResponse.getStatus(); + } + + @Override + public void setHeader( String name, String value) { + mResponse.setHeader(name, value); + } + + @Override + public void addHeader( String name, String value) { + mResponse.addHeader(name, value); + } + + + @Override + public String getHeader( String name) { + return mResponse.getHeader(name); + } + + @Override + public void setDateHeader( String name, long date) { + mResponse.setDateHeader(name, date); + } + + @Override + public void addDateHeader( String name, long date) { + mResponse.addDateHeader(name, date); + } + + @Override + public void setIntHeader( String name, int value) { + mResponse.setIntHeader(name, value); + } + + @Override + public void addIntHeader( String name, int value) { + mResponse.addIntHeader(name, value); + } + + @Override + public boolean containsHeader( String name) { + return mResponse.containsHeader(name); + } + + + @Override + public List getHeaders( String name) { + return mResponse.getHeaders(name); + } + + + @Override + public List getHeaderNames() { + return mResponse.getHeaderNames(); + } + + @Override + public void addCookie( Cookie cookie) { + mResponse.addCookie(cookie); + } + + @Override + public void sendRedirect( String location) { + mResponse.sendRedirect(location); + } + + @Override + public void setBody(ResponseBody body) { + mResponse.setBody(body); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/StandardContext.java b/api/src/main/java/com/yanzhenjie/andserver/http/StandardContext.java new file mode 100644 index 0000000000000000000000000000000000000000..4b91ebda7a9a3cdf6e56a6eee484b6ac7cacf431 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/StandardContext.java @@ -0,0 +1,48 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http; + + + + +/** + * Created by Zhenjie Yan on 2018/8/31. + */ +public class StandardContext implements HttpContext { + + private org.apache.httpcore.protocol.HttpContext mContext; + + public StandardContext(org.apache.httpcore.protocol.HttpContext context) { + this.mContext = context; + } + + + @Override + public Object getAttribute( String id) { + return mContext.getAttribute(id); + } + + @Override + public void setAttribute( String id, Object obj) { + mContext.setAttribute(id, obj); + } + + + @Override + public Object removeAttribute( String id) { + return mContext.removeAttribute(id); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/StandardRequest.java b/api/src/main/java/com/yanzhenjie/andserver/http/StandardRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..ab7d1886437f55d619d9d06a4db4cdb7629942a2 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/StandardRequest.java @@ -0,0 +1,591 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http; + +import com.yanzhenjie.andserver.DispatcherHandler; +import com.yanzhenjie.andserver.http.cookie.Cookie; +import com.yanzhenjie.andserver.http.cookie.CookieProcessor; +import com.yanzhenjie.andserver.http.cookie.StandardCookieProcessor; +import com.yanzhenjie.andserver.http.session.Session; +import com.yanzhenjie.andserver.http.session.SessionManager; +import com.yanzhenjie.andserver.util.*; +import org.apache.httpcore.Header; +import org.apache.httpcore.HttpEntity; +import org.apache.httpcore.HttpEntityEnclosingRequest; +import org.apache.httpcore.RequestLine; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.GZIPInputStream; + +/** + * Created by Zhenjie Yan on 2018/6/12. + */ +public class StandardRequest implements HttpRequest { + + private static final CookieProcessor COOKIE_PROCESSOR = new StandardCookieProcessor(); + + private org.apache.httpcore.HttpRequest mRequest; + private HttpContext mContext; + private DispatcherHandler mHandler; + private RequestLine mRequestLine; + private SessionManager mSessionManager; + + private Uri mUri; + private boolean isParsedUri; + + private MultiValueMap mQuery; + private boolean isParsedQuery; + + private List mAccepts; + private boolean isParsedAccept; + + private List mLocales; + private boolean isParsedLocale; + + private MultiValueMap mParameter; + private boolean isParsedParameter; + + public StandardRequest(org.apache.httpcore.HttpRequest request, HttpContext context, DispatcherHandler handler, + SessionManager sessionManager) { + this.mRequest = request; + this.mContext = context; + this.mHandler = handler; + this.mRequestLine = request.getRequestLine(); + this.mSessionManager = sessionManager; + } + + + @Override + public HttpMethod getMethod() { + return HttpMethod.reverse(mRequestLine.getMethod()); + } + + + @Override + public String getURI() { + parseUri(); + return mUri.toString(); + } + + private void parseUri() { + if (isParsedUri) { + return; + } + + String requestLine = mRequestLine.getUri(); + if (StringUtils.isEmpty(requestLine)) { + requestLine = "/"; + } + + String uri = "scheme://host:ip" + requestLine; + mUri = Uri.newBuilder(uri).build(); + isParsedUri = true; + } + + public void setPath(String path) { + parseUri(); + mUri = mUri.builder().setPath(path).build(); + } + + + @Override + public String getPath() { + parseUri(); + return mUri.getPath(); + } + + + @Override + public List getQueryNames() { + parseQuery(); + return new LinkedList<>(mQuery.keySet()); + } + + + @Override + public String getQuery( String name) { + parseQuery(); + return mQuery.getFirst(name); + } + + + @Override + public List getQueries( String name) { + parseQuery(); + List values = mQuery.get(name); + return (values == null || values.isEmpty()) ? Collections.emptyList() : values; + } + + + @Override + public MultiValueMap getQuery() { + parseQuery(); + return mQuery; + } + + private void parseQuery() { + if (isParsedQuery) { + return; + } + parseUri(); + + mQuery = mUri.getParams(); + isParsedQuery = true; + } + + + @Override + public List getHeaderNames() { + Header[] headers = mRequest.getAllHeaders(); + if (headers == null || headers.length == 0) { + return Collections.emptyList(); + } + + List nameList = new ArrayList<>(); + for (Header header: headers) { + nameList.add(header.getName()); + } + return nameList; + } + + + @Override + public String getHeader( String name) { + Header header = mRequest.getFirstHeader(name); + return header == null ? null : header.getValue(); + } + + + @Override + public List getHeaders( String name) { + Header[] headers = mRequest.getHeaders(name); + if (headers == null || headers.length == 0) { + return Collections.emptyList(); + } + + List valueList = new ArrayList<>(); + for (Header header: headers) { + valueList.add(header.getValue()); + } + return valueList; + } + + @Override + public long getDateHeader( String name) { + Header header = mRequest.getFirstHeader(name); + if (header == null) { + return -1; + } + + String value = header.getValue(); + long date = HttpDateFormat.parseDate(value); + + if (date == -1) { + String message = String.format("The %s cannot be converted to date.", value); + throw new IllegalStateException(message); + } + + return date; + } + + @Override + public int getIntHeader( String name) { + Header header = mRequest.getFirstHeader(name); + if (header == null) { + return -1; + } + + try { + return Integer.parseInt(header.getValue()); + } catch (NumberFormatException e) { + throw new IllegalStateException(e.getMessage(), e); + } + } + + + @Override + public MediaType getAccept() { + List mediaTypes = getAccepts(); + return mediaTypes.isEmpty() ? null : mediaTypes.get(0); + } + + + @Override + public List getAccepts() { + parseAccept(); + return mAccepts; + } + + private void parseAccept() { + if (isParsedAccept) { + return; + } + + mAccepts = new ArrayList<>(); + Header[] headers = mRequest.getHeaders(ACCEPT); + if (headers != null && headers.length > 0) { + for (Header header: headers) { + List mediaTypes = MediaType.parseMediaTypes(header.getValue()); + mAccepts.addAll(mediaTypes); + } + } + if (mAccepts.isEmpty()) { + mAccepts.add(MediaType.ALL); + } + + isParsedAccept = true; + } + + + @Override + public Locale getAcceptLanguage() { + return getAcceptLanguages().get(0); + } + + + @Override + public List getAcceptLanguages() { + parseLocale(); + return mLocales; + } + + private void parseLocale() { + if (isParsedLocale) { + return; + } + + mLocales = new ArrayList<>(); + Header[] headers = mRequest.getHeaders(ACCEPT_LANGUAGE); + if (headers != null && headers.length > 0) { + for (Header header: headers) { + List acceptLanguages = FAcceptLanguage.parse(header.getValue()); + for (FAcceptLanguage acceptLanguage: acceptLanguages) { + mLocales.add(acceptLanguage.getLocale()); + } + } + } + if (mLocales.isEmpty()) { + mLocales.add(Locale.getDefault()); + } + + isParsedLocale = true; + } + + + @Override + public String getCookieValue(String name) { + Cookie cookie = getCookie(name); + if (cookie != null) { + return cookie.getValue(); + } + return null; + } + + + @Override + public Cookie getCookie(String name) { + List cookies = getCookies(); + if (cookies.isEmpty()) { + return null; + } + + for (Cookie cookie: cookies) { + if (name.equalsIgnoreCase(cookie.getName())) { + return cookie; + } + } + return null; + } + + + @Override + public List getCookies() { + return COOKIE_PROCESSOR.parseCookieHeader(mRequest.getHeaders(COOKIE)); + } + + @Override + public long getContentLength() { + String contentLength = getHeader(CONTENT_LENGTH); + if (StringUtils.isEmpty(contentLength)) { + return -1; + } + try { + return Long.parseLong(contentLength); + } catch (NumberFormatException e) { + return -1; + } + } + + + @Override + public MediaType getContentType() { + String contentType = getHeader(CONTENT_TYPE); + if (contentType.isEmpty()) { + return null; + } + return MediaType.valueOf(contentType); + } + + + @Override + public List getParameterNames() { + parseParameter(); + List paramNames = new LinkedList<>(mParameter.keySet()); + List names = getQueryNames(); + if (!names.isEmpty()) { + paramNames.addAll(names); + } + return paramNames; + } + + + @Override + public String getParameter( String name) { + parseParameter(); + String value = mParameter.getFirst(name); + return value.isEmpty() ? getQuery(name) : value; + } + + + @Override + public List getParameters( String name) { + parseParameter(); + List values = mParameter.get(name); + if (values == null || values.isEmpty()) { + return getQueries(name); + } + return values; + } + + + @Override + public MultiValueMap getParameter() { + parseParameter(); + return mParameter.isEmpty() ? getQuery() : mParameter; + } + + private void parseParameter() { + if (isParsedParameter) { + return; + } + + if (!getMethod().allowBody()) { + mParameter = new LinkedMultiValueMap<>(); + return; + } + + MediaType mediaType = getContentType(); + if (MediaType.APPLICATION_FORM_URLENCODED.includes(mediaType)) { + try { + RequestBody body = getBody(); + String bodyString = body == null ? "" : body.string(); + mParameter = parseParameters(bodyString); + } catch (Exception ignored) { + } + } + if (mParameter == null) { + mParameter = new LinkedMultiValueMap<>(); + } + isParsedParameter = true; + } + + + @Override + public RequestBody getBody() { + if (getMethod().allowBody()) { + if (mRequest instanceof HttpEntityEnclosingRequest) { + HttpEntityEnclosingRequest request = (HttpEntityEnclosingRequest) mRequest; + HttpEntity entity = request.getEntity(); + if (entity == null) { + return null; + } + return new EntityToBody(entity); + } + return null; + } + throw new UnsupportedOperationException("This method does not allow body."); + } + + + @Override + public Session getValidSession() { + Session session = getSession(); + if (session == null) { + session = mSessionManager.createSession(); + } else if (session.isValid()) { + session = mSessionManager.createSession(); + } + + setAttribute(REQUEST_CREATED_SESSION, session); + return session; + } + + @Override + public Session getSession() { + Object objSession = getAttribute(REQUEST_CREATED_SESSION); + if (objSession instanceof Session) { + return (Session) objSession; + } + + List cookies = getCookies(); + if (cookies.isEmpty()) { + return null; + } + + String sessionId = null; + for (Cookie cookie: cookies) { + if (SESSION_NAME.equalsIgnoreCase(cookie.getName())) { + sessionId = cookie.getValue(); + break; + } + } + + if (sessionId.isEmpty()) { + return null; + } + + Session session = null; + try { + session = mSessionManager.findSession(sessionId); + } catch (IOException e) { + e.printStackTrace(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + + return session; + } + + @Override + public String changeSessionId() { + Session session = getSession(); + if (session == null) { + throw new IllegalStateException("No session associated with this request."); + } + mSessionManager.changeSessionId(session); + return session.getId(); + } + + @Override + public boolean isSessionValid() { + Session session = getSession(); + return session != null && session.isValid(); + } + + + @Override + public RequestDispatcher getRequestDispatcher(String path) { + return mHandler.getRequestDispatcher(this, path); + } + + @Override + public HttpContext getContext() { + return mContext; + } + + + @Override + public Object getAttribute(String id) { + return mContext.getAttribute(id); + } + + @Override + public void setAttribute( String id, Object obj) { + mContext.setAttribute(id, obj); + } + + + @Override + public Object removeAttribute( String id) { + return mContext.removeAttribute(id); + } + + + private static MultiValueMap parseParameters(String input) { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + StringTokenizer tokenizer = new StringTokenizer(input, "&"); + while (tokenizer.hasMoreElements()) { + String element = tokenizer.nextToken(); + int end = element.indexOf("="); + + if (end > 0 && end < element.length() - 1) { + String key = element.substring(0, end); + String value = element.substring(end + 1); + parameters.add(key, UrlCoder.urlDecode(value, StandardCharsets.UTF_8)); + } + } + return parameters; + } + + private static class EntityToBody implements RequestBody { + + private HttpEntity mEntity; + + private EntityToBody(HttpEntity entity) { + this.mEntity = entity; + } + + @Override + public String contentEncoding() { + Header encoding = mEntity.getContentType(); + return encoding == null ? "" : encoding.getValue(); + } + + @Override + public long length() { + return mEntity.getContentLength(); + } + + + @Override + public MediaType contentType() { + Header header = mEntity.getContentType(); + if (header == null) { + return null; + } + + String contentType = header.getValue(); + return MediaType.valueOf(contentType); + } + + + @Override + public InputStream stream() throws IOException { + InputStream stream = mEntity.getContent(); + if (contentEncoding().toLowerCase().contains("gzip")) { + stream = new GZIPInputStream(stream); + } + return stream; + } + + + @Override + public String string() throws IOException { + MimeType mimeType = contentType(); + Charset charset = mimeType == null ? null : mimeType.getCharset(); + + if (charset == null) { + return IOUtils.toString(stream()); + } else { + return IOUtils.toString(stream(), charset); + } + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/StandardResponse.java b/api/src/main/java/com/yanzhenjie/andserver/http/StandardResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..cbac08aa2037de1a2026cd5523aa920a56ff0480 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/StandardResponse.java @@ -0,0 +1,202 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http; + +import com.yanzhenjie.andserver.http.cookie.Cookie; +import com.yanzhenjie.andserver.http.cookie.CookieProcessor; +import com.yanzhenjie.andserver.http.cookie.StandardCookieProcessor; +import com.yanzhenjie.andserver.util.HttpDateFormat; +import com.yanzhenjie.andserver.util.MediaType; +import org.apache.httpcore.Header; +import org.apache.httpcore.HttpEntity; +import org.apache.httpcore.message.BasicHeader; +import org.apache.httpcore.util.EntityUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Created by Zhenjie Yan on 2018/6/12. + */ +public class StandardResponse implements HttpResponse { + + private static final CookieProcessor COOKIE_PROCESSOR = new StandardCookieProcessor(); + + private org.apache.httpcore.HttpResponse mResponse; + + public StandardResponse(org.apache.httpcore.HttpResponse response) { + this.mResponse = response; + } + + @Override + public void setStatus(int sc) { + mResponse.setStatusCode(sc); + } + + @Override + public int getStatus() { + return mResponse.getStatusLine().getStatusCode(); + } + + @Override + public void setHeader( String name, String value) { + mResponse.setHeader(name, value); + } + + @Override + public void addHeader( String name, String value) { + mResponse.addHeader(name, value); + } + + + @Override + public String getHeader( String name) { + Header header = mResponse.getFirstHeader(name); + return header == null ? null : header.getValue(); + } + + @Override + public void setDateHeader( String name, long date) { + setHeader(name, HttpDateFormat.formatDate(date)); + } + + @Override + public void addDateHeader( String name, long date) { + addHeader(name, HttpDateFormat.formatDate(date)); + } + + @Override + public void setIntHeader( String name, int value) { + setHeader(name, Integer.toString(value)); + } + + @Override + public void addIntHeader( String name, int value) { + addHeader(name, Integer.toString(value)); + } + + @Override + public boolean containsHeader( String name) { + return mResponse.containsHeader(name); + } + + + @Override + public List getHeaders( String name) { + Header[] headers = mResponse.getHeaders(name); + if (headers == null || headers.length == 0) { + return Collections.emptyList(); + } + + List list = new ArrayList<>(); + for (Header header: headers) { + list.add(header.getValue()); + } + return list; + } + + + @Override + public List getHeaderNames() { + Header[] headers = mResponse.getAllHeaders(); + if (headers == null || headers.length == 0) { + return Collections.emptyList(); + } + + List list = new ArrayList<>(); + for (Header header: headers) { + list.add(header.getName()); + } + return list; + } + + @Override + public void addCookie( Cookie cookie) { + addHeader(SET_COOKIE, COOKIE_PROCESSOR.generateHeader(cookie)); + } + + @Override + public void sendRedirect( String location) { + setStatus(SC_FOUND); + setHeader(LOCATION, location); + } + + @Override + public void setBody(ResponseBody body) { + mResponse.setEntity(new BodyToEntity(body)); + } + + private static class BodyToEntity implements HttpEntity { + + private ResponseBody mBody; + + private BodyToEntity(ResponseBody body) { + this.mBody = body; + } + + @Override + public boolean isRepeatable() { + return false; + } + + @Override + public boolean isChunked() { + return false; + } + + @Override + public long getContentLength() { + return mBody.contentLength(); + } + + @Override + public Header getContentType() { + MediaType mimeType = mBody.contentType(); + if (mimeType == null) { + return null; + } + return new BasicHeader(CONTENT_TYPE, mimeType.toString()); + } + + @Override + public Header getContentEncoding() { + return null; + } + + @Override + public InputStream getContent() throws IOException { + return null; + } + + @Override + public void writeTo(OutputStream out) throws IOException { + mBody.writeTo(out); + } + + @Override + public boolean isStreaming() { + return false; + } + + @Override + public void consumeContent() { + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/StatusCode.java b/api/src/main/java/com/yanzhenjie/andserver/http/StatusCode.java new file mode 100644 index 0000000000000000000000000000000000000000..9247a36b3322c2ab7a98086654ae4e404bf8efc6 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/StatusCode.java @@ -0,0 +1,246 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http; + +/** + * Created by Zhenjie Yan on 2018/7/26. + */ +public interface StatusCode { + + /** + * Status code (100) indicating the client can continue. + */ + int SC_CONTINUE = 100; + + /** + * Status code (101) indicating the server is switching protocols according to Upgrade header. + */ + int SC_SWITCHING_PROTOCOLS = 101; + + /** + * Status code (200) indicating the request succeeded normally. + */ + int SC_OK = 200; + + /** + * Status code (201) indicating the request succeeded and created a new resource on the server. + */ + int SC_CREATED = 201; + + /** + * Status code (202) indicating that a request was accepted for processing, but was not completed. + */ + int SC_ACCEPTED = 202; + + /** + * Status code (203) indicating that the meta information presented by the client did not originate from the + * server. + */ + int SC_NON_AUTHORITATIVE_INFORMATION = 203; + + /** + * Status code (204) indicating that the request succeeded but that there was no new information to return. + */ + int SC_NO_CONTENT = 204; + + /** + * Status code (205) indicating that the agent SHOULD reset the document view which caused the request to + * be sent. + */ + int SC_RESET_CONTENT = 205; + + /** + * Status code (206) indicating that the server has fulfilled the partial GET request for the resource. + */ + int SC_PARTIAL_CONTENT = 206; + + /** + * Status code (300) indicating that the requested resource corresponds to any one of a set of representations, each + * with its own specific location. + */ + int SC_MULTIPLE_CHOICES = 300; + + /** + * Status code (301) indicating that the resource has permanently moved to a new location, and that future + * references should use a new URI with their requests. + */ + int SC_MOVED_PERMANENTLY = 301; + + /** + * Status code (302) indicating that the resource reside temporarily under a different URI. Since the redirection + * might be altered on occasion, the client should continue to use the Request-URI for future requests.(HTTP/1.1) To + * represent the status code (302), it is recommended to use this variable. + */ + int SC_FOUND = 302; + + /** + * Status code (303) indicating that the response to the request can be found under a different URI. + */ + int SC_SEE_OTHER = 303; + + /** + * Status code (304) indicating that a conditional GET operation found that the resource was available and not + * modified. + */ + int SC_NOT_MODIFIED = 304; + + /** + * Status code (305) indicating that the requested resource MUST be accessed through the proxy given by + * the + * Location field. + */ + int SC_USE_PROXY = 305; + + /** + * Status code (307) indicating that the requested resource resides temporarily under a different URI. The temporary + * URI + * SHOULD be given by the Location field in the response. + */ + int SC_TEMPORARY_REDIRECT = 307; + + /** + * Status code (400) indicating the request sent by the client was syntactically incorrect. + */ + int SC_BAD_REQUEST = 400; + + /** + * Status code (401) indicating that the request requires HTTP authentication. + */ + int SC_UNAUTHORIZED = 401; + + /** + * Status code (402) reserved for future use. + */ + int SC_PAYMENT_REQUIRED = 402; + + /** + * Status code (403) indicating the server understood the request but refused to fulfill it. + */ + int SC_FORBIDDEN = 403; + + /** + * Status code (404) indicating that the requested resource is not available. + */ + int SC_NOT_FOUND = 404; + + /** + * Status code (405) indicating that the method specified in the Request-Line is not allowed + * for the resource identified by the Request-URI. + */ + int SC_METHOD_NOT_ALLOWED = 405; + + /** + * Status code (406) indicating that the resource identified by the request is only capable of generating response + * entities which have content characteristics not acceptable according to the accept headers sent in the request. + */ + int SC_NOT_ACCEPTABLE = 406; + + /** + * Status code (407) indicating that the client MUST first authenticate itself with the proxy. + */ + int SC_PROXY_AUTHENTICATION_REQUIRED = 407; + + /** + * Status code (408) indicating that the client did not produce a request within the time that the server was + * prepared to wait. + */ + int SC_REQUEST_TIMEOUT = 408; + + /** + * Status code (409) indicating that the request could not be completed due to a conflict with the current state of + * the resource. + */ + int SC_CONFLICT = 409; + + /** + * Status code (410) indicating that the resource is no longer available at the server and no forwarding address is + * known. This condition SHOULD be considered permanent. + */ + int SC_GONE = 410; + + /** + * Status code (411) indicating that the request cannot be handled without a defined + * Content-Length. + */ + int SC_LENGTH_REQUIRED = 411; + + /** + * Status code (412) indicating that the precondition given in one or more of the request-header fields evaluated to + * false when it was tested on the server. + */ + int SC_PRECONDITION_FAILED = 412; + + /** + * Status code (413) indicating that the server is refusing to process the request because the request entity is + * larger than the server is willing or able to process. + */ + int SC_REQUEST_ENTITY_TOO_LARGE = 413; + + /** + * Status code (414) indicating that the server is refusing to service the request because the + * Request-URI is longer than the server is willing to interpret. + */ + int SC_REQUEST_URI_TOO_LONG = 414; + + /** + * Status code (415) indicating that the server is refusing to service the request because the entity of the request + * is in a format not supported by the requested resource for the requested method. + */ + int SC_UNSUPPORTED_MEDIA_TYPE = 415; + + /** + * Status code (416) indicating that the server cannot serve the requested byte range. + */ + int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416; + + /** + * Status code (417) indicating that the server could not meet the expectation given in the Expect request header. + */ + int SC_EXPECTATION_FAILED = 417; + + /** + * Status code (500) indicating an error inside the HTTP server which prevented it from fulfilling the request. + */ + int SC_INTERNAL_SERVER_ERROR = 500; + + /** + * Status code (501) indicating the HTTP server does not support the functionality needed to fulfill the request. + */ + int SC_NOT_IMPLEMENTED = 501; + + /** + * Status code (502) indicating that the HTTP server received an invalid response from a server it consulted when + * acting as a proxy or gateway. + */ + int SC_BAD_GATEWAY = 502; + + /** + * Status code (503) indicating that the HTTP server is temporarily overloaded, and unable to handle the request. + */ + int SC_SERVICE_UNAVAILABLE = 503; + + /** + * Status code (504) indicating that the server did not receive a timely response from the upstream server while + * acting as a gateway or proxy. + */ + int SC_GATEWAY_TIMEOUT = 504; + + /** + * Status code (505) indicating that the server does not support or refuses to support the HTTP protocol version + * that was used in the request message. + */ + int SC_HTTP_VERSION_NOT_SUPPORTED = 505; +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/Uri.java b/api/src/main/java/com/yanzhenjie/andserver/http/Uri.java new file mode 100644 index 0000000000000000000000000000000000000000..df081667ea835103085b0b0bf810d661486c77d6 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/Uri.java @@ -0,0 +1,438 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http; + + +import com.yanzhenjie.andserver.util.*; +import org.apache.commons.io.Charsets; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.*; + +/** + * Add the mPath to the URL, such as: + *

+ * Uri url = Uri.newBuilder("http://www.example.com/xx")
+ *      .scheme("https")
+ *      .port(8080)
+ *      .path("yy")
+ *      .query("abc", "123")
+ *      .setFragment("article")
+ *      .build();
+ * ...
+ * The real url is: https://www.example.com:8080/xx/yy?abc=123#article.
+ * 
+ *
+ * Uri url = Uri.newBuilder("http://www.example.com/xx/yy?abc=123")
+ *      .setSegments("/aa/bb/cc")
+ *      .setQuery("mln=456&ijk=789")
+ *      .build();
+ * ...
+ * The real url is: http://www.example.com/aa/bb/cc?mln=456&ijk=789.
+ * 
+ *
+ * Uri url = Uri.newBuilder("http://www.example.com/user/photo/search?name=abc").build();
+ * Uri newUrl = url.location("../../get?name=mln");
+ * ...
+ * The new url is: http://www.example.com/get?name=abc.
+ * 
+ * Created by Zhenjie Yan on 2018/2/9. + */ +public class Uri implements Patterns { + + public static Builder newBuilder(String uri) { + return new Builder(uri); + } + + private final String mScheme; + private final String mHost; + private final int mPort; + private final String mPath; + private final String mQuery; + private final String mFragment; + + private Uri(Builder builder) { + this.mScheme = builder.mScheme; + this.mHost = builder.mHost; + this.mPort = builder.mPort; + this.mPath = pathsToPath(builder.mPath); + this.mQuery = parametersToQuery(builder.mQuery); + this.mFragment = builder.mFragment; + } + + + public String getScheme() { + return mScheme; + } + + + public String getHost() { + return mHost; + } + + public int getPort() { + return mPort; + } + + + public String getPath() { + return mPath; + } + + + public List getPaths() { + return pathToPaths(mPath); + } + + + public String getQuery() { + return mQuery; + } + + + public MultiValueMap getParams() { + return queryToParameters(mQuery); + } + + + public String getFragment() { + return mFragment; + } + + + public Builder builder() { + return new Builder(toString()); + } + + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (StringUtils.isEmpty(mScheme)) { + builder.append(mScheme).append(":"); + } + + if (!StringUtils.isEmpty(mHost) && mPort > 0) { + builder.append("//").append(mHost).append(":").append(mPort); + } + + if (!StringUtils.isEmpty(mPath)) { + builder.append(mPath); + } + + if (!StringUtils.isEmpty(mQuery)) { + builder.append("?").append(mQuery); + } + + if (!StringUtils.isEmpty(mFragment)) { + builder.append("#").append(mFragment); + } + + return builder.toString(); + } + + + public Uri location(String location) { + if (StringUtils.isEmpty(location)) { + return null; + } + + if (URLUtil.isNetworkUrl(location)) { + return newBuilder(location).build(); + } + + URI newUri = URI.create(location); + if (location.startsWith("/")) { + return builder().setPath(newUri.getPath()) + .setQuery(newUri.getQuery()) + .setFragment(newUri.getFragment()) + .build(); + } else if (location.contains("../")) { + List oldPathList = pathToPaths(getPath()); + List newPathList = pathToPaths(newUri.getPath()); + + int start = newPathList.lastIndexOf(".."); + newPathList = newPathList.subList(start + 1, newPathList.size()); + if (!oldPathList.isEmpty()) { + oldPathList = oldPathList.subList(0, oldPathList.size() - start - 2); + oldPathList.addAll(newPathList); + String path = TextUtils.join("/", oldPathList); + return builder().setPath(path).setQuery(newUri.getQuery()).setFragment(newUri.getFragment()).build(); + } + String path = TextUtils.join("/", newPathList); + return builder().setPath(path).setQuery(newUri.getQuery()).setFragment(newUri.getFragment()).build(); + } else { + List oldPathList = pathToPaths(getPath()); + oldPathList.addAll(pathToPaths(newUri.getPath())); + String path = TextUtils.join("/", oldPathList); + return builder().setPath(path).setQuery(newUri.getQuery()).setFragment(newUri.getFragment()).build(); + } + } + + public static class Builder { + + private String mScheme; + private String mHost; + private int mPort; + private List mPath; + private MultiValueMap mQuery; + private String mFragment; + + private Builder( String url) { + URI uri = URI.create(url); + + this.mScheme = uri.getScheme(); + this.mHost = uri.getHost(); + this.mPort = uri.getPort(); + String path = uri.getPath(); + this.mPath = pathToPaths(path); + String query = uri.getQuery(); + this.mQuery = queryToParameters(query); + this.mFragment = uri.getFragment(); + } + + public Builder setScheme( String scheme) { + this.mScheme = scheme; + return this; + } + + public Builder setHost( String host) { + this.mHost = host; + return this; + } + + public Builder setPort(int port) { + this.mPort = port; + return this; + } + + public Builder addPath(int value) { + return addPath(Integer.toString(value)); + } + + public Builder addPath(long value) { + return addPath(Long.toString(value)); + } + + public Builder addPath(boolean value) { + return addPath(Boolean.toString(value)); + } + + public Builder addPath(char value) { + return addPath(String.valueOf(value)); + } + + public Builder addPath(double value) { + return addPath(Double.toString(value)); + } + + public Builder addPath(float value) { + return addPath(Float.toString(value)); + } + + public Builder addPath( CharSequence path) { + mPath.add(path.toString()); + return this; + } + + public Builder addPath( String path) { + mPath.add(path); + return this; + } + + public Builder setPath( String path) { + mPath = pathToPaths(path); + return this; + } + + public Builder clearPath() { + mPath.clear(); + return this; + } + + public Builder addQuery( String key, int value) { + return addQuery(key, Integer.toString(value)); + } + + public Builder addQuery( String key, long value) { + return addQuery(key, Long.toString(value)); + } + + public Builder addQuery( String key, boolean value) { + return addQuery(key, Boolean.toString(value)); + } + + public Builder addQuery( String key, char value) { + return addQuery(key, String.valueOf(value)); + } + + public Builder addQuery( String key, double value) { + return addQuery(key, Double.toString(value)); + } + + public Builder addQuery( String key, float value) { + return addQuery(key, Float.toString(value)); + } + + public Builder addQuery( String key, short value) { + return addQuery(key, Integer.toString(value)); + } + + public Builder addQuery( String key, CharSequence value) { + mQuery.add(key, value.toString()); + return this; + } + + public Builder addQuery( String key, String value) { + mQuery.add(key, value); + return this; + } + + public Builder addQuery( String key, List values) { + for (String value: values) { + mQuery.add(key, value); + } + return this; + } + + public Builder setQuery( String query) { + mQuery = queryToParameters(query); + return this; + } + + public Builder setQuery( MultiValueMap query) { + mQuery = query; + return this; + } + + public Builder removeQuery( String key) { + mQuery.remove(key); + return this; + } + + public Builder clearQuery() { + mQuery.clear(); + return this; + } + + public Builder setFragment( String fragment) { + this.mFragment = fragment; + return this; + } + + public Uri build() { + return new Uri(this); + } + } + + public static List pathToPaths(String path) { + List pathList = new LinkedList<>(); + if (StringUtils.isEmpty(path)) { + return pathList; + } + + while (path.contains("//")) { + path = path.replace("//", "/"); + } + + while (path.contains("/")) { + if (path.startsWith("/")) { + pathList.add(""); + path = path.substring(1); + } else { + int index = path.indexOf("/"); + pathList.add(path.substring(0, index)); + path = path.substring(index + 1); + } + + if (!path.contains("/")) { + pathList.add(path); + } + } + return pathList; + } + + public static MultiValueMap queryToParameters(String query) { + MultiValueMap valueMap = new LinkedMultiValueMap<>(); + if (!StringUtils.isEmpty(query)) { + if (query.startsWith("?")) { + query = query.substring(1); + } + + StringTokenizer tokenizer = new StringTokenizer(query, "&"); + while (tokenizer.hasMoreElements()) { + String element = tokenizer.nextToken(); + int end = element.indexOf("="); + + if (end > 0 && end < element.length() - 1) { + String key = element.substring(0, end); + String value = element.substring(end + 1); + valueMap.add(key, UrlCoder.urlDecode(value, StandardCharsets.UTF_8)); + } + } + } + return valueMap; + } + + public static String pathsToPath(List pathList) { + if (pathList == null || pathList.isEmpty()) { + return ""; + } + + StringBuilder builder = new StringBuilder(); + for (String path: pathList) { + builder.append("/").append(path); + } + + String path = builder.toString(); + while (path.contains("//")) { + path = path.replace("//", "/"); + } + return path; + } + + public static String parametersToQuery(MultiValueMap params) { + StringBuilder builder = new StringBuilder(); + Iterator>> iterator = params.entrySet().iterator(); + if (iterator.hasNext()) { + Map.Entry> param = iterator.next(); + String key = param.getKey(); + List valueList = param.getValue(); + if (valueList != null && !valueList.isEmpty()) { + for (String value: valueList) { + builder.append(key).append("=").append(value); + } + } else { + builder.append(key).append("="); + } + } + + while (iterator.hasNext()) { + Map.Entry> param = iterator.next(); + String key = param.getKey(); + List valueList = param.getValue(); + if (valueList != null && !valueList.isEmpty()) { + for (String value: valueList) { + builder.append("&").append(key).append("=").append(value); + } + } else { + builder.append("&").append(key).append("="); + } + } + return builder.toString(); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/cookie/Cookie.java b/api/src/main/java/com/yanzhenjie/andserver/http/cookie/Cookie.java new file mode 100644 index 0000000000000000000000000000000000000000..86dfcb9edfcffa159668aff90fb235c00f4d3f45 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/cookie/Cookie.java @@ -0,0 +1,338 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http.cookie; + +import com.yanzhenjie.andserver.util.StringUtils; + +import java.io.Serializable; +import java.util.Locale; + +/** + * Created by Zhenjie Yan on 2018 + */ +public class Cookie implements Cloneable, Serializable { + + private static final String TSPECIALS = "/()<>@,;:\\\"[]?={} \t"; + + private String name; // NAME= ... "$Name" style is reserved. + private String value; // value of NAME. + + private String comment; // ;Comment=VALUE ... describes cookie's use. + // ;Discard ... implied by maxAge < 0 + private String domain; // ;Domain=VALUE ... domain that sees cookie. + private int maxAge = -1; // ;Max-Age=VALUE ... cookies auto-expire. + private String path; // ;Path=VALUE ... URLs that see the cookie. + private boolean secure; // ;Secure ... e.g. use SSL. + private int version = 0; // ;Version=1 ... means RFC 2109++ style. + private boolean isHttpOnly = false; + + /** + * Constructs a cookie with the specified name and value. + * + *

The name must conform to RFC 2109. + * + *

The name of a cookie cannot be changed once the cookie has been created. + * + *

The value can be anything the server chooses to send. Its value is probably of interest only to the server. + * The cookie's value can be changed after creation with the {@link #setValue(String)} method. + * + *

The version can be changed with the {@link #setVersion(int)} method. + * + * @param name the name of the cookie. + * @param value the value of the cookie. + * + * @throws IllegalArgumentException if the cookie name is null or empty or contains any illegal characters (e.g. + * a comma, space, or semicolon) or matches a token reserved for use by the cookie protocol. + * @see #setValue(String) + * @see #setVersion(int) + */ + public Cookie( String name, String value) { + if (StringUtils.isEmpty(name)) { + throw new IllegalArgumentException("The name of the cookie cannot be empty or null."); + } + if (!isToken(name) || name.equalsIgnoreCase("Comment") || // rfc2019 + name.equalsIgnoreCase("Discard") || // 2019++ + name.equalsIgnoreCase("Domain") || name.equalsIgnoreCase("Expires") || // (old cookies) + name.equalsIgnoreCase("Max-Age") || // rfc2019 + name.equalsIgnoreCase("Path") || name.equalsIgnoreCase("Secure") || name.equalsIgnoreCase("Version") || + name.startsWith("$")) { + String message = String.format("This name [%1$s] is a reserved character.", name); + throw new IllegalArgumentException(message); + } + + this.name = name; + this.value = value; + } + + /** + * Specifies a comment that describes a cookie's purpose. The comment is useful if the browser presents the cookie + * to the user. + * + * @param purpose a {@link String} specifying the comment to display to the user. + * + * @see #getComment() + */ + public void setComment(String purpose) { + comment = purpose; + } + + /** + * Returns the comment describing the purpose of this cookie, or {@code null} if the cookie has no comment. + * + * @return the comment of the cookie, or {@code null} if unspecified. + * + * @see #setComment(String) + */ + + public String getComment() { + return comment; + } + + /** + * Specifies the domain within which this cookie should be presented. + * + *

The form of the domain name is specified by RFC 2109. StandardCookieProcessor domain name begins with a dot + * ({@code .foo.com}) and means that the cookie is visible to servers in a specified Domain Name System (DNS) zone + * (e.g. {@code www.foo.com}, but not {@code a.b.foo.com}). By default, cookies are only returned to the server that + * sent them. + * + * @param domain the domain name within which this cookie is visible; form is according to RFC 2109. + * + * @see #getDomain() + */ + public void setDomain(String domain) { + if (!StringUtils.isEmpty(domain)) { + this.domain = domain.toLowerCase(Locale.ENGLISH); // IE allegedly needs this. + } else { + this.domain = null; + } + } + + /** + * Gets the domain name of this Cookie. + * + *

Domain names are formatted according to RFC 2109. + * + * @return the domain name of this Cookie. + * + * @see #setDomain(String) + */ + + public String getDomain() { + return domain; + } + + /** + * Sets the maximum age in seconds for this Cookie. + * + *

StandardCookieProcessor positive value indicates that the cookie will expire after that many seconds have + * passed. Note that the value is the maximum age when the cookie will expire, not the cookie's current age. + * + *

StandardCookieProcessor negative value means that the cookie is not stored persistently and will be deleted + * when the Web browser exits. StandardCookieProcessor zero value causes the cookie to be deleted. + * + * @param expiry an integer specifying the maximum age of the cookie in seconds; if negative, means the cookie + * is not stored; if zero, deletes the cookie. + * + * @see #getMaxAge() + */ + public void setMaxAge(int expiry) { + maxAge = expiry; + } + + /** + * Gets the maximum age in seconds of this Cookie. + * + *

By default, {@code -1} is returned, which indicates that the cookie will persist until browser shutdown. + * + * @see #setMaxAge(int) + */ + public int getMaxAge() { + return maxAge; + } + + /** + * Specifies a path for the cookie to which the client should return the cookie. + * + *

The cookie is visible to all the pages in the directory you specify, and all the pages in that directory's + * subdirectories. + * + *

Consult RFC 2109 (available on the Internet) for more information on setting path names for cookies. + * + * @param path a {@code String} specifying a path. + * + * @see #getPath() + */ + public void setPath(String path) { + this.path = path; + } + + /** + * Returns the path on the server to which the browser returns this cookie. The cookie is visible to all subpaths on + * the server. + * + * @see #setPath(String) + */ + + public String getPath() { + return path; + } + + /** + * Indicates to the browser whether the cookie should only be sent using a secure protocol, e.g. HTTPS or SSL. + * + *

The default value is {@code false}. + * + * @param flag if {@code true}, sends the cookie from the browser to the server only when using a secure + * protocol; if {@code false}, sent on any protocol. + * + * @see #getSecure() + */ + public void setSecure(boolean flag) { + secure = flag; + } + + /** + * Returns {@code true} if the browser is sending cookies only over a secure protocol, or {@code false} if the + * browser can send cookies using any protocol. + * + * @return {@code true} if the browser uses a secure protocol, {@code false} otherwise. + * + * @see #setSecure(boolean) + */ + public boolean getSecure() { + return secure; + } + + /** + * Returns the name of the cookie. The name cannot be changed after creation. + * + * @return the name of the cookie. + */ + + public String getName() { + return name; + } + + /** + * Assigns a new value to this Cookie. + * + *

If you use a binary value, you may want to use BASE64 encoding. + * + *

With Version 0 cookies, values should not contain white space, brackets, parentheses, equals signs, commas, + * double quotes, slashes, question marks, at signs, colons, and semicolons. Empty values may not behave the same + * way on all browsers. + * + * @param newValue the new value of the cookie. + * + * @see #getValue() + */ + public void setValue(String newValue) { + value = newValue; + } + + /** + * Gets the current value of this Cookie. + * + * @return the current value of this Cookie. + * + * @see #setValue(String) + */ + + public String getValue() { + return value; + } + + /** + * Returns the version of the protocol this cookie complies with. Version 1 complies with RFC 2109, and version 0 + * complies with the original cookie specification drafted by Netscape. Cookies provided by a browser use and + * identify the browser's cookie version. + * + * @see #setVersion(int) + */ + public int getVersion() { + return version; + } + + /** + * Sets the version of the cookie protocol that this Cookie complies with. + * + *

Version 0 complies with the original Netscape cookie specification. Version 1 complies with RFC 2109. + * + *

Since RFC 2109 is still somewhat new, consider version 1 as experimental; do not use it yet on production + * sites. + * + * @param v 0 if the cookie should comply with the original Netscape specification; 1 if the cookie should + * comply with RFC 2109. + * + * @see #getVersion() + */ + public void setVersion(int v) { + version = v; + } + + /** + * Tests a string and returns true if the string counts as a reserved token in the Java language. + * + * @param value the {@code String} to be tested. + * + * @return {@code true} if the {@code String} is a reserved token; {@code false} otherwise. + */ + private boolean isToken(String value) { + int len = value.length(); + for (int i = 0; i < len; i++) { + char c = value.charAt(i); + if (c < 0x20 || c >= 0x7f || TSPECIALS.indexOf(c) != -1) { + return false; + } + } + + return true; + } + + @Override + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e.getMessage()); + } + } + + /** + * Marks or unmarks this Cookie as HttpOnly. + * + *

If isHttpOnly is set to true, this cookie is marked as HttpOnly, by adding the + * HttpOnly attribute to it. + * + *

HttpOnly cookies are not supposed to be exposed to client-side scripting code, and may therefore help + * mitigate certain kinds of cross-site scripting attacks. + * + * @param isHttpOnly true if this cookie is to be marked as HttpOnly, false otherwise. + */ + public void setHttpOnly(boolean isHttpOnly) { + this.isHttpOnly = isHttpOnly; + } + + /** + * Checks whether this Cookie has been marked as HttpOnly. + * + * @return true if this Cookie has been marked as HttpOnly, false otherwise. + */ + public boolean isHttpOnly() { + return isHttpOnly; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/cookie/CookieProcessor.java b/api/src/main/java/com/yanzhenjie/andserver/http/cookie/CookieProcessor.java new file mode 100644 index 0000000000000000000000000000000000000000..0c30bad639a8d79794002fc691726b232133c0be --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/cookie/CookieProcessor.java @@ -0,0 +1,47 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http.cookie; + + + + +import org.apache.httpcore.Header; + +import java.util.List; + +/** + * Created by Zhenjie Yan on 2018/7/27. + */ +public interface CookieProcessor { + + /** + * Parse the provided headers into server cookie objects. + * + * @param headers the HTTP headers to parse. + */ + + List parseCookieHeader(Header[] headers); + + /** + * Generate the {@code Set-Cookie} HTTP header value for the given {@code Cookie}. + * + * @param cookie the cookie for which the header will be generated. + * + * @return the header value in a form that can be added directly to the response. + */ + + String generateHeader(Cookie cookie); +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/cookie/StandardCookieProcessor.java b/api/src/main/java/com/yanzhenjie/andserver/http/cookie/StandardCookieProcessor.java new file mode 100644 index 0000000000000000000000000000000000000000..7d5616c97661f4ccc74a7acfce48083dd17c6aa0 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/cookie/StandardCookieProcessor.java @@ -0,0 +1,215 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http.cookie; + +import com.yanzhenjie.andserver.util.StringUtils; +import org.apache.httpcore.Header; + +import java.text.DateFormat; +import java.text.FieldPosition; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * Created by Zhenjie Yan on 2018/7/27. + */ +public class StandardCookieProcessor implements CookieProcessor { + + private static final String COOKIE_DATE_PATTERN = "EEE, dd-MMM-yyyy HH:mm:ss z"; + + private static final ThreadLocal COOKIE_DATE_FORMAT = new ThreadLocal() { + @Override + protected DateFormat initialValue() { + DateFormat df = new SimpleDateFormat(COOKIE_DATE_PATTERN, Locale.US); + df.setTimeZone(TimeZone.getTimeZone("GMT")); + return df; + } + }; + + private static final String ANCIENT_DATE = COOKIE_DATE_FORMAT.get().format(new Date(10000)); + private static final BitSet DOMAIN_VALID = new BitSet(128); + + static { + for (char c = '0'; c <= '9'; c++) { + DOMAIN_VALID.set(c); + } + for (char c = 'a'; c <= 'z'; c++) { + DOMAIN_VALID.set(c); + } + for (char c = 'A'; c <= 'Z'; c++) { + DOMAIN_VALID.set(c); + } + DOMAIN_VALID.set('.'); + DOMAIN_VALID.set('-'); + } + + + @Override + public List parseCookieHeader(Header[] headers) { + if (headers == null || headers.length == 0) { + return Collections.emptyList(); + } + + List cookieList = new ArrayList<>(); + for (Header header: headers) { + String name = header.getName(); + if ("Cookie".equalsIgnoreCase(name)) { + String headerValue = header.getValue(); + parserCookieValue(headerValue, cookieList); + } + } + return cookieList; + } + + private void parserCookieValue(String headerValue, List cookieList) { + StringTokenizer tokenizer = new StringTokenizer(headerValue, ";"); + while (tokenizer.hasMoreTokens()) { + String segment = tokenizer.nextToken(); + int split = segment.indexOf("="); + int valueIndex = split + 1; + if (split > 0 && valueIndex < segment.length()) { + String name = segment.substring(0, split).trim(); + String value = segment.substring(valueIndex, segment.length()).trim(); + cookieList.add(new Cookie(name, value)); + } + } + } + + + @Override + public String generateHeader( Cookie cookie) { + // Can't use StringBuilder due to DateFormat. + StringBuffer header = new StringBuffer(); + header.append(cookie.getName()); + header.append('='); + String value = cookie.getValue(); + if (!StringUtils.isEmpty(value)) { + validateCookieValue(value); + header.append(value); + } + + // RFC 6265 prefers Max-Age to Expires but... (see below). + int maxAge = cookie.getMaxAge(); + if (maxAge > -1) { + // Negative Max-Age is equivalent to no Max-Age. + header.append("; Max-Age="); + header.append(maxAge); + + // Microsoft IE and Microsoft Edge don't understand Max-Age so send + // expires as well. Without this, persistent cookies fail with those + // browsers. See http://tomcat.markmail.org/thread/g6sipbofsjossacn. + + // Wdy, DD-Mon-YY HH:MM:SS GMT (Expires Netscape format). + header.append("; Expires="); + // To expire immediately we need to set the time in past. + if (maxAge == 0) { + header.append(ANCIENT_DATE); + } else { + Date date = new Date(System.currentTimeMillis() + maxAge * 1000L); + COOKIE_DATE_FORMAT.get().format(date, header, new FieldPosition(0)); + } + } + + String domain = cookie.getDomain(); + if (domain != null && domain.length() > 0) { + validateDomain(domain); + header.append("; Domain="); + header.append(domain); + } + + String path = cookie.getPath(); + if (path != null && path.length() > 0) { + validatePath(path); + header.append("; Path="); + header.append(path); + } + + if (cookie.getSecure()) { + header.append("; Secure"); + } + + if (cookie.isHttpOnly()) { + header.append("; HttpOnly"); + } + + return header.toString(); + } + + + private void validateCookieValue(String value) { + int start = 0; + int end = value.length(); + + if (end > 1 && value.charAt(0) == '"' && value.charAt(end - 1) == '"') { + start = 1; + end--; + } + + char[] chars = value.toCharArray(); + for (int i = start; i < end; i++) { + char c = chars[i]; + if (c < 0x21 || c == 0x22 || c == 0x2c || c == 0x3b || c == 0x5c || c == 0x7f) { + String message = String.format("The cookie's value [%1$s] is invalid.", value); + throw new IllegalArgumentException(message); + } + } + } + + + private void validateDomain(String domain) { + int i = 0; + int prev = -1; + int cur = -1; + char[] chars = domain.toCharArray(); + while (i < chars.length) { + prev = cur; + cur = chars[i]; + if (!DOMAIN_VALID.get(cur)) { + String message = String.format("The cookie's domain [%1$s] is invalid.", domain); + throw new IllegalArgumentException(message); + } + // labels must start with a letter or number. + if ((prev == '.' || prev == -1) && (cur == '.' || cur == '-')) { + String message = String.format("The cookie's domain [%1$s] is invalid.", domain); + throw new IllegalArgumentException(message); + } + // labels must end with a letter or number. + if (prev == '-' && cur == '.') { + String message = String.format("The cookie's domain [%1$s] is invalid.", domain); + throw new IllegalArgumentException(message); + } + i++; + } + // domain must end with a label. + if (cur == '.' || cur == '-') { + String message = String.format("The cookie's domain [%1$s] is invalid.", domain); + throw new IllegalArgumentException(message); + } + } + + + private void validatePath(String path) { + char[] chars = path.toCharArray(); + + for (int i = 0; i < chars.length; i++) { + char ch = chars[i]; + if (ch < 0x20 || ch > 0x7E || ch == ';') { + String message = String.format("The cookie's path [%1$s] is invalid.", path); + throw new IllegalArgumentException(message); + } + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/multipart/BodyContext.java b/api/src/main/java/com/yanzhenjie/andserver/http/multipart/BodyContext.java new file mode 100644 index 0000000000000000000000000000000000000000..39867610049d82e32de6f0eb2bdbcc07e64a256c --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/multipart/BodyContext.java @@ -0,0 +1,69 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http.multipart; + + + +import com.yanzhenjie.andserver.http.RequestBody; +import com.yanzhenjie.andserver.util.MediaType; +import org.apache.commons.fileupload.UploadContext; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Created by Zhenjie Yan on 2018/8/9. + */ +public class BodyContext implements UploadContext { + + private final RequestBody mBody; + + public BodyContext( RequestBody body) { + this.mBody = body; + } + + @Override + public String getCharacterEncoding() { + return mBody.contentEncoding(); + } + + @Override + public String getContentType() { + MediaType contentType = mBody.contentType(); + return contentType == null ? null : contentType.toString(); + } + + @Override + public int getContentLength() { + long contentLength = contentLength(); + return contentLength > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) contentLength; + } + + @Override + public long contentLength() { + return mBody.length(); + } + + @Override + public InputStream getInputStream() throws IOException { + return mBody.stream(); + } + + @Override + public String toString() { + return String.format("ContentLength=%s, Mime=%s", contentLength(), getContentType()); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/multipart/MultipartFile.java b/api/src/main/java/com/yanzhenjie/andserver/http/multipart/MultipartFile.java new file mode 100644 index 0000000000000000000000000000000000000000..8868aba43fecd4d4f110533c5f467d86ee07d218 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/multipart/MultipartFile.java @@ -0,0 +1,99 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http.multipart; + + +import com.yanzhenjie.andserver.util.MediaType; +import org.apache.commons.fileupload.FileItem; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +/** + * Created by Zhenjie Yan on 2018/6/18. + */ +public interface MultipartFile { + + /** + * Return the name of the parameter in the multipart form. + * + * @return the name of the parameter. + */ + + String getName(); + + /** + * Return the original filename in the client's filesystem. + * + * @return the original filename, or the empty String if no file, or null if not defined. + * + * @see FileItem#getName() + */ + + String getFilename(); + + /** + * Return the content type of the file. + * + * @return the content type, or the empty String if no file, or null if not defined. + */ + + MediaType getContentType(); + + /** + * Return whether the uploaded file is empty. + * + * @return true, otherwise is false. + */ + boolean isEmpty(); + + /** + * Return the size of the file in bytes. + * + * @return the size of the file, or 0 if empty. + */ + long getSize(); + + /** + * Return the contents of the file as an array of bytes. + * + * @return the contents of the file as bytes, or an empty byte array if empty. + * + * @throws IOException in case of access errors (if the temporary store fails). + */ + byte[] getBytes() throws IOException; + + /** + * Return an {@code InputStream} to read the contents of the file from. + * + * @return the contents of the file as stream, or an empty stream if empty. + * + * @throws IOException in case of access errors. + */ + + InputStream getStream() throws IOException; + + /** + * Writing the received file to the given destination file. If the destination file already exists, it will be + * deleted first. + * + *

If the target file has been written to disk, this operation cannot be invoked again afterwards. + * + * @param dest the destination file. + */ + void transferTo(File dest) throws IOException, IllegalStateException; +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/multipart/MultipartRequest.java b/api/src/main/java/com/yanzhenjie/andserver/http/multipart/MultipartRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..f80b4bdfe7621d8dd10d4d49ddb9ff442c7331ef --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/multipart/MultipartRequest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http.multipart; + +import com.yanzhenjie.andserver.http.HttpRequest; +import com.yanzhenjie.andserver.util.MultiValueMap; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + *

This interface defines the multipart request access operations that are exposed for actual multipart requests. + *

+ * + * Created by Zhenjie Yan on 2018/6/21. + */ +public interface MultipartRequest extends HttpRequest { + + /** + *

Return an {@link Iterator} of String objects containing the parameter names of the multipart files + * contained in this request. These are the field names of the form (like with normal parameters), not the original + * file names.

+ * + * @return the names of the files. + */ + + Iterator getFileNames(); + + /** + * Return the contents plus description of an uploaded file in this request, or null if it does not exist. + * + * @param name parameter name. + * + * @return a {@link MultipartFile} object. + */ + + MultipartFile getFile(String name); + + /** + * Return the contents plus description of uploaded files in this request, or an empty list if it does not exist. + * + * @param name parameter name. + * + * @return a {@link MultipartFile} list. + */ + + List getFiles(String name); + + /** + * Return a {@link Map} of the multipart files contained in this request. + * + * @return a map containing the parameter names as keys, and the {@link MultipartFile} objects as values. + */ + + Map getFileMap(); + + /** + * Return a {@link MultiValueMap} of the multipart files contained in this request. + * + * @return a map containing the parameter names as keys, and a list of {@link MultipartFile} objects as values. + */ + + MultiValueMap getMultiFileMap(); + + /** + * Determine the content type of the specified request part. + * + * @param paramOrFileName the name of the part. + * + * @return the associated content type, or null if not defined. + */ + + String getMultipartContentType(String paramOrFileName); +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/multipart/MultipartResolver.java b/api/src/main/java/com/yanzhenjie/andserver/http/multipart/MultipartResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..ef49fd49c7825a58dddc6bf9f9efac5685b868ff --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/multipart/MultipartResolver.java @@ -0,0 +1,91 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http.multipart; + +import com.yanzhenjie.andserver.error.MultipartException; +import com.yanzhenjie.andserver.http.HttpRequest; +import org.apache.commons.fileupload.FileUpload; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; + +import java.io.File; + +/** + * Created by Zhenjie Yan on 2018/8/8. + */ +public interface MultipartResolver { + + /** + * Set the maximum size (in bytes) allowed for uploading. -1 indicates no limit (the default). + * + * @param allFileMaxSize the maximum upload size allowed. + * + * @see FileUpload#setSizeMax(long) + */ + void setAllFileMaxSize(long allFileMaxSize); + + /** + * Set the maximum size (in bytes) allowed for each individual file. -1 indicates no limit (the default). + * + * @param fileMaxSize the maximum upload size per file. + * + * @see FileUpload#setFileSizeMax(long) + */ + void setFileMaxSize(long fileMaxSize); + + /** + * Set the maximum allowed size (in bytes) before uploads are written to disk, default is 10240. + * + * @param maxInMemorySize the maximum in memory size allowed. + * + * @see DiskFileItemFactory#setSizeThreshold(int) + */ + void setMaxInMemorySize(int maxInMemorySize); + + /** + * Set the temporary directory where uploaded files get stored. + */ + void setUploadTempDir(File uploadTempDir); + + /** + * Determine if the given request contains multipart content, will typically check for content type + * "multipart/form-data". + * + * @param request the request to be evaluated. + * + * @return whether the request contains multipart content. + */ + boolean isMultipart(HttpRequest request); + + /** + * Parse the given request into multipart files and parameters, and wrap the request inside a {@link + * MultipartRequest} object that provides access to file descriptors and makes contained parameters accessible via + * the standard HttpRequest methods. + * + * @param request the request to wrap (must be of a multipart content type). + * + * @return the wrapped request. + * + * @throws MultipartException if the request is not multipart, or encounter other problems. + */ + MultipartRequest resolveMultipart(HttpRequest request) throws MultipartException; + + /** + * Cleanup any resources used for the multipart handling, like a storage for the uploaded files. + * + * @param request the request to cleanup resources for. + */ + void cleanupMultipart(MultipartRequest request); +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/multipart/StandardMultipartFile.java b/api/src/main/java/com/yanzhenjie/andserver/http/multipart/StandardMultipartFile.java new file mode 100644 index 0000000000000000000000000000000000000000..ac55895ae0f7c36879a6a2e7b4b243fb558023b7 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/multipart/StandardMultipartFile.java @@ -0,0 +1,168 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http.multipart; + +import com.yanzhenjie.andserver.util.IOUtils; +import com.yanzhenjie.andserver.util.MediaType; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.fileupload.disk.DiskFileItem; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; + +/** + * Created by Zhenjie Yan on 2018/6/21. + */ +public class StandardMultipartFile implements MultipartFile, Serializable { + + private final FileItem fileItem; + private final long size; + + /** + * Create an instance wrapping the given FileItem. + * + * @param fileItem the FileItem to wrap. + */ + public StandardMultipartFile(FileItem fileItem) { + this.fileItem = fileItem; + this.size = fileItem.getSize(); + } + + /** + * Return the underlying {@link FileItem} instance. There is hardly any need to access this. + */ + public final FileItem getFileItem() { + return fileItem; + } + + + @Override + public String getName() { + return this.fileItem.getFieldName(); + } + + + @Override + public String getFilename() { + String filename = this.fileItem.getName(); + if (filename == null) { + // Should never happen. + return ""; + } + + // Check for Unix-style path. + int unixSep = filename.lastIndexOf("/"); + // Check for Windows-style path. + int winSep = filename.lastIndexOf("\\"); + // Cut off at latest possible point. + int pos = (winSep > unixSep ? winSep : unixSep); + if (pos != -1) { + // Any sort of path separator found... + return filename.substring(pos + 1); + } else { + // A plain name. + return filename; + } + } + + + @Override + public MediaType getContentType() { + String mimeType = fileItem.getContentType(); + MediaType mediaType; + try { + mediaType = MediaType.parseMediaType(mimeType); + } catch (Exception e) { + mediaType = MediaType.APPLICATION_OCTET_STREAM; + } + return mediaType; + } + + @Override + public boolean isEmpty() { + return size == 0; + } + + @Override + public long getSize() { + return size; + } + + @Override + public byte[] getBytes() { + if (!isAvailable()) { + throw new IllegalStateException("File has been moved - cannot be read again."); + } + byte[] bytes = this.fileItem.get(); + return (bytes != null ? bytes : new byte[0]); + } + + + @Override + public InputStream getStream() throws IOException { + if (!isAvailable()) { + throw new IllegalStateException("File has been moved - cannot be read again."); + } + InputStream inputStream = fileItem.getInputStream(); + return (inputStream != null ? inputStream : IOUtils.createEmptyInput()); + } + + @Override + public void transferTo( File dest) throws IOException, IllegalStateException { + if (!isAvailable()) { + throw new IllegalStateException("File has already been moved - cannot be transferred again."); + } + + if (dest.exists() && !dest.delete()) { + throw new IOException( + "Destination file [" + dest.getAbsolutePath() + "] already exists and could not be deleted."); + } + + try { + fileItem.write(dest); + } catch (FileUploadException ex) { + throw new IllegalStateException(ex.getMessage(), ex); + } catch (IllegalStateException ex) { + // Pass through when coming from FileItem directly. + throw ex; + } catch (IOException ex) { + // From I/O operations within FileItem.write. + throw ex; + } catch (Exception ex) { + throw new IOException("File transfer failed", ex); + } + } + + /** + * Determine whether the multipart content is still available. If a temporary file has been moved, the content is no + * longer available. + */ + protected boolean isAvailable() { + // If in memory, it's available. + if (fileItem.isInMemory()) { + return true; + } + // Check actual existence of temporary file. + if (fileItem instanceof DiskFileItem) { + return ((DiskFileItem) fileItem).getStoreLocation().exists(); + } + // Check whether current file size is different than original one. + return (fileItem.getSize() == size); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/multipart/StandardMultipartRequest.java b/api/src/main/java/com/yanzhenjie/andserver/http/multipart/StandardMultipartRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..783d0da5eb1e119d2c75249c68da5189e6381303 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/multipart/StandardMultipartRequest.java @@ -0,0 +1,124 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http.multipart; + + +import com.yanzhenjie.andserver.http.HttpRequest; +import com.yanzhenjie.andserver.http.RequestWrapper; +import com.yanzhenjie.andserver.util.LinkedMultiValueMap; +import com.yanzhenjie.andserver.util.MultiValueMap; + +import java.util.*; + +/** + * Created by Zhenjie Yan on 2018/8/9. + */ +public class StandardMultipartRequest extends RequestWrapper implements MultipartRequest { + + private HttpRequest mRequest; + + private MultiValueMap mMultipartFiles; + private MultiValueMap mMultipartParameters; + private Map mMultipartContentTypes; + + public StandardMultipartRequest( HttpRequest request, MultiValueMap mpFiles, + MultiValueMap mpParams, + Map mpContentTypes) { + super(request); + this.mRequest = request; + this.mMultipartFiles = new LinkedMultiValueMap<>(Collections.unmodifiableMap(mpFiles)); + this.mMultipartParameters = new LinkedMultiValueMap<>(Collections.unmodifiableMap(mpParams)); + this.mMultipartContentTypes = Collections.unmodifiableMap(mpContentTypes); + } + + + @Override + public Iterator getFileNames() { + return mMultipartFiles.keySet().iterator(); + } + + + @Override + public MultipartFile getFile(String name) { + return mMultipartFiles.getFirst(name); + } + + + @Override + public List getFiles(String name) { + List multipartFiles = mMultipartFiles.get(name); + if (multipartFiles != null) { + return multipartFiles; + } else { + return Collections.emptyList(); + } + } + + + @Override + public Map getFileMap() { + return mMultipartFiles.toSingleValueMap(); + } + + + @Override + public MultiValueMap getMultiFileMap() { + return mMultipartFiles; + } + + + @Override + public String getMultipartContentType(String paramOrFileName) { + MultipartFile file = getFile(paramOrFileName); + return file == null ? mMultipartContentTypes.get(paramOrFileName) : file.getContentType().getType(); + } + + + @Override + public List getParameterNames() { + if (mMultipartParameters.isEmpty()) { + return mRequest.getParameterNames(); + } + + List paramNames = new LinkedList<>(); + List names = mRequest.getParameterNames(); + if (!names.isEmpty()) { + paramNames.addAll(names); + } + paramNames.addAll(mMultipartParameters.keySet()); + return paramNames; + } + + + @Override + public String getParameter( String name) { + String value = mMultipartParameters.getFirst(name); + return value.isEmpty() ? mRequest.getParameter(name) : value; + } + + + @Override + public List getParameters( String name) { + List values = mMultipartParameters.get(name); + return values == null ? mRequest.getParameters(name) : values; + } + + + @Override + public MultiValueMap getParameter() { + return mMultipartParameters.isEmpty() ? mRequest.getParameter() : mMultipartParameters; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/multipart/StandardMultipartResolver.java b/api/src/main/java/com/yanzhenjie/andserver/http/multipart/StandardMultipartResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..2f9fd100aa0d458d9e31fa2d4554f580335e691b --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/multipart/StandardMultipartResolver.java @@ -0,0 +1,287 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http.multipart; + +import com.yanzhenjie.andserver.AndServer; +import com.yanzhenjie.andserver.error.MaxUploadSizeExceededException; +import com.yanzhenjie.andserver.error.MultipartException; +import com.yanzhenjie.andserver.http.HttpRequest; +import com.yanzhenjie.andserver.http.RequestBody; +import com.yanzhenjie.andserver.util.Assert; +import com.yanzhenjie.andserver.util.LinkedMultiValueMap; +import com.yanzhenjie.andserver.util.MediaType; +import com.yanzhenjie.andserver.util.MultiValueMap; +import ohos.hiviewdfx.HiLog; +import ohos.hiviewdfx.HiLogLabel; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileUpload; +import org.apache.commons.fileupload.FileUploadBase; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.io.Charsets; + +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * Created by Zhenjie Yan on 2018/8/9. + */ +public class StandardMultipartResolver implements MultipartResolver { + + private DiskFileItemFactory mFileItemFactory; + private FileUpload mFileUpload; + + public StandardMultipartResolver() { + this.mFileItemFactory = new DiskFileItemFactory(); + this.mFileUpload = new FileUpload(mFileItemFactory); + } + + @Override + public void setAllFileMaxSize(long allFileMaxSize) { + this.mFileUpload.setSizeMax(allFileMaxSize); + } + + @Override + public void setFileMaxSize(long fileMaxSize) { + this.mFileUpload.setFileSizeMax(fileMaxSize); + } + + @Override + public void setMaxInMemorySize(int maxInMemorySize) { + this.mFileItemFactory.setSizeThreshold(maxInMemorySize); + } + + @Override + public void setUploadTempDir(File uploadTempDir) { + if (!uploadTempDir.exists() && !uploadTempDir.mkdirs()) { + String message = "Given uploadTempDir [" + uploadTempDir + "] could not be created."; + throw new IllegalArgumentException(message); + } + this.mFileItemFactory.setRepository(uploadTempDir); + } + + @Override + public boolean isMultipart(HttpRequest request) { + if (!request.getMethod().allowBody()) { + return false; + } + + RequestBody body = request.getBody(); + return body != null && FileUploadBase.isMultipartContent(new BodyContext(body)); + } + + @Override + public MultipartRequest resolveMultipart(HttpRequest request) throws MultipartException { + if (request instanceof MultipartRequest) { + return (MultipartRequest) request; + } + + MultipartParsingResult result = parseRequest(request); + return new StandardMultipartRequest(request, result.getMultipartFiles(), result.getMultipartParameters(), + result.getMultipartContentTypes()); + } + + @Override + public void cleanupMultipart(MultipartRequest request) { + if (request != null) { + try { + MultiValueMap multipartFiles = request.getMultiFileMap(); + for (List files: multipartFiles.values()) { + for (MultipartFile file: files) { + if (file instanceof StandardMultipartFile) { + StandardMultipartFile cmf = (StandardMultipartFile) file; + cmf.getFileItem().delete(); + } + } + } + } catch (Throwable ex) { + HiLog.warn(new HiLogLabel(0,1, AndServer.TAG),"Failed to perform multipart cleanup for servlet request."); + } + } + } + + /** + * Parse the given request, resolving its multipart elements. + * + * @param request the request to parse. + * + * @return the parsing result. + * + * @throws MultipartException if multipart resolution failed. + */ + private MultipartParsingResult parseRequest(HttpRequest request) throws MultipartException { + String encoding = determineEncoding(request); + FileUpload fileUpload = prepareFileUpload(encoding); + try { + RequestBody body = request.getBody(); + Assert.notNull(body, "The body cannot be null."); + List fileItems = fileUpload.parseRequest(new BodyContext(body)); + return parseFileItems(fileItems, encoding); + } catch (FileUploadBase.SizeLimitExceededException ex) { + throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex); + } catch (FileUploadBase.FileSizeLimitExceededException ex) { + throw new MaxUploadSizeExceededException(fileUpload.getFileSizeMax(), ex); + } catch (FileUploadException ex) { + throw new MultipartException("Failed to parse multipart servlet request.", ex); + } + } + + /** + * Determine the encoding for the given request. + * + *

The default implementation checks the request encoding, falling back to the default encoding specified for + * this resolver. + * + * @param request current request + * + * @return the encoding for the request . + */ + + private String determineEncoding(HttpRequest request) { + MediaType mimeType = request.getContentType(); + if (mimeType == null) { + return StandardCharsets.UTF_8.name(); + } + Charset charset = mimeType.getCharset(); + return charset == null ? StandardCharsets.UTF_8.name() : charset.name(); + } + + /** + * Determine an appropriate FileUpload instance for the given encoding. + * + *

Default implementation returns the shared FileUpload instance if the encoding matches, else creates a new + * FileUpload instance with the same configuration other than the desired encoding. + * + * @param encoding the character encoding to use. + * + * @return an appropriate FileUpload instance. + */ + private FileUpload prepareFileUpload( String encoding) { + FileUpload actualFileUpload = mFileUpload; + if (!encoding.equalsIgnoreCase(mFileUpload.getHeaderEncoding())) { + actualFileUpload = new FileUpload(mFileItemFactory); + actualFileUpload.setSizeMax(mFileUpload.getSizeMax()); + actualFileUpload.setFileSizeMax(mFileUpload.getFileSizeMax()); + actualFileUpload.setHeaderEncoding(encoding); + } + return actualFileUpload; + } + + /** + * Parse the given List of Commons FileItems into a MultipartParsingResult, containing MultipartFile instances and a + * Map of multipart parameter. + * + * @param fileItems the Commons FileItems to parse. + * @param encoding the encoding to use for form fields. + * + * @return the MultipartParsingResult. + * + * @see StandardMultipartFile#StandardMultipartFile(FileItem) + */ + protected MultipartParsingResult parseFileItems(List fileItems, String encoding) { + MultiValueMap multipartFiles = new LinkedMultiValueMap<>(); + MultiValueMap multipartParameters = new LinkedMultiValueMap<>(); + Map multipartContentTypes = new HashMap<>(); + + // Extract multipart files and multipart parameters. + for (FileItem fileItem: fileItems) { + if (fileItem.isFormField()) { + String value; + String partEncoding = determineEncoding(fileItem.getContentType(), encoding); + if (partEncoding != null) { + try { + value = fileItem.getString(partEncoding); + } catch (UnsupportedEncodingException ex) { + value = fileItem.getString(); + } + } else { + value = fileItem.getString(); + } + List curParam = multipartParameters.get(fileItem.getFieldName()); + if (curParam == null) { + // Simple form field. + curParam = new LinkedList<>(); + curParam.add(value); + multipartParameters.put(fileItem.getFieldName(), curParam); + } else { + // Array of simple form fields. + curParam.add(value); + } + multipartContentTypes.put(fileItem.getFieldName(), fileItem.getContentType()); + } else { + StandardMultipartFile file = createMultipartFile(fileItem); + multipartFiles.add(file.getName(), file); + } + } + return new MultipartParsingResult(multipartFiles, multipartParameters, multipartContentTypes); + } + + /** + * Create a {@link StandardMultipartFile} wrapper for the given Commons {@link FileItem}. + * + * @param fileItem the Commons FileItem to wrap. + * + * @return the corresponding StandardMultipartFile (potentially a custom subclass). + */ + protected StandardMultipartFile createMultipartFile(FileItem fileItem) { + return new StandardMultipartFile(fileItem); + } + + private String determineEncoding(String contentTypeHeader, String defaultEncoding) { + if (contentTypeHeader.isEmpty()) { + return defaultEncoding; + } + MediaType contentType = MediaType.parseMediaType(contentTypeHeader); + Charset charset = contentType.getCharset(); + return charset != null ? charset.name() : defaultEncoding; + } + + /** + * Holder for a Map of MultipartFiles and a Map of multipart parameters. + */ + protected static class MultipartParsingResult { + + private final MultiValueMap multipartFiles; + private final MultiValueMap multipartParameters; + private final Map multipartContentTypes; + + public MultipartParsingResult(MultiValueMap mpFiles, + MultiValueMap mpParams, + Map mpParamContentTypes) { + this.multipartFiles = mpFiles; + this.multipartParameters = mpParams; + this.multipartContentTypes = mpParamContentTypes; + } + + public MultiValueMap getMultipartFiles() { + return this.multipartFiles; + } + + public MultiValueMap getMultipartParameters() { + return this.multipartParameters; + } + + public Map getMultipartContentTypes() { + return this.multipartContentTypes; + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/session/IdGenerator.java b/api/src/main/java/com/yanzhenjie/andserver/http/session/IdGenerator.java new file mode 100644 index 0000000000000000000000000000000000000000..7430e082d9f5550dd2fdd868e3c39ec96db78ab9 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/session/IdGenerator.java @@ -0,0 +1,32 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http.session; + + + +/** + * Created by Zhenjie Yan on 2018/7/26. + */ +public interface IdGenerator { + + /** + * Generate and return a new identifier. + * + * @return the newly generated id. + */ + + String generateId(); +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/session/Session.java b/api/src/main/java/com/yanzhenjie/andserver/http/session/Session.java new file mode 100644 index 0000000000000000000000000000000000000000..254dcc6590fefede0320fe0e367618e5e08f50f4 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/session/Session.java @@ -0,0 +1,119 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http.session; + + + + +import java.util.Enumeration; + +/** + * Created by Zhenjie Yan on 2018/7/13. + */ +public interface Session { + + /** + * Returns a string containing the unique identifier assigned to this session. + */ + + String getId(); + + /** + * Returns the time when this session was created, measured in milliseconds since midnight January 1, 1970 GMT. + */ + long getCreatedTime(); + + /** + * Returns the last time the client sent a request associated with this session, as the number of milliseconds since + * midnight January 1, 1970 GMT, and marked by the time the container received the request. + * + *

Actions that your application takes, such as getting or setting a value associated with the session, do not + * affect the access time. + */ + long getLastAccessedTime(); + + /** + * Specifies the time, in seconds, between client requests before the server will invalidate this session. + * + *

An interval value of zero or less indicates that the session should never timeout. + * + * @param interval an integer specifying the number of seconds. + */ + void setMaxInactiveInterval(int interval); + + /** + * Returns the maximum time interval, in seconds, that the server will keep this session open between client + * accesses. After this interval, the server will invalidate the session. The maximum time interval can be set with + * the {@link #setMaxInactiveInterval(int)} method. + * + *

A return value of zero or less indicates that the session will never timeout. + */ + int getMaxInactiveInterval(); + + /** + * Returns the object bound with the specified name in this session, or {@code null} if no object is bound under the + * name. + * + * @param name a string specifying the name of the object. + */ + + Object getAttribute(String name); + + /** + * Returns an {@code Enumeration} of {@code String} objects containing the names of all the objects bound to this + * session. + */ + + Enumeration getAttributeNames(); + + /** + * Binds an object to this session, using the name specified. If an object of the same name is already bound to the + * session, the object is replaced. + * + *

If the value passed in is null, this has the same effect as calling {@link #removeAttribute(String)}. + * + * @param name the name to which the object is bound, cannot be null. + * @param value the object to be bound. + */ + void setAttribute(String name, Object value); + + /** + * Removes the object bound with the specified name from this session. If the session does not have an object bound + * with the specified name, this method does nothing. + * + * @param name the name of the object to remove from this session. + */ + void removeAttribute(String name); + + /** + * Invalidates this session then unbinds any objects bound to it. + */ + void invalidate(); + + /** + * Returns {@code true} if the client does not yet know about the session or if the client chooses not to join the + * session. E.g. if the server used only cookie-based sessions, and the client had disabled the use of cookies, then + * a session would be new on each request. + * + * @return {@code true} if the server has created a session, but the client has not yet joined. + */ + boolean isNew(); + + /** + * @return returns {@code true} is the session is valid, or false if the session is invalid. + */ + boolean isValid(); +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/session/SessionManager.java b/api/src/main/java/com/yanzhenjie/andserver/http/session/SessionManager.java new file mode 100644 index 0000000000000000000000000000000000000000..5f58255f9cb74903ef9ff9ad2b9f44c1dadd1e48 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/session/SessionManager.java @@ -0,0 +1,71 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http.session; + + + + +import java.io.IOException; + +/** + * Created by Zhenjie Yan on 2018/7/26. + */ +public interface SessionManager { + + /** + * Add this session to the set of active sessions for this {@code SessionManager}. + * + * @param session session to be added. + * + * @throws IOException if an output error occurs while processing this request. + */ + void add(Session session) throws IOException; + + /** + * Change the session ID of the current session to a new randomly generated session ID. + * + * @param session the session to change the session ID for. + */ + void changeSessionId(Session session); + + /** + * Create a new session object. + * + * @return an empty session object. + */ + + Session createSession(); + + /** + * Return the active session, with the specified session id (if any); otherwise return {@code null}. + * + * @param id the session id for the session to be returned. + * + * @return the request session or {@code null}. + * + * @throws IllegalStateException if a new session cannot be instantiated for any reason. + * @throws IOException if an output error occurs while processing this request. + */ + + Session findSession(String id) throws IOException, ClassNotFoundException; + + /** + * Remove this session from the active sessions for this {@code SessionManager}. + * + * @param session session to be removed. + */ + void remove(Session session); +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/session/StandardIdGenerator.java b/api/src/main/java/com/yanzhenjie/andserver/http/session/StandardIdGenerator.java new file mode 100644 index 0000000000000000000000000000000000000000..87c9d30fd041df6629ab10df98725a368f159935 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/session/StandardIdGenerator.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http.session; + + + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +public class StandardIdGenerator implements IdGenerator { + + private static final int ID_LENGTH = 30; + + private SecureRandom mRandom; + + public StandardIdGenerator() { + this.mRandom = createSecureRandom(); + } + + + @Override + public String generateId() { + byte random[] = new byte[16]; + + // Render the result as a String of hexadecimal digits. + // Start with enough space for sessionIdLength and medium route size. + StringBuilder buffer = new StringBuilder(2 * ID_LENGTH + 20); + + int resultLenBytes = 0; + + while (resultLenBytes < ID_LENGTH) { + mRandom.nextBytes(random); + for (int j = 0; j < random.length && resultLenBytes < ID_LENGTH; j++) { + byte b1 = (byte) ((random[j] & 0xf0) >> 4); + byte b2 = (byte) (random[j] & 0x0f); + if (b1 < 10) { + buffer.append((char) ('0' + b1)); + } else { + buffer.append((char) ('A' + (b1 - 10))); + } + if (b2 < 10) { + buffer.append((char) ('0' + b2)); + } else { + buffer.append((char) ('A' + (b2 - 10))); + } + resultLenBytes++; + } + } + return buffer.toString(); + } + + /** + * Create a new random number generator instance we should use for generating session identifiers. + */ + private SecureRandom createSecureRandom() { + SecureRandom result; + try { + result = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + result = new SecureRandom(); + } + + // Force seeding to take place. + result.nextInt(); + return result; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/session/StandardSession.java b/api/src/main/java/com/yanzhenjie/andserver/http/session/StandardSession.java new file mode 100644 index 0000000000000000000000000000000000000000..f3507e22941a4a67e8c836330d2d4d71bd2559e4 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/session/StandardSession.java @@ -0,0 +1,228 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http.session; + + +import com.yanzhenjie.andserver.util.Assert; +import com.yanzhenjie.andserver.util.StringUtils; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Created by Zhenjie Yan on 2018/7/26. + */ +public class StandardSession implements Session { + + private static final String EMPTY_ARRAY[] = new String[0]; + private String id; + private long createdTime; + private long lastAccessedTime; + private int maxInactiveInterval = -1; + private Map mAttributes = new ConcurrentHashMap<>(); + private boolean isNew; + private boolean isValid; + + public StandardSession() { + } + + public void setId( String id) { + if (StringUtils.isEmpty(id)) { + throw new IllegalArgumentException("The id can not be empty or null."); + } + this.id = id; + } + + + @Override + public String getId() { + return id; + } + + public void setCreatedTime(long createdTime) { + this.createdTime = createdTime; + } + + @Override + public long getCreatedTime() { + return createdTime; + } + + public void setLastAccessedTime(long lastAccessedTime) { + this.lastAccessedTime = lastAccessedTime; + } + + @Override + public long getLastAccessedTime() { + validate(); + + return lastAccessedTime; + } + + @Override + public void setMaxInactiveInterval(int interval) { + this.maxInactiveInterval = interval; + } + + @Override + public int getMaxInactiveInterval() { + return maxInactiveInterval; + } + + @Override + public Object getAttribute(String name) { + validate(); + + if (name == null) { + return null; + } + return mAttributes.get(name); + } + + + @Override + public Enumeration getAttributeNames() { + validate(); + + return Collections.enumeration(new HashSet<>(mAttributes.keySet())); + } + + @Override + public void setAttribute( String name, Object value) { + validate(); + + Assert.notNull(name, "The name cannot be null."); + + if (value == null) { + return; + } + mAttributes.put(name, value); + } + + @Override + public void removeAttribute(String name) { + validate(); + + if (name == null) { + return; + } + mAttributes.remove(name); + } + + @Override + public void invalidate() { + validate(); + + this.isValid = false; + } + + public void setNew(boolean aNew) { + this.isNew = aNew; + } + + @Override + public boolean isNew() { + validate(); + + return isNew; + } + + private void validate() { + if (!isValid()) { + throw new IllegalStateException("This session is invalid."); + } + } + + public void setValid(boolean valid) { + this.isValid = valid; + } + + @Override + public boolean isValid() { + if (!isValid) { + return false; + } + + if (maxInactiveInterval > 0) { + long inactiveInterval = System.currentTimeMillis() - lastAccessedTime; + int timeIdle = (int) (inactiveInterval / 1000L); + if (timeIdle >= maxInactiveInterval) { + isValid = false; + } + } else { + isValid = true; + } + + return isValid; + } + + /** + * Write attribute values to the stream. + * + * @param stream stream. + * + * @throws IOException if the output error occurs while processing this request. + */ + public void writeObject( ObjectOutputStream stream) throws IOException { + stream.writeObject(id); + stream.writeLong(createdTime); + stream.writeLong(lastAccessedTime); + stream.writeInt(maxInactiveInterval); + stream.writeBoolean(isNew); + stream.writeBoolean(isValid); + stream.writeInt(mAttributes.size()); + String keys[] = mAttributes.keySet().toArray(EMPTY_ARRAY); + for (String key: keys) { + Object value = mAttributes.get(key); + if (value != null && value instanceof Serializable) { + stream.writeObject(key); + stream.writeObject(value); + } + } + } + + /** + * Read attribute values from the stream. + * + * @param stream stream. + * + * @throws IllegalStateException if a new session cannot be instantiated for any reason. + * @throws IOException if the input error occurs while processing this request. + */ + public final void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { + id = (String) stream.readObject(); + createdTime = stream.readLong(); + lastAccessedTime = stream.readLong(); + maxInactiveInterval = stream.readInt(); + isNew = stream.readBoolean(); + isValid = stream.readBoolean(); + + // Deserialize the attribute count and attribute values + int size = stream.readInt(); + for (int i = 0; i < size; i++) { + String name = (String) stream.readObject(); + Object value = stream.readObject(); + mAttributes.put(name, value); + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/session/StandardSessionManager.java b/api/src/main/java/com/yanzhenjie/andserver/http/session/StandardSessionManager.java new file mode 100644 index 0000000000000000000000000000000000000000..83cb53d319386ff2df20fdadaa53a18754086452 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/session/StandardSessionManager.java @@ -0,0 +1,89 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http.session; + +import ohos.app.Context; + +import java.io.File; +import java.io.IOException; + +/** + * Created by Zhenjie Yan on 2018/7/26. + */ +public class StandardSessionManager implements SessionManager { + + private IdGenerator mIdGenerator; + private Store mStore; + + public StandardSessionManager(Context context) { + this.mIdGenerator = new StandardIdGenerator(); + + File sessionDir = new File(context.getCacheDir(), "_andserver_session_"); + this.mStore = new StandardStore(sessionDir); + } + + @Override + public void add( Session session) throws IOException { + if (session instanceof StandardSession && session.isNew()) { + StandardSession standardSession = (StandardSession) session; + standardSession.setNew(false); + mStore.replace(standardSession); + } + } + + @Override + public void changeSessionId( Session session) { + if (session instanceof StandardSession) { + StandardSession standardSession = (StandardSession) session; + standardSession.setId(mIdGenerator.generateId()); + } + } + + + @Override + public Session createSession() { + StandardSession session = newSession(); + session.setId(mIdGenerator.generateId()); + return session; + } + + + @Override + public Session findSession( String id) throws IOException, ClassNotFoundException { + StandardSession session = mStore.getSession(id); + if (session != null) { + session.setLastAccessedTime(System.currentTimeMillis()); + } + return session; + } + + @Override + public void remove( Session session) { + if (session instanceof StandardSession) { + mStore.remove((StandardSession) session); + } + } + + private StandardSession newSession() { + StandardSession session = new StandardSession(); + long currentTime = System.currentTimeMillis(); + session.setCreatedTime(currentTime); + session.setLastAccessedTime(currentTime); + session.setNew(true); + session.setValid(true); + return session; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/session/StandardStore.java b/api/src/main/java/com/yanzhenjie/andserver/http/session/StandardStore.java new file mode 100644 index 0000000000000000000000000000000000000000..6adad190f099ab560fd9d9fd58f30eaf30222370 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/session/StandardStore.java @@ -0,0 +1,101 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http.session; + + +import com.yanzhenjie.andserver.util.Assert; +import com.yanzhenjie.andserver.util.IOUtils; +import com.yanzhenjie.andserver.util.StringUtils; + +import java.io.*; + +/** + * Created by Zhenjie Yan on 2018/7/26. + */ +public class StandardStore implements Store { + + private File mDirectory; + + public StandardStore(File directory) { + this.mDirectory = directory; + } + + @Override + public boolean replace( StandardSession session) throws IOException { + Assert.notNull(session, "The session can not be null."); + + String id = session.getId(); + if (StringUtils.isEmpty(id)) { + throw new IllegalStateException("The session id can not be empty or null."); + } + + ObjectOutputStream writer = null; + try { + if (!IOUtils.createFolder(mDirectory)) { + return false; + } + + File file = new File(mDirectory, id); + if (!IOUtils.createNewFile(file)) { + return false; + } + + writer = new ObjectOutputStream(new FileOutputStream(file)); + session.writeObject(writer); + return true; + } catch (IOException e) { + IOUtils.delFileOrFolder(new File(mDirectory, id)); + throw e; + } finally { + IOUtils.closeQuietly(writer); + } + } + + + @Override + public StandardSession getSession( String id) throws IOException, ClassNotFoundException { + if (id.isEmpty()) { + throw new IllegalArgumentException("The id can not be empty or null."); + } + + ObjectInputStream reader = null; + try { + File file = new File(mDirectory, id); + if (!file.exists() || file.isDirectory()) { + return null; + } + + reader = new ObjectInputStream(new FileInputStream(file)); + StandardSession session = new StandardSession(); + session.readObject(reader); + return session; + } catch (IOException e) { + IOUtils.delFileOrFolder(new File(mDirectory, id)); + throw e; + } finally { + IOUtils.closeQuietly(reader); + } + } + + @Override + public boolean remove( StandardSession session) { + String id = session.getId(); + if (id.isEmpty()) { + throw new IllegalStateException("The session id can not be empty or null."); + } + return IOUtils.delFileOrFolder(new File(mDirectory, session.getId())); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/http/session/Store.java b/api/src/main/java/com/yanzhenjie/andserver/http/session/Store.java new file mode 100644 index 0000000000000000000000000000000000000000..53d90cbd1c6b7ad3956da8e22cbe7430ffbe5eaf --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/http/session/Store.java @@ -0,0 +1,59 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.http.session; + + + + +import java.io.IOException; + +/** + * Created by Zhenjie Yan on 2018/7/26. + */ +interface Store { + + /** + * Increase the session to the persistent store. + * + * @param session the session. + * + * @return true if it is successfully added or replaced, otherwise is false. + * + * @throws IOException if an output error occurs while processing this request. + */ + boolean replace(StandardSession session) throws IOException; + + /** + * Get the session from the persistent store. + * + * @param id the session ID. + * + * @return a session object. + * + * @throws IOException if the input error occurs while processing this request. + */ + + StandardSession getSession(String id) throws IOException, ClassNotFoundException; + + /** + * Remove the session from the persistent store. + * + * @param session the session. + * + * @return true if successful removal, otherwise is false. + */ + boolean remove(StandardSession session); +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/register/OnRegister.java b/api/src/main/java/com/yanzhenjie/andserver/register/OnRegister.java new file mode 100644 index 0000000000000000000000000000000000000000..2a93909a4ef8b5c1d0eb699f7bf5c16ce83d9554 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/register/OnRegister.java @@ -0,0 +1,34 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.register; + +import ohos.app.Context; + +/** + * Created by Zhenjie Yan on 2018/9/10. + */ +public interface OnRegister { + + /** + * Register the component. + * + * @param context context. + * @param group group name. + * @param register onRegister. + */ + void onRegister(Context context, String group, Register register); + +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/register/Register.java b/api/src/main/java/com/yanzhenjie/andserver/register/Register.java new file mode 100644 index 0000000000000000000000000000000000000000..e1e0444789cd3fdbf5a4842011dbc4addc00fa93 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/register/Register.java @@ -0,0 +1,65 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.register; + + +import com.yanzhenjie.andserver.framework.ExceptionResolver; +import com.yanzhenjie.andserver.framework.HandlerInterceptor; +import com.yanzhenjie.andserver.framework.MessageConverter; +import com.yanzhenjie.andserver.framework.config.Multipart; +import com.yanzhenjie.andserver.framework.handler.HandlerAdapter; + +/** + * Created by Zhenjie Yan on 2018/9/10. + */ +public interface Register { + + /** + * Increase the handler adapter. + * + * @param adapter {@link HandlerAdapter}. + */ + void addAdapter(HandlerAdapter adapter); + + /** + * Increase handler interceptor. + * + * @param interceptor {@link HandlerInterceptor}. + */ + void addInterceptor(HandlerInterceptor interceptor); + + /** + * Set up a message converter to convert messages that are not recognized by AndServer. + * + * @param converter {@link MessageConverter}. + */ + void setConverter(MessageConverter converter); + + /** + * Set the exception handler. If you don't want you to let AndServer output the default error message, set it to + * take over the exception. + * + * @param resolver {@link ExceptionResolver}. + */ + void setResolver(ExceptionResolver resolver); + + /** + * Set the parameters used to resolve the multipart request. + * + * @param multipart {@link Multipart}. + */ + void setMultipart(Multipart multipart); +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/server/BasicServer.java b/api/src/main/java/com/yanzhenjie/andserver/server/BasicServer.java new file mode 100644 index 0000000000000000000000000000000000000000..0e7e51bfbccc620730117e03a47785db53a5f974 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/server/BasicServer.java @@ -0,0 +1,247 @@ +/* + * Copyright 2020 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.server; + +import com.yanzhenjie.andserver.AndServer; +import com.yanzhenjie.andserver.SSLSocketInitializer; +import com.yanzhenjie.andserver.Server; +import com.yanzhenjie.andserver.util.Executors; +import org.apache.httpcore.ExceptionLogger; +import org.apache.httpcore.config.SocketConfig; +import org.apache.httpcore.impl.bootstrap.HttpServer; +import org.apache.httpcore.impl.bootstrap.SSLServerSetupHandler; +import org.apache.httpcore.impl.bootstrap.ServerBootstrap; +import org.apache.httpcore.protocol.HttpRequestHandler; + +import javax.net.ServerSocketFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLServerSocket; +import java.net.InetAddress; +import java.util.concurrent.TimeUnit; + +/** + * Created by Zhenjie Yan on 3/7/20. + */ +public abstract class BasicServer implements Server { + + static final int BUFFER = 8 * 1024; + + protected final InetAddress mInetAddress; + protected final int mPort; + protected final int mTimeout; + protected final ServerSocketFactory mSocketFactory; + protected final SSLContext mSSLContext; + protected final SSLSocketInitializer mSSLSocketInitializer; + protected final Server.ServerListener mListener; + + private HttpServer mHttpServer; + protected boolean isRunning; + + BasicServer(T builder) { + this.mInetAddress = builder.inetAddress; + this.mPort = builder.port; + this.mTimeout = builder.timeout; + this.mSocketFactory = builder.mSocketFactory; + this.mSSLContext = builder.sslContext; + this.mSSLSocketInitializer = builder.mSSLSocketInitializer; + this.mListener = builder.listener; + } + + @Override + public boolean isRunning() { + return isRunning; + } + + @Override + public void startup() { + if (isRunning) { + return; + } + + Executors.getInstance().execute(new Runnable() { + @Override + public void run() { + try { + mHttpServer = ServerBootstrap.bootstrap() + .setServerSocketFactory(mSocketFactory) + .setSocketConfig( + SocketConfig.custom() + .setSoKeepAlive(true) + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .setSoTimeout(mTimeout) + .setBacklogSize(BUFFER) + .setRcvBufSize(BUFFER) + .setSndBufSize(BUFFER) + .setSoLinger(0) + .build() + ) + .setLocalAddress(mInetAddress) + .setListenerPort(mPort) + .setSslContext(mSSLContext) + .setSslSetupHandler(new SSLSetup(mSSLSocketInitializer)) + .setServerInfo(AndServer.INFO) + .registerHandler("*", requestHandler()) + .setExceptionLogger(ExceptionLogger.NO_OP) + .create(); + + mHttpServer.start(); + isRunning = true; + + Executors.getInstance().post(new Runnable() { + @Override + public void run() { + if (mListener != null) { + mListener.onStarted(); + } + } + }); + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + mHttpServer.shutdown(3, TimeUnit.SECONDS); + } + }); + } catch (final Exception e) { + Executors.getInstance().post(new Runnable() { + @Override + public void run() { + if (mListener != null) { + mListener.onException(e); + } + } + }); + } + } + }); + } + + /** + * Assigns {@link HttpRequestHandler} instance. + */ + protected abstract HttpRequestHandler requestHandler(); + + /** + * Quit the server. + */ + @Override + public void shutdown() { + if (!isRunning) { + return; + } + + Executors.getInstance().execute(new Runnable() { + @Override + public void run() { + if (mHttpServer != null) { + mHttpServer.shutdown(3, TimeUnit.SECONDS); + isRunning = false; + Executors.getInstance().post(new Runnable() { + @Override + public void run() { + if (mListener != null) { + mListener.onStopped(); + } + } + }); + } + } + }); + } + + private static final class SSLSetup implements SSLServerSetupHandler { + + private final SSLSocketInitializer mInitializer; + + public SSLSetup( SSLSocketInitializer initializer) { + this.mInitializer = initializer; + } + + @Override + public void initialize(SSLServerSocket socket) throws SSLException { + mInitializer.onCreated(socket); + } + } + + @Override + public InetAddress getInetAddress() { + if (isRunning) { + return mHttpServer.getInetAddress(); + } + throw new IllegalStateException("The server has not been started yet."); + } + + @Override + public int getPort() { + if (isRunning) { + return mHttpServer.getLocalPort(); + } + throw new IllegalStateException("The server has not been started yet."); + } + + protected abstract static class Builder { + + InetAddress inetAddress; + int port; + int timeout; + ServerSocketFactory mSocketFactory; + SSLContext sslContext; + SSLSocketInitializer mSSLSocketInitializer; + Server.ServerListener listener; + + Builder() { + } + @SuppressWarnings("unchecked") + public T inetAddress(InetAddress inetAddress) { + this.inetAddress = inetAddress; + return (T) this; + } + @SuppressWarnings("unchecked") + public T port(int port) { + this.port = port; + return (T) this; + } + @SuppressWarnings("unchecked") + public T timeout(int timeout, TimeUnit timeUnit) { + long timeoutMs = timeUnit.toMillis(timeout); + this.timeout = (int) Math.min(timeoutMs, Integer.MAX_VALUE); + return (T) this; + } + @SuppressWarnings("unchecked") + public T serverSocketFactory(ServerSocketFactory factory) { + this.mSocketFactory = factory; + return (T) this; + } + @SuppressWarnings("unchecked") + public T sslContext(SSLContext sslContext) { + this.sslContext = sslContext; + return (T) this; + } + @SuppressWarnings("unchecked") + public T sslSocketInitializer(SSLSocketInitializer initializer) { + this.mSSLSocketInitializer = initializer; + return (T) this; + } + @SuppressWarnings("unchecked") + public T listener(Server.ServerListener listener) { + this.listener = listener; + return (T) this; + } + + public abstract S build(); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/server/ProxyServer.java b/api/src/main/java/com/yanzhenjie/andserver/server/ProxyServer.java new file mode 100644 index 0000000000000000000000000000000000000000..6bbaee6ef8d4db3d2adc2c5c79a70bfab80322ba --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/server/ProxyServer.java @@ -0,0 +1,378 @@ +/* + * Copyright 2020 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.server; + + +import com.yanzhenjie.andserver.AndServer; +import com.yanzhenjie.andserver.ProxyHandler; +import com.yanzhenjie.andserver.SSLSocketInitializer; +import com.yanzhenjie.andserver.Server; +import com.yanzhenjie.andserver.util.Executors; +import org.apache.httpcore.ConnectionClosedException; +import org.apache.httpcore.HttpException; +import org.apache.httpcore.HttpHost; +import org.apache.httpcore.HttpServerConnection; +import org.apache.httpcore.impl.DefaultBHttpClientConnection; +import org.apache.httpcore.impl.DefaultBHttpServerConnection; +import org.apache.httpcore.protocol.*; + +import javax.net.ServerSocketFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocket; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Created by Zhenjie Yan on 3/7/20. + */ +public class ProxyServer extends BasicServer { + + public static final String PROXY_CONN_CLIENT = "http.proxy.conn.client"; + public static final String PROXY_CONN_ALIVE = "http.proxy.conn.alive"; + + public static Builder newBuilder() { + return new Builder(); + } + + private final InetAddress mInetAddress; + private final int mPort; + private final int mTimeout; + private final ServerSocketFactory mSocketFactory; + private final SSLContext mSSLContext; + private final SSLSocketInitializer mSSLSocketInitializer; + private final Server.ServerListener mListener; + + private Map mHostList; + + private HttpServer mHttpServer; + private boolean isRunning; + + private ProxyServer(Builder builder) { + super(builder); + this.mInetAddress = builder.inetAddress; + this.mPort = builder.port; + this.mTimeout = builder.timeout; + this.mSocketFactory = builder.mSocketFactory; + this.mSSLContext = builder.sslContext; + this.mSSLSocketInitializer = builder.mSSLSocketInitializer; + this.mListener = builder.listener; + + this.mHostList = builder.mHostList; + } + + @Override + protected HttpRequestHandler requestHandler() { + return new ProxyHandler(mHostList); + } + + @Override + public void startup() { + if (isRunning) { + return; + } + + Executors.getInstance().execute(new Runnable() { + @Override + public void run() { + ServerSocketFactory socketFactory = mSocketFactory; + if (socketFactory == null) { + if (mSSLContext != null) { + socketFactory = mSSLContext.getServerSocketFactory(); + } else { + socketFactory = ServerSocketFactory.getDefault(); + } + } + + mHttpServer = new HttpServer(mInetAddress, + mPort, + mTimeout, + socketFactory, + mSSLSocketInitializer, + requestHandler()); + try { + mHttpServer.startServer(); + isRunning = true; + + Executors.getInstance().post(new Runnable() { + @Override + public void run() { + if (mListener != null) { + mListener.onStarted(); + } + } + }); + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + mHttpServer.stopServer(); + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + + @Override + public void shutdown() { + if (!isRunning) { + return; + } + + Executors.getInstance().execute(new Runnable() { + @Override + public void run() { + if (mHttpServer != null) { + mHttpServer.stopServer(); + isRunning = false; + Executors.getInstance().post(new Runnable() { + @Override + public void run() { + if (mListener != null) { + mListener.onStopped(); + } + } + }); + } + } + }); + } + + public static class Builder extends BasicServer.Builder + implements Server.ProxyBuilder { + + private Map mHostList = new HashMap<>(); + + public Builder() { + } + + @Override + public Builder addProxy(String hostName, String proxyHost) { + mHostList.put(hostName.toLowerCase(Locale.ROOT), HttpHost.create(proxyHost)); + return this; + } + + @Override + public ProxyServer build() { + return new ProxyServer(this); + } + } + + private static class HttpServer implements Runnable { + + private final InetAddress mInetAddress; + private final int mPort; + private final int mTimeout; + private final ServerSocketFactory mSocketFactory; + private final SSLSocketInitializer mSSLSocketInitializer; + private final HttpRequestHandler mHandler; + + private final ThreadPoolExecutor mServerExecutor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, + new SynchronousQueue<>(), new ThreadFactoryImpl("HTTP-Server-")); + private final ThreadGroup mWorkerThreads = new ThreadGroup("HTTP-workers"); + private final ThreadPoolExecutor mWorkerExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 1L, + TimeUnit.SECONDS, new SynchronousQueue<>(), new ThreadFactoryImpl("HTTP-Handlers-", mWorkerThreads)) { + @Override + protected void beforeExecute(Thread t, Runnable r) { + if (r instanceof Worker) { + mWorkerSet.put((Worker) r, Boolean.TRUE); + } + } + + @Override + protected void afterExecute(Runnable r, Throwable t) { + if (r instanceof Worker) { + mWorkerSet.remove(r); + } + } + }; + private final Map mWorkerSet = new ConcurrentHashMap<>(); + + private HttpService mHttpService; + private ServerSocket mServerSocket; + + public HttpServer(InetAddress inetAddress, int port, int timeout, ServerSocketFactory socketFactory, + SSLSocketInitializer sslSocketInitializer, HttpRequestHandler handler) { + this.mInetAddress = inetAddress; + this.mPort = port; + this.mTimeout = timeout; + this.mSocketFactory = socketFactory; + this.mSSLSocketInitializer = sslSocketInitializer; + this.mHandler = handler; + + HttpProcessor inProcessor = new ImmutableHttpProcessor( + new ResponseDate(), + new ResponseServer(AndServer.INFO), + new ResponseContent(), + new ResponseConnControl()); + + UriHttpRequestHandlerMapper mapper = new UriHttpRequestHandlerMapper(); + mapper.register("*", mHandler); + + this.mHttpService = new HttpService(inProcessor, mapper); + } + + public void startServer() throws IOException { + mServerSocket = mSocketFactory.createServerSocket(); + mServerSocket.setReuseAddress(true); + mServerSocket.bind(new InetSocketAddress(mInetAddress, mPort), BUFFER); + mServerSocket.setReceiveBufferSize(BUFFER); + if (mSSLSocketInitializer != null && mServerSocket instanceof SSLServerSocket) { + mSSLSocketInitializer.onCreated((SSLServerSocket) mServerSocket); + } + + mServerExecutor.execute(this); + } + + public void stopServer() { + mServerExecutor.shutdown(); + mWorkerExecutor.shutdown(); + try { + mServerSocket.close(); + } catch (IOException ignored) { + } + mWorkerThreads.interrupt(); + + try { + mWorkerExecutor.awaitTermination(3, TimeUnit.SECONDS); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + + Set workers = mWorkerSet.keySet(); + for (Worker worker : workers) { + HttpServerConnection conn = worker.getServerConn(); + try { + conn.shutdown(); + } catch (IOException ignored) { + } + } + } + + @Override + public void run() { + try { + while (!Thread.interrupted()) { + Socket socket = mServerSocket.accept(); + socket.setSoTimeout(mTimeout); + socket.setKeepAlive(true); + socket.setTcpNoDelay(true); + socket.setReceiveBufferSize(BUFFER); + socket.setSendBufferSize(BUFFER); + socket.setSoLinger(true, 0); + + DefaultBHttpServerConnection serverConn = new DefaultBHttpServerConnection(BUFFER); + serverConn.bind(socket); + + DefaultBHttpClientConnection clientConn = new DefaultBHttpClientConnection(BUFFER); + Worker worker = new Worker(mHttpService, serverConn, clientConn); + + mWorkerExecutor.execute(worker); + } + } catch (Exception ignored) { + } + } + } + + private static class Worker implements Runnable { + + private final HttpService mHttpService; + private final DefaultBHttpServerConnection mServerConn; + private final DefaultBHttpClientConnection mClientConn; + + public Worker(HttpService httpservice, + DefaultBHttpServerConnection serverConn, DefaultBHttpClientConnection clientConn) { + this.mHttpService = httpservice; + this.mServerConn = serverConn; + this.mClientConn = clientConn; + } + + public DefaultBHttpServerConnection getServerConn() { + return mServerConn; + } + + @Override + public void run() { + BasicHttpContext localContext = new BasicHttpContext(); + HttpCoreContext context = HttpCoreContext.adapt(localContext); + context.setAttribute(PROXY_CONN_CLIENT, mClientConn); + + try { + while (!Thread.interrupted()) { + if (!mServerConn.isOpen()) { + mClientConn.close(); + break; + } + + mHttpService.handleRequest(mServerConn, context); + + Boolean keepAlive = (Boolean) context.getAttribute(PROXY_CONN_ALIVE); + if (!Boolean.TRUE.equals(keepAlive)) { + mClientConn.close(); + mServerConn.close(); + break; + } + } + } catch (ConnectionClosedException ex) { + System.err.println("Client closed connection."); + } catch (IOException ex) { + System.err.println("I/O error: " + ex.getMessage()); + } catch (HttpException ex) { + System.err.println("Unrecoverable HTTP protocol violation: " + ex.getMessage()); + } finally { + try { + mServerConn.shutdown(); + } catch (IOException ignore) { + } + try { + mClientConn.shutdown(); + } catch (IOException ignore) { + } + } + } + } + + private static class ThreadFactoryImpl implements ThreadFactory { + private final String mPrefix; + private final ThreadGroup mGroup; + private final AtomicLong mCount; + + ThreadFactoryImpl(String prefix, ThreadGroup group) { + this.mPrefix = prefix; + this.mGroup = group; + this.mCount = new AtomicLong(); + } + + ThreadFactoryImpl(String mPrefix) { + this(mPrefix, null); + } + + @Override + public Thread newThread( Runnable target) { + return new Thread(mGroup, target, mPrefix + "-" + mCount.incrementAndGet()); + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/server/WebServer.java b/api/src/main/java/com/yanzhenjie/andserver/server/WebServer.java new file mode 100644 index 0000000000000000000000000000000000000000..cbd3a3e92f912037d52ea4a9fda1db527a296d2d --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/server/WebServer.java @@ -0,0 +1,72 @@ +/* + * Copyright 2020 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.server; + +import com.yanzhenjie.andserver.ComponentRegister; +import com.yanzhenjie.andserver.DispatcherHandler; +import com.yanzhenjie.andserver.Server; +import ohos.app.Context; +import org.apache.httpcore.protocol.HttpRequestHandler; + +/** + * Created by Zhenjie Yan on 3/7/20. + */ +public class WebServer extends BasicServer { + + public static Builder newBuilder(Context context, String group) { + return new Builder(context, group); + } + + private Context mContext; + private String mGroup; + + private WebServer(Builder builder) { + super(builder); + this.mContext = builder.context; + this.mGroup = builder.group; + } + + @Override + protected HttpRequestHandler requestHandler() { + DispatcherHandler handler = new DispatcherHandler(mContext); + ComponentRegister register = new ComponentRegister(mContext); + try { + register.register(handler, mGroup); + } catch (InstantiationException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + return handler; + } + + public static class Builder extends BasicServer.Builder + implements Server.Builder { + + private Context context; + private String group; + + private Builder(Context context, String group) { + this.context = context; + this.group = group; + } + + @Override + public WebServer build() { + return new WebServer(this); + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/AcceptLanguage.java b/api/src/main/java/com/yanzhenjie/andserver/util/AcceptLanguage.java new file mode 100644 index 0000000000000000000000000000000000000000..8419bf515452d3366b981d056dd98efcac1a043e --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/AcceptLanguage.java @@ -0,0 +1,33 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util; + +import com.yanzhenjie.andserver.http.FAcceptLanguage; + +import java.util.Locale; + +/** + * Created by Zhenjie Yan on 2018/8/7. + * + * @deprecated use {@link FAcceptLanguage} instead. + */ +@Deprecated +public class AcceptLanguage extends FAcceptLanguage { + + protected AcceptLanguage(Locale locale, double quality) { + super(locale, quality); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/Assert.java b/api/src/main/java/com/yanzhenjie/andserver/util/Assert.java new file mode 100644 index 0000000000000000000000000000000000000000..04173d6db82f52e36928c5384dfb7fb51f0245fe --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/Assert.java @@ -0,0 +1,329 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util; + + +import java.util.Collection; +import java.util.Map; + +/** + * Created by Zhenjie Yan on 2018/7/5. + */ +public abstract class Assert { + + /** + * Assert a boolean expression, throwing an {@code IllegalStateException} if the expression evaluates to {@code + * false}. + * + *

Call {@link #isTrue} if you wish to throw an {@code IllegalArgumentException} on an assertion failure.

Assert.state(id == null, "The id property must not already be initialized");
+ * + * @param expression a boolean expression. + * @param message the exception message to use if the assertion fails. + * + * @throws IllegalStateException if {@code expression} is {@code false} + */ + public static void state(boolean expression, String message) { + if (!expression) { + throw new IllegalStateException(message); + } + } + + /** + * Assert a boolean expression, throwing an {@code IllegalArgumentException} if the expression evaluates to {@code + * false}. + * + *
Assert.isTrue(i > 0, "The value must be greater than zero");
+ * + * @param expression a boolean expression. + * @param message the exception message to use if the assertion fails. + * + * @throws IllegalArgumentException if {@code expression} is {@code false}. + */ + public static void isTrue(boolean expression, String message) { + if (!expression) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an object is {@code null}. + * + *
Assert.isNull(value, "The value must be null");
+ * + * @param object the object to check. + * @param message the exception message to use if the assertion fails. + * + * @throws IllegalArgumentException if the object is not {@code null}. + */ + public static void isNull(Object object, String message) { + if (object != null) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an object is not {@code null}. + * + *
Assert.notNull(clazz, "The class must not be null");
+ * + * @param object the object to check. + * @param message the exception message to use if the assertion fails. + * + * @throws IllegalArgumentException if the object is {@code null} + */ + public static void notNull(Object object, String message) { + if (object == null) { + throw new IllegalArgumentException(message); + } + } + + + /** + * Assert that the given String is not empty; that is, it must not be {@code null} and not the empty String. + * + *
Assert.hasLength(name, "Name must not be empty");
+ * + * @param text the String to check. + * @param message the exception message to use if the assertion fails. + * + * @throws IllegalArgumentException if the text is empty. + */ + public static void hasLength(String text, String message) { + if (text.isEmpty()) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given String contains valid text content; that is, it must not be {@code null} and must contain + * at least one non-whitespace character. + * + *
Assert.hasText(name, "'name' must not be empty");
+ * + * @param text the String to check. + * @param message the exception message to use if the assertion fails. + * + * @throws IllegalArgumentException if the text does not contain valid text content. + */ + public static void hasText(String text, String message) { + if (text.isEmpty()) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given text does not contain the given substring. + * + *
Assert .doesNotContain(name, "rod", "Name must not contain 'rod'");
+ * + * @param textToSearch the text to search. + * @param substring the substring to find within the text. + * @param message the exception message to use if the assertion fails. + * + * @throws IllegalArgumentException if the text contains the substring. + */ + public static void doesNotContain(String textToSearch, String substring, String message) { + if (!textToSearch.isEmpty() && !substring.isEmpty() && textToSearch.contains(substring)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an array contains elements; that is, it must not be {@code null} and must contain at least one + * element. + * + *
Assert.notEmpty(array, "The array must contain elements");
+ * + * @param array the array to check. + * @param message the exception message to use if the assertion fails. + * + * @throws IllegalArgumentException if the object array is {@code null} or contains no elements. + */ + public static void notEmpty(Object[] array, String message) { + if (array == null || array.length == 0) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an array contains no {@code null} elements.

Note: Does not complain if the array is empty! + * + *

Assert.noNullElements(array, "The array must contain non-null elements");
+ * + * @param array the array to check. + * @param message the exception message to use if the assertion fails. + * + * @throws IllegalArgumentException if the object array contains a {@code null} element. + */ + public static void noNullElements(Object[] array, String message) { + if (array != null) { + for (Object element: array) { + if (element == null) { + throw new IllegalArgumentException(message); + } + } + } + } + + /** + * Assert that a collection contains elements; that is, it must not be {@code null} and must contain at least one + * element. + * + *
Assert.notEmpty(collection, "Collection must contain elements");
+ * + * @param collection the collection to check. + * @param message the exception message to use if the assertion fails. + * + * @throws IllegalArgumentException if the collection is {@code null} or contains no elements. + */ + public static void notEmpty(Collection collection, String message) { + if (collection == null || collection.isEmpty()) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that a Map contains entries; that is, it must not be {@code null} and must contain at least one entry. + * + *
Assert.notEmpty(map, "Map must contain entries");
+ * + * @param map the map to check. + * @param message the exception message to use if the assertion fails. + * + * @throws IllegalArgumentException if the map is {@code null} or contains no entries. + */ + public static void notEmpty(Map map, String message) { + if (map == null || map.isEmpty()) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the provided object is an instance of the provided class. + * + *
Assert.instanceOf(Foo.class, foo, "Foo expected");
+ * + * @param type the type to check against. + * @param obj the object to check. + * @param message a message which will be prepended to provide further context. If it is empty or ends in ":" or + * ";" or "," or ".", a full exception message will be appended. If it ends in a space, the name of the + * offending object's type will be appended. In any other case, a ":" with a space and the name of the offending + * object's type will be appended. + * + * @throws IllegalArgumentException if the object is not an instance of type. + */ + public static void isInstanceOf(Class type, Object obj, String message) { + notNull(type, "Type to check against must not be null"); + + if (!type.isInstance(obj)) { + instanceCheckFailed(type, obj, message); + } + } + + /** + * Assert that the provided object is an instance of the provided class. + * + *
Assert.instanceOf(Foo.class, foo);
+ * + * @param type the type to check against. + * @param obj the object to check. + * + * @throws IllegalArgumentException if the object is not an instance of type. + */ + public static void isInstanceOf(Class type, Object obj) { + isInstanceOf(type, obj, ""); + } + + /** + * Assert that {@code superType.isAssignableFrom(subType)} is {@code true}. + * + *
Assert.isAssignable(Number.class, myClass, "Number expected");
+ * + * @param superType the super type to check against. + * @param subType the sub type to check. + * @param message a message which will be prepended to provide further context. If it is empty or ends in ":" or + * ";" or "," or ".", a full exception message will be appended. If it ends in a space, the name of the + * offending sub type will be appended. In any other case, a ":" with a space and the name of the offending sub + * type will be appended. + * + * @throws IllegalArgumentException if the classes are not assignable. + */ + public static void isAssignable(Class superType, Class subType, String message) { + notNull(superType, "Super type to check against must not be null"); + + if (subType == null || !superType.isAssignableFrom(subType)) { + assignableCheckFailed(superType, subType, message); + } + } + + /** + * Assert that {@code superType.isAssignableFrom(subType)} is {@code true}. + * + *
Assert.isAssignable(Number.class, myClass);
+ * + * @param superType the super type to check. + * @param subType the sub type to check. + * + * @throws IllegalArgumentException if the classes are not assignable. + */ + public static void isAssignable(Class superType, Class subType) { + isAssignable(superType, subType, ""); + } + + private static void instanceCheckFailed(Class type, Object obj, String msg) { + String className = (obj != null ? obj.getClass().getName() : "null"); + String result = ""; + boolean defaultMessage = true; + if (!msg.isEmpty()) { + if (endsWithSeparator(msg)) { + result = msg + " "; + } else { + result = messageWithTypeName(msg, className); + defaultMessage = false; + } + } + if (defaultMessage) { + result = result + ("Object of class [" + className + "] must be an instance of " + type); + } + throw new IllegalArgumentException(result); + } + + private static void assignableCheckFailed(Class superType, Class subType, String msg) { + String result = ""; + boolean defaultMessage = true; + if (!msg.isEmpty()) { + if (endsWithSeparator(msg)) { + result = msg + " "; + } else { + result = messageWithTypeName(msg, subType); + defaultMessage = false; + } + } + if (defaultMessage) { + result = result + (subType + " is not assignable to " + superType); + } + throw new IllegalArgumentException(result); + } + + private static boolean endsWithSeparator(String msg) { + return (msg.endsWith(":") || msg.endsWith(";") || msg.endsWith(",") || msg.endsWith(".")); + } + + private static String messageWithTypeName(String msg, Object typeName) { + return msg + (msg.endsWith(" ") ? "" : ": ") + typeName; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/CollectionUtils.java b/api/src/main/java/com/yanzhenjie/andserver/util/CollectionUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d80c7a3596c0496d52b3237b3355e012a1344018 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/CollectionUtils.java @@ -0,0 +1,527 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util; + +import java.io.Serializable; +import java.util.*; + +/** + * Created by Zhenjie Yan on 2018/7/11. + * + * @deprecated use apache commons-collection instead. + */ +@Deprecated +public abstract class CollectionUtils { + + /** + * Return {@code true} if the supplied Collection is {@code null} or empty. Otherwise, return {@code false}. + * + * @param collection the Collection to check. + * + * @return whether the given Collection is empty. + */ + public static boolean isEmpty(Collection collection) { + return (collection == null || collection.isEmpty()); + } + + /** + * Return {@code true} if the supplied Map is {@code null} or empty. Otherwise, return {@code false}. + * + * @param map the Map to check. + * + * @return whether the given Map is empty. + */ + public static boolean isEmpty(Map map) { + return (map == null || map.isEmpty()); + } + + /** + * Convert the supplied array into a List. StandardCookieProcessor primitive array gets converted into a List of the + * appropriate wrapper type. + * + *

NOTE: Generally prefer the standard {@link Arrays#asList} method. This {@code arrayToList} method is + * just meant to deal with an incoming Object value that might be an {@code Object[]} or a primitive array at + * runtime. + * + *

StandardCookieProcessor {@code null} source value will be converted to an empty List. + * + * @param source the (potentially primitive) array. + * + * @return the converted List result. + */ + public static List arrayToList(Object source) { + return Arrays.asList(ObjectUtils.toObjectArray(source)); + } + + /** + * Merge the given array into the given Collection. + * + * @param array the array to merge (may be {@code null}). + * @param collection the target Collection to merge the array into. + */ + @SuppressWarnings("unchecked") + public static void mergeArrayIntoCollection(Object array, Collection collection) { + if (collection == null) { + throw new IllegalArgumentException("Collection must not be null"); + } + Object[] arr = ObjectUtils.toObjectArray(array); + for (Object elem: arr) { + collection.add((E) elem); + } + } + + /** + * Merge the given Properties instance into the given Map, copying all properties (key-value pairs) over.

Uses + * {@code Properties.propertyNames()} to even catch default properties linked into the original Properties + * instance. + * + * @param props the Properties instance to merge (may be {@code null}). + * @param map the target Map to merge the properties into. + */ + @SuppressWarnings("unchecked") + public static void mergePropertiesIntoMap(Properties props, Map map) { + if (map == null) { + throw new IllegalArgumentException("Map must not be null"); + } + if (props != null) { + for (Enumeration en = props.propertyNames(); en.hasMoreElements(); ) { + String key = (String) en.nextElement(); + Object value = props.get(key); + if (value == null) { + // Allow for defaults fallback or potentially overridden accessor... + value = props.getProperty(key); + } + map.put((K) key, (V) value); + } + } + } + + + /** + * Check whether the given Iterator contains the given element. + * + * @param iterator the Iterator to check. + * @param element the element to look for. + * + * @return {@code true} if found, {@code false} else. + */ + public static boolean contains(Iterator iterator, Object element) { + if (iterator != null) { + while (iterator.hasNext()) { + Object candidate = iterator.next(); + if (ObjectUtils.nullSafeEquals(candidate, element)) { + return true; + } + } + } + return false; + } + + /** + * Check whether the given Enumeration contains the given element. + * + * @param enumeration the Enumeration to check. + * @param element the element to look for. + * + * @return {@code true} if found, {@code false} else. + */ + public static boolean contains(Enumeration enumeration, Object element) { + if (enumeration != null) { + while (enumeration.hasMoreElements()) { + Object candidate = enumeration.nextElement(); + if (ObjectUtils.nullSafeEquals(candidate, element)) { + return true; + } + } + } + return false; + } + + /** + * Check whether the given Collection contains the given element instance.

Enforces the given instance to be + * present, rather than returning {@code true} for an equal element as well. + * + * @param collection the Collection to check. + * @param element the element to look for. + * + * @return {@code true} if found, {@code false} else. + */ + public static boolean containsInstance(Collection collection, Object element) { + if (collection != null) { + for (Object candidate: collection) { + if (candidate == element) { + return true; + } + } + } + return false; + } + + /** + * Return {@code true} if any element in '{@code candidates}' is contained in '{@code source}'; otherwise returns + * {@code false}. + * + * @param source the source Collection. + * @param candidates the candidates to search for. + * + * @return whether any of the candidates has been found. + */ + public static boolean containsAny(Collection source, Collection candidates) { + if (isEmpty(source) || isEmpty(candidates)) { + return false; + } + for (Object candidate: candidates) { + if (source.contains(candidate)) { + return true; + } + } + return false; + } + + /** + * Return the first element in '{@code candidates}' that is contained in '{@code source}'. If no element in '{@code + * candidates}' is present in '{@code source}' returns {@code null}. Iteration order is {@link Collection} + * implementation specific. + * + * @param source the source Collection. + * @param candidates the candidates to search for. + * + * @return the first present object, or {@code null} if not found. + */ + @SuppressWarnings("unchecked") + public static E findFirstMatch(Collection source, Collection candidates) { + if (isEmpty(source) || isEmpty(candidates)) { + return null; + } + for (Object candidate: candidates) { + if (source.contains(candidate)) { + return (E) candidate; + } + } + return null; + } + + /** + * Find a single value of the given type in the given Collection. + * + * @param collection the Collection to search. + * @param type the type to look for. + * + * @return a value of the given type found if there is a clear match, or {@code null} if none or more than one such + * value found. + */ + @SuppressWarnings("unchecked") + public static T findValueOfType(Collection collection, Class type) { + if (isEmpty(collection)) { + return null; + } + T value = null; + for (Object element: collection) { + if (type == null || type.isInstance(element)) { + if (value != null) { + // More than one value found... no clear single value. + return null; + } + value = (T) element; + } + } + return value; + } + + /** + * Find a single value of one of the given types in the given Collection: searching the Collection for a value of + * the first type, then searching for a value of the second type, etc. + * + * @param collection the collection to search. + * @param types the types to look for, in prioritized order. + * + * @return a value of one of the given types found if there is a clear match, or {@code null} if none or more than + * one such value found. + */ + public static Object findValueOfType(Collection collection, Class[] types) { + if (isEmpty(collection) || ObjectUtils.isEmpty(types)) { + return null; + } + for (Class type: types) { + Object value = findValueOfType(collection, type); + if (value != null) { + return value; + } + } + return null; + } + + /** + * Determine whether the given Collection only contains a single unique object. + * + * @param collection the Collection to check. + * + * @return {@code true} if the collection contains a single reference or multiple references to the same instance, + * {@code false} else. + */ + public static boolean hasUniqueObject(Collection collection) { + if (isEmpty(collection)) { + return false; + } + boolean hasCandidate = false; + Object candidate = null; + for (Object elem: collection) { + if (!hasCandidate) { + hasCandidate = true; + candidate = elem; + } else if (candidate != elem) { + return false; + } + } + return true; + } + + /** + * Find the common element type of the given Collection, if any. + * + * @param collection the Collection to check. + * + * @return the common element type, or {@code null} if no clear common type has been found (or the collection was + * empty). + */ + public static Class findCommonElementType(Collection collection) { + if (isEmpty(collection)) { + return null; + } + Class candidate = null; + for (Object val: collection) { + if (val != null) { + if (candidate == null) { + candidate = val.getClass(); + } else if (candidate != val.getClass()) { + return null; + } + } + } + return candidate; + } + + /** + * Marshal the elements from the given enumeration into an array of the given type. Enumeration elements must be + * assignable to the type of the given array. The array returned will be a different instance than the array given. + */ + public static A[] toArray(Enumeration enumeration, A[] array) { + ArrayList elements = new ArrayList<>(); + while (enumeration.hasMoreElements()) { + elements.add(enumeration.nextElement()); + } + return elements.toArray(array); + } + + /** + * Adapt an enumeration to an iterator. + * + * @param enumeration the enumeration. + * + * @return the iterator. + */ + public static Iterator toIterator(Enumeration enumeration) { + return new EnumerationIterator<>(enumeration); + } + + /** + * Adapt a {@code Map>} to an {@code MultiValueMap}. + * + * @param map the original map. + * + * @return the multi-value map. + */ + public static MultiValueMap toMultiValueMap(Map> map) { + return new MultiValueMapAdapter<>(map); + } + + /** + * Return an unmodifiable view of the specified multi-value map. + * + * @param map the map for which an unmodifiable view is to be returned. + * + * @return an unmodifiable view of the specified multi-value map. + */ + @SuppressWarnings("unchecked") + public static MultiValueMap unmodifiableMultiValueMap(MultiValueMap map) { + Assert.notNull(map, "'map' must not be null"); + Map> result = new LinkedHashMap<>(map.size()); + for (Map.Entry> entry: map.entrySet()) { + List values = Collections.unmodifiableList(entry.getValue()); + result.put(entry.getKey(), (List) values); + } + Map> unmodifiableMap = Collections.unmodifiableMap(result); + return toMultiValueMap(unmodifiableMap); + } + + + /** + * Iterator wrapping an Enumeration. + */ + private static class EnumerationIterator implements Iterator { + + private final Enumeration enumeration; + + public EnumerationIterator(Enumeration enumeration) { + this.enumeration = enumeration; + } + + @Override + public boolean hasNext() { + return this.enumeration.hasMoreElements(); + } + + @Override + public E next() { + return this.enumeration.nextElement(); + } + + @Override + public void remove() throws UnsupportedOperationException { + throw new UnsupportedOperationException("Not supported"); + } + } + + /** + * Adapts a Map to the MultiValueMap contract. + */ + private static class MultiValueMapAdapter implements MultiValueMap, Serializable { + + private final Map> mMap; + + public MultiValueMapAdapter(Map> map) { + Assert.notNull(map, "'map' must not be null"); + this.mMap = map; + } + + @Override + public void add(K key, V value) { + List values = this.mMap.get(key); + if (values == null) { + values = new LinkedList<>(); + this.mMap.put(key, values); + } + values.add(value); + } + + @Override + public V getFirst(K key) { + List values = this.mMap.get(key); + return (values != null ? values.get(0) : null); + } + + @Override + public void set(K key, V value) { + List values = new LinkedList<>(); + values.add(value); + this.mMap.put(key, values); + } + + @Override + public void setAll(Map values) { + for (Entry entry: values.entrySet()) { + set(entry.getKey(), entry.getValue()); + } + } + + @Override + public Map toSingleValueMap() { + LinkedHashMap singleValueMap = new LinkedHashMap<>(this.mMap.size()); + for (Entry> entry: mMap.entrySet()) { + singleValueMap.put(entry.getKey(), entry.getValue().get(0)); + } + return singleValueMap; + } + + @Override + public int size() { + return this.mMap.size(); + } + + @Override + public boolean isEmpty() { + return this.mMap.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return this.mMap.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return this.mMap.containsValue(value); + } + + @Override + public List get(Object key) { + return this.mMap.get(key); + } + + @Override + public List put(K key, List value) { + return this.mMap.put(key, value); + } + + @Override + public List remove(Object key) { + return this.mMap.remove(key); + } + + @Override + public void putAll(Map> map) { + this.mMap.putAll(map); + } + + @Override + public void clear() { + this.mMap.clear(); + } + + @Override + public Set keySet() { + return this.mMap.keySet(); + } + + @Override + public Collection> values() { + return this.mMap.values(); + } + + @Override + public Set>> entrySet() { + return this.mMap.entrySet(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + return mMap.equals(other); + } + + @Override + public int hashCode() { + return this.mMap.hashCode(); + } + + @Override + public String toString() { + return this.mMap.toString(); + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/DigestUtils.java b/api/src/main/java/com/yanzhenjie/andserver/util/DigestUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..89cae7477aa4962ba0655ce94d32807c928422a1 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/DigestUtils.java @@ -0,0 +1,195 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Created by Zhenjie Yan on 2018/9/6. + */ +public class DigestUtils { + + private static final String MD5_ALGORITHM_NAME = "MD5"; + + private static final char[] HEX_CHARS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', + 'f'}; + + /** + * Calculate the MD5 digest of string. + * + * @param string string to calculate the digest over. + * + * @return the digest. + */ + public static byte[] md5Digest(String string) throws UnsupportedEncodingException { + return md5Digest(string.getBytes("UTF-8")); + } + + /** + * Calculate the MD5 digest of the given bytes. + * + * @param bytes the bytes to calculate the digest over. + * + * @return the digest. + */ + public static byte[] md5Digest(byte[] bytes) { + return digest(MD5_ALGORITHM_NAME, bytes); + } + + /** + * Calculate the MD5 digest of the given stream. + * + * @param inputStream the InputStream to calculate the digest over. + * + * @return the digest. + */ + public static byte[] md5Digest(InputStream inputStream) throws IOException { + return digest(MD5_ALGORITHM_NAME, inputStream); + } + + /** + * Return a hexadecimal string representation of the MD5 digest of string. + * + * @param string string to calculate the digest over. + * + * @return a hexadecimal digest string. + */ + public static String md5DigestAsHex(String string) throws UnsupportedEncodingException { + return md5DigestAsHex(string.getBytes("UTF-8")); + } + + /** + * Return a hexadecimal string representation of the MD5 digest of the given bytes. + * + * @param bytes the bytes to calculate the digest over. + * + * @return a hexadecimal digest string. + */ + public static String md5DigestAsHex(byte[] bytes) { + return digestAsHexString(MD5_ALGORITHM_NAME, bytes); + } + + /** + * Return a hexadecimal string representation of the MD5 digest of the given stream. + * + * @param inputStream the InputStream to calculate the digest over. + * + * @return a hexadecimal digest string. + */ + public static String md5DigestAsHex(InputStream inputStream) throws IOException { + return digestAsHexString(MD5_ALGORITHM_NAME, inputStream); + } + + /** + * Append a hexadecimal string representation of the MD5 digest of the given bytes to the given {@link + * StringBuilder}. + * + * @param bytes the bytes to calculate the digest over. + * @param builder the string builder to append the digest to. + * + * @return the given string builder. + */ + public static StringBuilder appendMd5DigestAsHex(byte[] bytes, StringBuilder builder) { + return appendDigestAsHex(MD5_ALGORITHM_NAME, bytes, builder); + } + + /** + * Append a hexadecimal string representation of the MD5 digest of the given inputStream to the given {@link + * StringBuilder}. + * + * @param inputStream the inputStream to calculate the digest over. + * @param builder the string builder to append the digest to. + * + * @return the given string builder. + */ + public static StringBuilder appendMd5DigestAsHex(InputStream inputStream, StringBuilder builder) + throws IOException { + return appendDigestAsHex(MD5_ALGORITHM_NAME, inputStream, builder); + } + + + /** + * Create a new {@link MessageDigest} with the given algorithm. Necessary because {@code MessageDigest} is not + * thread-safe. + */ + private static MessageDigest getDigest(String algorithm) { + try { + return MessageDigest.getInstance(algorithm); + } catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException("Could not find MessageDigest with algorithm \"" + algorithm + "\"", ex); + } + } + + private static byte[] digest(String algorithm, byte[] bytes) { + return getDigest(algorithm).digest(bytes); + } + + private static byte[] digest(String algorithm, InputStream inputStream) throws IOException { + MessageDigest messageDigest = getDigest(algorithm); + final byte[] buffer = new byte[2048]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + messageDigest.update(buffer, 0, bytesRead); + } + return messageDigest.digest(); + } + + private static String digestAsHexString(String algorithm, byte[] bytes) { + char[] hexDigest = digestAsHexChars(algorithm, bytes); + return new String(hexDigest); + } + + private static String digestAsHexString(String algorithm, InputStream inputStream) throws IOException { + char[] hexDigest = digestAsHexChars(algorithm, inputStream); + return new String(hexDigest); + } + + private static StringBuilder appendDigestAsHex(String algorithm, byte[] bytes, StringBuilder builder) { + char[] hexDigest = digestAsHexChars(algorithm, bytes); + return builder.append(hexDigest); + } + + private static StringBuilder appendDigestAsHex(String algorithm, InputStream inputStream, StringBuilder builder) + throws IOException { + + char[] hexDigest = digestAsHexChars(algorithm, inputStream); + return builder.append(hexDigest); + } + + private static char[] digestAsHexChars(String algorithm, byte[] bytes) { + byte[] digest = digest(algorithm, bytes); + return encodeHex(digest); + } + + private static char[] digestAsHexChars(String algorithm, InputStream inputStream) throws IOException { + byte[] digest = digest(algorithm, inputStream); + return encodeHex(digest); + } + + private static char[] encodeHex(byte[] bytes) { + char chars[] = new char[32]; + for (int i = 0; i < chars.length; i = i + 2) { + byte b = bytes[i / 2]; + chars[i] = HEX_CHARS[(b >>> 0x4) & 0xf]; + chars[i + 1] = HEX_CHARS[b & 0xf]; + } + return chars; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/Executors.java b/api/src/main/java/com/yanzhenjie/andserver/util/Executors.java new file mode 100644 index 0000000000000000000000000000000000000000..72569f58b2f70e2d2fac2c36a4e1494fe9d724e3 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/Executors.java @@ -0,0 +1,105 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util; + + +import ohos.eventhandler.EventHandler; +import ohos.eventhandler.EventRunner; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +/** + * Created by Zhenjie Yan on 2018/9/10. + */ +public class Executors { + + private static Executors instance; + + /** + * Get instance. + * + * @return {@link Executors}. + */ + public static Executors getInstance() { + if (instance == null) { + synchronized (Executors.class) { + if (instance == null) { + instance = new Executors(); + } + } + } + return instance; + } + + /** + * Executor Service. + */ + private final ExecutorService mService; + + /** + * Handler. + */ + private static EventHandler mHandler; + + private Executors() { + mService = java.util.concurrent.Executors.newCachedThreadPool(); + mHandler = new EventHandler(EventRunner.getMainEventRunner()); + } + + /** + * Execute a runnable. + */ + public void execute(Runnable runnable) { + mService.execute(runnable); + } + + /** + * Submit a runnable. + */ + public Future submit(Runnable runnable) { + return mService.submit(runnable); + } + + /** + * Submit a runnable. + */ + public Future submit(Runnable runnable, T result) { + return mService.submit(runnable, result); + } + + /** + * Submit a callable. + */ + public Future submit(Callable callable) { + return mService.submit(callable); + } + + /** + * Post a runnable. + */ + public void post(Runnable command) { + mHandler.postTask(command); + } + + /** + * Delay post a runnable. + */ + public void postDelayed(Runnable command, long delayedMillis) { + mHandler.postTask(command, delayedMillis); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/HttpDateFormat.java b/api/src/main/java/com/yanzhenjie/andserver/util/HttpDateFormat.java new file mode 100644 index 0000000000000000000000000000000000000000..d53affe24608b380735f5c41d07672f6fa8c4645 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/HttpDateFormat.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +/** + * Utility class to generate HTTP dates. + * + * @author Remy Maucherat + */ +public final class HttpDateFormat { + + /** + * The date format of the Http header. + */ + private static final String RFC1123_DATE = "EEE, dd MMM yyyy HH:mm:ss zzz"; + private static final SimpleDateFormat[] FORMATS_TEMPLATE = {new SimpleDateFormat(RFC1123_DATE, Locale.US), + new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US), + new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US)}; + + private static final TimeZone GMT_ZONE = TimeZone.getTimeZone("GMT"); + private static final SimpleDateFormat FORMAT = new SimpleDateFormat(RFC1123_DATE, Locale.US); + + static { + FORMAT.setTimeZone(GMT_ZONE); + } + + /** + * Get the current date in HTTP format. + * + * @return the HTTP date. + */ + public static String getCurrentDate() { + synchronized (FORMAT) { + long now = System.currentTimeMillis(); + return FORMAT.format(new Date(now)); + } + } + + /** + * Get the HTTP format of the specified date. + * + * @param value the date. + * + * @return the HTTP date. + */ + public static String formatDate(long value) { + synchronized (HttpDateFormat.class) { + Date dateValue = new Date(value); + return FORMAT.format(dateValue); + } + } + + /** + * Try to parse the given date as a HTTP date. + * + * @param value the HTTP date. + * + * @return the date as a long. + */ + public static long parseDate(String value) { + Date date = null; + for (SimpleDateFormat format : FORMATS_TEMPLATE) { + try { + date = format.parse(value); + } catch (ParseException e) { + // Nothing. + } + } + + if (date == null) { + return -1L; + } + return date.getTime(); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/HttpHeaders.java b/api/src/main/java/com/yanzhenjie/andserver/util/HttpHeaders.java new file mode 100644 index 0000000000000000000000000000000000000000..ee91c1a87e832b919f272b89d928923801e68924 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/HttpHeaders.java @@ -0,0 +1,25 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util; + +/** + * Created by Zhenjie Yan on 2018/9/7. + * + * @deprecated use {@link com.yanzhenjie.andserver.http.HttpHeaders} instead. + */ +@Deprecated +public interface HttpHeaders extends com.yanzhenjie.andserver.http.HttpHeaders { +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/IOUtils.java b/api/src/main/java/com/yanzhenjie/andserver/util/IOUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..9065b782223928b9934f5bf8401a7a47f6226d2f --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/IOUtils.java @@ -0,0 +1,665 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util; + +import com.yanzhenjie.andserver.http.RequestBody; +import ohos.data.usage.StatVfs; +import ohos.system.version.SystemVersion; + +import java.io.*; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +/** + * Created in 2016/4/12 21:21. + */ +public class IOUtils { + + private static final byte[] EMPTY_CONTENT = new byte[0]; + + public static void closeQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (Exception ignored) { + } + } + + RequestBody requestBody; + } + + public static void flushQuietly(Flushable flushable) { + if (flushable != null) { + try { + flushable.flush(); + } catch (Exception ignored) { + } + } + } + + public static BufferedInputStream toBufferedInputStream(InputStream inputStream) { + return inputStream instanceof BufferedInputStream + ? (BufferedInputStream) inputStream + : new BufferedInputStream(inputStream); + } + + public static BufferedOutputStream toBufferedOutputStream(OutputStream outputStream) { + return outputStream instanceof BufferedOutputStream + ? (BufferedOutputStream) outputStream + : new BufferedOutputStream(outputStream); + } + + public static BufferedReader toBufferedReader(Reader reader) { + return reader instanceof BufferedReader ? (BufferedReader) reader : new BufferedReader(reader); + } + + public static BufferedWriter toBufferedWriter(Writer writer) { + return writer instanceof BufferedWriter ? (BufferedWriter) writer : new BufferedWriter(writer); + } + + public static InputStream toInputStream(CharSequence input) throws UnsupportedEncodingException { + return new ByteArrayInputStream(input.toString().getBytes("UTF-8")); + } + + public static InputStream toInputStream(CharSequence input, String charset) { + return toInputStream(input, Charset.forName(charset)); + } + + public static InputStream toInputStream(CharSequence input, Charset charset) { + byte[] bytes = input.toString().getBytes(charset); + return new ByteArrayInputStream(bytes); + } + + public static InputStream createEmptyInput() { + return new ByteArrayInputStream(EMPTY_CONTENT); + } + + public static InputStream toNonClosing(InputStream in) { + Assert.notNull(in, "No InputStream specified"); + return new NonClosingInputStream(in); + } + + public static OutputStream toNonClosing(OutputStream out) { + Assert.notNull(out, "No OutputStream specified."); + return new NonClosingOutputStream(out); + } + + public static String toString(InputStream input) throws IOException { + return new String(toByteArray(input),"UTF-8"); + } + + public static String toString(InputStream input, String charset) throws IOException { + return new String(toByteArray(input), charset); + } + + public static String toString(InputStream input, Charset charset) throws IOException { + return new String(toByteArray(input), charset); + } + + public static String toString(Reader input) throws IOException { + return new String(toByteArray(input),"UTF-8"); + } + + public static String toString(Reader input, String charset) throws IOException { + return new String(toByteArray(input), charset); + } + + public static String toString(Reader input, Charset charset) throws IOException { + return new String(toByteArray(input), charset); + } + + public static String toString(byte[] byteArray) throws UnsupportedEncodingException { + return new String(byteArray,"UTF-8"); + } + + public static String toString(byte[] byteArray, String charset) { + return toString(byteArray, Charset.forName(charset)); + } + + public static String toString(byte[] byteArray, Charset charset) { + return new String(byteArray, charset); + } + + public static byte[] toByteArray(CharSequence input) throws UnsupportedEncodingException { + if (input == null) { + return new byte[0]; + } + return input.toString().getBytes("UTF-8"); + } + + public static byte[] toByteArray(CharSequence input, String charset) { + return toByteArray(input, Charset.forName(charset)); + } + + public static byte[] toByteArray(CharSequence input, Charset charset) { + if (input == null) { + return new byte[0]; + } else { + return input.toString().getBytes(charset); + } + } + + public static byte[] toByteArray(InputStream input) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + write(input, output); + output.close(); + return output.toByteArray(); + } + + public static byte[] toByteArray(InputStream input, int size) throws IOException { + if (size < 0) { + throw new IllegalArgumentException("Size must be equal or greater than zero: " + size); + } + + if (size == 0) { + return new byte[0]; + } + + byte[] data = new byte[size]; + int offset = 0; + int byteCount; + while ((offset < size) && (byteCount = input.read(data, offset, size - offset)) != -1) { + offset += byteCount; + } + + if (offset != size) { + throw new IOException("Unexpected byte count size. current: " + offset + ", excepted: " + size); + } + return data; + } + + public static byte[] toByteArray(Reader input) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + write(input, output); + output.close(); + return output.toByteArray(); + } + + public static byte[] toByteArray(Reader input, String charset) throws IOException { + return toByteArray(input, Charset.forName(charset)); + } + + public static byte[] toByteArray(Reader input, Charset charset) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + write(input, output, charset); + output.close(); + return output.toByteArray(); + } + + public static char[] toCharArray(CharSequence input) throws IOException { + CharArrayWriter output = new CharArrayWriter(); + write(output, input); + return output.toCharArray(); + } + + public static char[] toCharArray(InputStream input) throws IOException { + CharArrayWriter output = new CharArrayWriter(); + write(input, output); + return output.toCharArray(); + } + + public static char[] toCharArray(InputStream input, String charset) throws IOException { + return toCharArray(input, Charset.forName(charset)); + } + + public static char[] toCharArray(InputStream input, Charset charset) throws IOException { + CharArrayWriter output = new CharArrayWriter(); + write(input, output, charset); + return output.toCharArray(); + } + + public static char[] toCharArray(Reader input) throws IOException { + CharArrayWriter output = new CharArrayWriter(); + write(input, output); + return output.toCharArray(); + } + + public static List readLines(InputStream input, String charset) throws IOException { + return readLines(input, Charset.forName(charset)); + } + + public static List readLines(InputStream input, Charset charset) throws IOException { + Reader reader = new InputStreamReader(input, charset); + return readLines(reader); + } + + public static List readLines(InputStream input) throws IOException { + Reader reader = new InputStreamReader(input,"UTF-8"); + return readLines(reader); + } + + public static List readLines(Reader input) throws IOException { + BufferedReader reader = toBufferedReader(input); + List list = new ArrayList<>(); + String line = reader.readLine(); + while (line != null) { + list.add(line); + line = reader.readLine(); + } + return list; + } + + public static void write(OutputStream output, byte[] data) throws IOException { + if (data != null) { + output.write(data); + output.flush(); + } + } + + public static void write(Writer output, byte[] data) throws IOException { + if (data != null) { + output.write(new String(data,"UTF-8")); + output.flush(); + } + } + + public static void write(Writer output, byte[] data, String charset) throws IOException { + write(output, data, Charset.forName(charset)); + } + + public static void write(Writer output, byte[] data, Charset charset) throws IOException { + if (data != null) { + output.write(new String(data, charset)); + output.flush(); + } + } + + public static void write(Writer output, char[] data) throws IOException { + if (data != null) { + output.write(data); + output.flush(); + } + } + + public static void write(OutputStream output, char[] data) throws IOException { + if (data != null) { + output.write(new String(data).getBytes("UTF-8")); + output.flush(); + } + } + + public static void write(OutputStream output, char[] data, String charset) throws IOException { + write(output, data, Charset.forName(charset)); + } + + public static void write(OutputStream output, char[] data, Charset charset) throws IOException { + if (data != null) { + output.write(new String(data).getBytes(charset)); + output.flush(); + } + } + + public static void write(Writer output, CharSequence data) throws IOException { + if (data != null) { + output.write(data.toString()); + output.flush(); + } + } + + public static void write(OutputStream output, CharSequence data) throws IOException { + if (data != null) { + output.write(data.toString().getBytes("UTF-8")); + output.flush(); + } + } + + public static void write(OutputStream output, CharSequence data, String charset) throws IOException { + write(output, data, Charset.forName(charset)); + } + + public static void write(OutputStream output, CharSequence data, Charset charset) throws IOException { + if (data != null) { + output.write(data.toString().getBytes(charset)); + output.flush(); + } + } + + public static void write(Reader input, OutputStream output) throws IOException { + Writer out = new OutputStreamWriter(output,"UTF-8"); + write(input, out); + } + + public static void write(InputStream input, OutputStream output) throws IOException { + int len; + byte[] buffer = new byte[4096]; + while ((len = input.read(buffer)) != -1) { + output.write(buffer, 0, len); + output.flush(); + } + } + + public static void write(InputStream input, Writer output) throws IOException { + Reader in = new InputStreamReader(input,"UTF-8"); + write(in, output); + } + + public static void write(Reader input, OutputStream output, String charset) throws IOException { + write(input, output, Charset.forName(charset)); + } + + public static void write(Reader input, OutputStream output, Charset charset) throws IOException { + Writer out = new OutputStreamWriter(output, charset); + write(input, out); + } + + public static void write(InputStream input, OutputStream output, String charset) throws IOException { + write(input, output, Charset.forName(charset)); + } + + public static void write(InputStream input, OutputStream output, Charset charset) throws IOException { + Reader in = new InputStreamReader(input, charset); + write(in, output); + } + + public static void write(InputStream input, Writer output, String charset) throws IOException { + write(input, output, Charset.forName(charset)); + } + + public static void write(InputStream input, Writer output, Charset charset) throws IOException { + Reader in = new InputStreamReader(input, charset); + write(in, output); + } + + public static void write(Reader input, Writer output) throws IOException { + int len; + char[] buffer = new char[4096]; + while (-1 != (len = input.read(buffer))) { + output.write(buffer, 0, len); + output.flush(); + } + } + + public static boolean contentEquals(InputStream input1, InputStream input2) throws IOException { + input1 = toBufferedInputStream(input1); + input2 = toBufferedInputStream(input2); + + int ch = input1.read(); + while (-1 != ch) { + int ch2 = input2.read(); + if (ch != ch2) { + return false; + } + ch = input1.read(); + } + int ch2 = input2.read(); + return ch2 == -1; + } + + public static boolean contentEquals(Reader input1, Reader input2) throws IOException { + input1 = toBufferedReader(input1); + input2 = toBufferedReader(input2); + + int ch = input1.read(); + while (-1 != ch) { + int ch2 = input2.read(); + if (ch != ch2) { + return false; + } + ch = input1.read(); + } + + int ch2 = input2.read(); + return ch2 == -1; + } + + public static boolean contentEqualsIgnoreEOL(Reader input1, Reader input2) throws IOException { + BufferedReader br1 = toBufferedReader(input1); + BufferedReader br2 = toBufferedReader(input2); + + String line1 = br1.readLine(); + String line2 = br2.readLine(); + while ((line1 != null) && (line2 != null) && (line1.equals(line2))) { + line1 = br1.readLine(); + line2 = br2.readLine(); + } + return line1 != null && (line2 == null || line1.equals(line2)); + } + + /** + * Access to a directory available size. + * + * @param path path. + * + * @return space size. + */ + public static long getDirSize(String path) { + StatVfs stat; + try { + stat = new StatVfs(path); + } catch (Exception e) { + return 0; + } + if (SystemVersion.getBuildVersion() >= 18) { + return stat.getFreeSpace() * stat.getAvailableSpace(); + } else { + return (long) stat.getFreeSpace() * (long) stat.getAvailableSpace(); + } + } + + /** + * If the folder can be written. + * + * @param path path. + * + * @return True: success, or false: failure. + */ + public static boolean canWrite(String path) { + return new File(path).canWrite(); + } + + /** + * If the folder can be readResponse. + * + * @param path path. + * + * @return True: success, or false: failure. + */ + public static boolean canRead(String path) { + return new File(path).canRead(); + } + + /** + * Create a folder, If the folder exists is not created. + * + * @param folderPath folder path. + * + * @return True: success, or false: failure. + */ + public static boolean createFolder(String folderPath) { + if (!folderPath.isEmpty()) { + File folder = new File(folderPath); + return createFolder(folder); + } + return false; + } + + /** + * Create a folder, If the folder exists is not created. + * + * @param targetFolder folder path. + * + * @return True: success, or false: failure. + */ + public static boolean createFolder(File targetFolder) { + if (targetFolder.exists()) { + if (targetFolder.isDirectory()) { + return true; + } + //noinspection ResultOfMethodCallIgnored + targetFolder.delete(); + } + return targetFolder.mkdirs(); + } + + /** + * Create a folder, If the folder exists is not created. + * + * @param folderPath folder path. + * + * @return True: success, or false: failure. + */ + public static boolean createNewFolder(String folderPath) { + return delFileOrFolder(folderPath) && createFolder(folderPath); + } + + /** + * Create a folder, If the folder exists is not created. + * + * @param targetFolder folder path. + * + * @return True: success, or false: failure. + */ + public static boolean createNewFolder(File targetFolder) { + return delFileOrFolder(targetFolder) && createFolder(targetFolder); + } + + /** + * Create a file, If the file exists is not created. + * + * @param filePath file path. + * + * @return True: success, or false: failure. + */ + public static boolean createFile(String filePath) { + if (!filePath.isEmpty()) { + File file = new File(filePath); + return createFile(file); + } + return false; + } + + /** + * Create a file, If the file exists is not created. + * + * @param targetFile file. + * + * @return True: success, or false: failure. + */ + public static boolean createFile(File targetFile) { + if (targetFile.exists()) { + if (targetFile.isFile()) { + return true; + } + delFileOrFolder(targetFile); + } + try { + return targetFile.createNewFile(); + } catch (IOException e) { + return false; + } + } + + /** + * Create a new file, if the file exists, delete and create again. + * + * @param filePath file path. + * + * @return True: success, or false: failure. + */ + public static boolean createNewFile(String filePath) { + if (!filePath.isEmpty()) { + File file = new File(filePath); + return createNewFile(file); + } + return false; + } + + /** + * Create a new file, if the file exists, delete and create again. + * + * @param targetFile file. + * + * @return True: success, or false: failure. + */ + public static boolean createNewFile(File targetFile) { + if (targetFile.exists()) { + delFileOrFolder(targetFile); + } + try { + return targetFile.createNewFile(); + } catch (IOException e) { + return false; + } + } + + /** + * Delete file or folder. + * + * @param path path. + * + * @return is succeed. + * + * @see #delFileOrFolder(File) + */ + public static boolean delFileOrFolder(String path) { + return delFileOrFolder(new File(path)); + } + + /** + * Delete file or folder. + * + * @param file file. + * + * @return is succeed. + * + * @see #delFileOrFolder(String) + */ + public static boolean delFileOrFolder(File file) { + if (file == null || !file.exists()) { + // do nothing + } else if (file.isFile()) { + file.delete(); + } else if (file.isDirectory()) { + File[] files = file.listFiles(); + if (files != null) { + for (File sonFile: files) { + delFileOrFolder(sonFile); + } + } + file.delete(); + } + return true; + } + + private static class NonClosingInputStream extends FilterInputStream { + + public NonClosingInputStream(InputStream in) { + super(in); + } + + @Override + public void close() throws IOException { + } + } + + private static class NonClosingOutputStream extends FilterOutputStream { + + public NonClosingOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(byte[] b, int off, int let) throws IOException { + // It is critical that we override this method for performance. + out.write(b, off, let); + } + + @Override + public void close() throws IOException { + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/LinkedCaseInsensitiveMap.java b/api/src/main/java/com/yanzhenjie/andserver/util/LinkedCaseInsensitiveMap.java new file mode 100644 index 0000000000000000000000000000000000000000..86ef7282a57d6e4547837878ee41e0b04e1e4ba8 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/LinkedCaseInsensitiveMap.java @@ -0,0 +1,249 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util; + + +import java.io.Serializable; +import java.util.*; + +/** + * Created by Zhenjie Yan on 2018/6/29. + */ +public class LinkedCaseInsensitiveMap implements Map, Serializable, Cloneable { + + private final LinkedHashMap mSource; + private final HashMap mCaseInsensitiveKeys; + private final Locale mLocale; + + /** + * Create a new instance that stores case-insensitive keys according to the default mLocale, by default in lower + * case. + */ + public LinkedCaseInsensitiveMap() { + this((Locale) null); + } + + /** + * Create a new instance that stores case-insensitive keys according to the given mLocale, by default in lower + * case. + * + * @param locale the {@link Locale} to use for case-insensitive key conversion. + */ + public LinkedCaseInsensitiveMap(Locale locale) { + this(16, locale); + } + + /** + * Create a new instance that wraps a {@link LinkedHashMap} with the given initial capacity and stores + * case-insensitive keys according to the default mLocale, by default in lower case. + * + * @param initialCapacity the initial capacity. + */ + public LinkedCaseInsensitiveMap(int initialCapacity) { + this(initialCapacity, null); + } + + /** + * Create a new instance that wraps a {@link LinkedHashMap} with the given initial capacity and stores + * case-insensitive keys according to the given mLocale, by default in lower case. + * + * @param initialCapacity the initial capacity. + * @param locale the mLocale to use for case-insensitive key conversion. + */ + public LinkedCaseInsensitiveMap(int initialCapacity, Locale locale) { + this.mSource = new LinkedHashMap(initialCapacity) { + @Override + public boolean containsKey(Object key) { + return LinkedCaseInsensitiveMap.this.containsKey(key); + } + + @Override + protected boolean removeEldestEntry(Entry eldest) { + boolean isRemoved = LinkedCaseInsensitiveMap.this.removeEldestEntry(eldest); + if (isRemoved) { + mCaseInsensitiveKeys.remove(convertKey(eldest.getKey())); + } + return isRemoved; + } + }; + this.mCaseInsensitiveKeys = new HashMap<>(initialCapacity); + this.mLocale = (locale != null ? locale : Locale.getDefault()); + } + + @SuppressWarnings("unchecked") + private LinkedCaseInsensitiveMap(LinkedCaseInsensitiveMap other) { + this.mSource = (LinkedHashMap) other.mSource.clone(); + this.mCaseInsensitiveKeys = (HashMap) other.mCaseInsensitiveKeys.clone(); + this.mLocale = other.mLocale; // No need to clone. + } + + + @Override + public int size() { + return this.mSource.size(); + } + + @Override + public boolean isEmpty() { + return this.mSource.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return (key instanceof String && this.mCaseInsensitiveKeys.containsKey(convertKey((String) key))); + } + + @Override + public boolean containsValue(Object value) { + return this.mSource.containsValue(value); + } + + @Override + public V get(Object key) { + if (key instanceof String) { + String caseInsensitiveKey = this.mCaseInsensitiveKeys.get(convertKey((String) key)); + if (caseInsensitiveKey != null) { + return this.mSource.get(caseInsensitiveKey); + } + } + return null; + } + + @Override + public V getOrDefault(Object key, V defaultValue) { + if (key instanceof String) { + String caseInsensitiveKey = this.mCaseInsensitiveKeys.get(convertKey((String) key)); + if (caseInsensitiveKey != null) { + return this.mSource.get(caseInsensitiveKey); + } + } + return defaultValue; + } + + @Override + public V put(String key, V value) { + String oldKey = this.mCaseInsensitiveKeys.put(convertKey(key), key); + if (oldKey != null && !oldKey.equals(key)) { + this.mSource.remove(oldKey); + } + return this.mSource.put(key, value); + } + + @Override + public void putAll( Map map) { + if (map.isEmpty()) { + return; + } + + for (Entry entry : map.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public V remove(Object key) { + if (key instanceof String) { + String caseInsensitiveKey = this.mCaseInsensitiveKeys.remove(convertKey((String) key)); + if (caseInsensitiveKey != null) { + return this.mSource.remove(caseInsensitiveKey); + } + } + return null; + } + + @Override + public void clear() { + this.mCaseInsensitiveKeys.clear(); + this.mSource.clear(); + } + + + @Override + public Set keySet() { + return this.mSource.keySet(); + } + + + @Override + public Collection values() { + return this.mSource.values(); + } + + + @Override + public Set> entrySet() { + return this.mSource.entrySet(); + } + + @Override + public LinkedCaseInsensitiveMap clone() { + return new LinkedCaseInsensitiveMap<>(this); + } + + @Override + public boolean equals(Object obj) { + return this.mSource.equals(obj); + } + + @Override + public int hashCode() { + return this.mSource.hashCode(); + } + + @Override + public String toString() { + return this.mSource.toString(); + } + + + /** + * Return the locale used by this {@code LinkedCaseInsensitiveMap}. Used for case-insensitive key conversion. + * + * @see #LinkedCaseInsensitiveMap(Locale) + * @see #convertKey(String) + */ + public Locale getLocale() { + return this.mLocale; + } + + /** + * Convert the given key to a case-insensitive key. + * + *

The default implementation converts the key to lower-case according to this map's locale. + * + * @param key the user-specified key. + * + * @return the key to use for string. + * + * @see String#toLowerCase(Locale) + */ + protected String convertKey(String key) { + return key.toLowerCase(getLocale()); + } + + /** + * Determine whether this map should remove the given eldest entry. + * + * @param eldest the candidate entry. + * + * @return true for removing it, false for keeping it. + * + * @see LinkedHashMap#removeEldestEntry(Entry) + */ + protected boolean removeEldestEntry(Entry eldest) { + return false; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/LinkedMultiValueMap.java b/api/src/main/java/com/yanzhenjie/andserver/util/LinkedMultiValueMap.java new file mode 100644 index 0000000000000000000000000000000000000000..617cb7af044938146609d9469cddb36f5715278c --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/LinkedMultiValueMap.java @@ -0,0 +1,165 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util; + +import java.util.*; + +/** + * Created by Zhenjie Yan on 2018/6/21. + */ +public class LinkedMultiValueMap implements MultiValueMap, Cloneable { + + private final Map> mSource; + + public LinkedMultiValueMap() { + mSource = new LinkedHashMap<>(); + } + + public LinkedMultiValueMap(int initialCapacity) { + mSource = new LinkedHashMap<>(initialCapacity); + } + + public LinkedMultiValueMap(Map> otherMap) { + mSource = new LinkedHashMap<>(otherMap); + } + + @Override + public void add(K key, V value) { + List values = mSource.get(key); + if (values == null) { + values = new LinkedList<>(); + mSource.put(key, values); + } + values.add(value); + } + + @Override + public V getFirst(K key) { + List values = mSource.get(key); + return (values != null ? values.get(0) : null); + } + + @Override + public void set(K key, V value) { + List values = new LinkedList<>(); + values.add(value); + this.mSource.put(key, values); + } + + @Override + public void setAll(Map values) { + for (Map.Entry entry : values.entrySet()) { + set(entry.getKey(), entry.getValue()); + } + } + + @Override + public Map toSingleValueMap() { + LinkedHashMap singleValueMap = new LinkedHashMap<>(mSource.size()); + for (Map.Entry> entry : mSource.entrySet()) { + singleValueMap.put(entry.getKey(), entry.getValue().get(0)); + } + return singleValueMap; + } + + @Override + public int size() { + return mSource.size(); + } + + @Override + public boolean isEmpty() { + return mSource.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return mSource.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return mSource.containsValue(value); + } + + @Override + public List get(Object key) { + return mSource.get(key); + } + + @Override + public List put(K key, List value) { + return mSource.put(key, value); + } + + @Override + public List remove(Object key) { + return mSource.remove(key); + } + + @Override + public void putAll(Map> map) { + mSource.putAll(map); + } + + @Override + public void clear() { + mSource.clear(); + } + + @Override + public Set keySet() { + return mSource.keySet(); + } + + @Override + public Collection> values() { + return mSource.values(); + } + + @Override + public Set>> entrySet() { + return mSource.entrySet(); + } + + @Override + public LinkedMultiValueMap clone() { + return new LinkedMultiValueMap<>(this); + } + + @Override + public boolean equals(Object obj) { + return mSource.equals(obj); + } + + @Override + public int hashCode() { + return mSource.hashCode(); + } + + @Override + public String toString() { + return mSource.toString(); + } + + public LinkedMultiValueMap deepCopy() { + LinkedMultiValueMap copy = new LinkedMultiValueMap<>(mSource.size()); + for (Map.Entry> entry : mSource.entrySet()) { + copy.put(entry.getKey(), new LinkedList<>(entry.getValue())); + } + return copy; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/MediaType.java b/api/src/main/java/com/yanzhenjie/andserver/util/MediaType.java new file mode 100644 index 0000000000000000000000000000000000000000..1e2787651629def6ee440d4a672b02485992bb6a --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/MediaType.java @@ -0,0 +1,678 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util; + +import com.yanzhenjie.andserver.error.InvalidMediaTypeException; +import com.yanzhenjie.andserver.error.InvalidMimeTypeException; +import com.yanzhenjie.andserver.util.comparator.CompoundComparator; + +import java.io.Serializable; +import java.nio.charset.Charset; +import java.util.*; + +/** + * Created by Zhenjie Yan on 2018/6/27. + */ +public class MediaType extends MimeType implements Serializable { + + /** + * Public constant media type that includes all media ranges (i.e. "*/*"). + */ + public static final MediaType ALL; + + /** + * A String equivalent of {@code MediaType#ALL}. + */ + public static final String ALL_VALUE = "*/*"; + + /** + * Public constant media type for {@code application/json}. + * + * @see #APPLICATION_JSON_UTF8 + */ + public final static MediaType APPLICATION_JSON; + + /** + * A String equivalent of {@code MediaType#APPLICATION_JSON}. + * + * @see #APPLICATION_JSON_UTF8_VALUE + */ + public final static String APPLICATION_JSON_VALUE = "application/json"; + + /** + * Public constant media type for {@code application/json;charset=UTF-8}. + */ + public final static MediaType APPLICATION_JSON_UTF8; + + /** + * A String equivalent of {@link MediaType#APPLICATION_JSON_UTF8}. + */ + public final static String APPLICATION_JSON_UTF8_VALUE = APPLICATION_JSON_VALUE + ";charset=UTF-8"; + /** + * Public constant media type for {@code application/xml}. + */ + public final static MediaType APPLICATION_XML; + + /** + * A String equivalent of {@link MediaType#APPLICATION_XML}. + */ + public final static String APPLICATION_XML_VALUE = "application/xml"; + + /** + * Public constant media type for {@code application/xml}. + */ + public final static MediaType APPLICATION_XML_UTF8; + + /** + * A String equivalent of {@link MediaType#APPLICATION_XML}. + */ + public final static String APPLICATION_XML_UTF8_VALUE = APPLICATION_XML_VALUE + ";charset=UTF-8"; + + /** + * Public constant media type for {@code application/atom+xml}. + */ + public final static MediaType APPLICATION_ATOM_XML; + + /** + * A String equivalent of {@link MediaType#APPLICATION_ATOM_XML}. + */ + public final static String APPLICATION_ATOM_XML_VALUE = "application/atom+xml"; + + /** + * Public constant media type for {@code application/x-www-form-urlencoded}. + */ + public final static MediaType APPLICATION_FORM_URLENCODED; + + /** + * A String equivalent of {@link MediaType#APPLICATION_FORM_URLENCODED}. + */ + public final static String APPLICATION_FORM_URLENCODED_VALUE = "application/x-www-form-urlencoded"; + + /** + * Public constant media type for {@code application/octet-stream}. + */ + public final static MediaType APPLICATION_OCTET_STREAM; + + /** + * A String equivalent of {@link MediaType#APPLICATION_OCTET_STREAM}. + */ + public final static String APPLICATION_OCTET_STREAM_VALUE = "application/octet-stream"; + + /** + * Public constant media type for {@code application/rss+xml}. + */ + public final static MediaType APPLICATION_RSS_XML; + + /** + * A String equivalent of {@link MediaType#APPLICATION_RSS_XML}. + */ + public final static String APPLICATION_RSS_XML_VALUE = "application/rss+xml"; + + /** + * Public constant media type for {@code application/xhtml+xml}. + */ + public final static MediaType APPLICATION_XHTML_XML; + + /** + * A String equivalent of {@link MediaType#APPLICATION_XHTML_XML}. + */ + public final static String APPLICATION_XHTML_XML_VALUE = "application/xhtml+xml"; + + /** + * Public constant media type for {@code application/pdf}. + */ + public final static MediaType APPLICATION_PDF; + + /** + * A String equivalent of {@link MediaType#APPLICATION_PDF}. + */ + public final static String APPLICATION_PDF_VALUE = "application/pdf"; + + /** + * Public constant media type for {@code image/gif}. + */ + public final static MediaType IMAGE_GIF; + + /** + * A String equivalent of {@link MediaType#IMAGE_GIF}. + */ + public final static String IMAGE_GIF_VALUE = "image/gif"; + + /** + * Public constant media type for {@code image/jpeg}. + */ + public final static MediaType IMAGE_JPEG; + + /** + * A String equivalent of {@link MediaType#IMAGE_JPEG}. + */ + public final static String IMAGE_JPEG_VALUE = "image/jpeg"; + + /** + * Public constant media type for {@code image/png}. + */ + public final static MediaType IMAGE_PNG; + + /** + * A String equivalent of {@link MediaType#IMAGE_PNG}. + */ + public final static String IMAGE_PNG_VALUE = "image/png"; + + /** + * Public constant media type for {@code multipart/form-data}. + */ + public final static MediaType MULTIPART_FORM_DATA; + + /** + * A String equivalent of {@link MediaType#MULTIPART_FORM_DATA}. + */ + public final static String MULTIPART_FORM_DATA_VALUE = "multipart/form-data"; + + /** + * Public constant media type for {@code text/event-stream}. + * + * @see Server-Sent Events W3C recommendation + */ + public final static MediaType TEXT_EVENT_STREAM; + + /** + * A String equivalent of {@link MediaType#TEXT_EVENT_STREAM}. + */ + public final static String TEXT_EVENT_STREAM_VALUE = "text/event-stream"; + + /** + * Public constant media type for {@code text/html}. + */ + public final static MediaType TEXT_HTML; + + /** + * A String equivalent of {@link MediaType#TEXT_HTML}. + */ + public final static String TEXT_HTML_VALUE = "text/html"; + + /** + * Public constant media type for {@code text/markdown}. + */ + public final static MediaType TEXT_MARKDOWN; + + /** + * A String equivalent of {@link MediaType#TEXT_MARKDOWN}. + */ + public final static String TEXT_MARKDOWN_VALUE = "text/markdown"; + + /** + * Public constant media type for {@code text/plain}. + */ + public final static MediaType TEXT_PLAIN; + + /** + * A String equivalent of {@link MediaType#TEXT_PLAIN}. + */ + public final static String TEXT_PLAIN_VALUE = "text/plain"; + + /** + * Public constant media type for {@code text/xml}. + */ + public final static MediaType TEXT_XML; + + /** + * A String equivalent of {@link MediaType#TEXT_XML}. + */ + public final static String TEXT_XML_VALUE = "text/xml"; + + private static final String PARAM_QUALITY_FACTOR = "q"; + + static { + ALL = valueOf(ALL_VALUE); + APPLICATION_JSON = valueOf(APPLICATION_JSON_VALUE); + APPLICATION_JSON_UTF8 = valueOf(APPLICATION_JSON_UTF8_VALUE); + APPLICATION_XML = valueOf(APPLICATION_XML_VALUE); + APPLICATION_XML_UTF8 = valueOf(APPLICATION_XML_UTF8_VALUE); + APPLICATION_ATOM_XML = valueOf(APPLICATION_ATOM_XML_VALUE); + APPLICATION_FORM_URLENCODED = valueOf(APPLICATION_FORM_URLENCODED_VALUE); + APPLICATION_OCTET_STREAM = valueOf(APPLICATION_OCTET_STREAM_VALUE); + APPLICATION_RSS_XML = valueOf(APPLICATION_RSS_XML_VALUE); + APPLICATION_XHTML_XML = valueOf(APPLICATION_XHTML_XML_VALUE); + APPLICATION_PDF = valueOf(APPLICATION_PDF_VALUE); + IMAGE_GIF = valueOf(IMAGE_GIF_VALUE); + IMAGE_JPEG = valueOf(IMAGE_JPEG_VALUE); + IMAGE_PNG = valueOf(IMAGE_PNG_VALUE); + MULTIPART_FORM_DATA = valueOf(MULTIPART_FORM_DATA_VALUE); + TEXT_EVENT_STREAM = valueOf(TEXT_EVENT_STREAM_VALUE); + TEXT_HTML = valueOf(TEXT_HTML_VALUE); + TEXT_MARKDOWN = valueOf(TEXT_MARKDOWN_VALUE); + TEXT_PLAIN = valueOf(TEXT_PLAIN_VALUE); + TEXT_XML = valueOf(TEXT_XML_VALUE); + } + + /** + * Create a new {@code MediaType} for the given primary type.

The {@linkplain #getSubtype() subtype} is set to + * "*", parameters empty. + * + * @param type the primary type. + * + * @throws IllegalArgumentException if any of the parameters contain illegal characters. + */ + public MediaType(String type) { + super(type); + } + + /** + * Create a new {@code MediaType} for the given primary type and subtype. + * + *

The parameters are empty. + * + * @param type the primary type. + * @param subtype the subtype. + * + * @throws IllegalArgumentException if any of the parameters contain illegal characters. + */ + public MediaType(String type, String subtype) { + super(type, subtype, Collections.emptyMap()); + } + + /** + * Create a new {@code MediaType} for the given type, subtype, and character set. + * + * @param type the primary type. + * @param subtype the subtype. + * @param charset the character set. + * + * @throws IllegalArgumentException if any of the parameters contain illegal characters. + */ + public MediaType(String type, String subtype, Charset charset) { + super(type, subtype, charset); + } + + /** + * Create a new {@code MediaType} for the given type, subtype, and quality value. + * + * @param type the primary type. + * @param subtype the subtype. + * @param qualityValue the quality value. + * + * @throws IllegalArgumentException if any of the parameters contain illegal characters. + */ + public MediaType(String type, String subtype, double qualityValue) { + this(type, subtype, Collections.singletonMap(PARAM_QUALITY_FACTOR, Double.toString(qualityValue))); + } + + /** + * Copy-constructor that copies the type, subtype and parameters of the given {@code MediaType}, and allows to set + * the specified character set. + * + * @param other the other media type. + * @param charset the character set. + * + * @throws IllegalArgumentException if any of the parameters contain illegal characters. + */ + public MediaType(MediaType other, Charset charset) { + super(other, charset); + } + + /** + * Copy-constructor that copies the type and subtype of the given {@code MediaType}, and allows for different + * parameter. + * + * @param other the other media type. + * @param parameters the parameters, may be null. + * + * @throws IllegalArgumentException if any of the parameters contain illegal characters. + */ + public MediaType(MediaType other, Map parameters) { + super(other.getType(), other.getSubtype(), parameters); + } + + /** + * Create a new {@code MediaType} for the given type, subtype, and parameters. + * + * @param type the primary type. + * @param subtype the subtype. + * @param parameters the parameters, may be null. + * + * @throws IllegalArgumentException if any of the parameters contain illegal characters. + */ + public MediaType(String type, String subtype, Map parameters) { + super(type, subtype, parameters); + } + + @Override + protected void checkParameters(String attribute, String value) { + super.checkParameters(attribute, value); + if (PARAM_QUALITY_FACTOR.equals(attribute)) { + value = unquote(value); + double d = Double.parseDouble(value); + String message = "Invalid quality value '" + value + "': should be between 0.0 and 1.0"; + Assert.isTrue(d >= 0D && d <= 1D, message); + } + } + + /** + * Return the quality value, as indicated by a {@code q} parameter, if any. Defaults to {@code 1.0}. + * + * @return the quality factory. + */ + public double getQualityValue() { + String qualityFactory = getParameter(PARAM_QUALITY_FACTOR); + return (qualityFactory != null ? Double.parseDouble(unquote(qualityFactory)) : 1D); + } + + /** + * Indicate whether this {@code MediaType} includes the given media type. + * + *

For instance, {@code text/*} includes {@code text/plain} and {@code text/html}, and {@code application/*+xml} + * includes {@code application/soap+xml}, etc. This method is not symmetric. + * + * @param other the reference media type with which to compare. + * + * @return {@code true} if this media type includes the given media type; {@code false} otherwise. + */ + public boolean includes(MediaType other) { + return super.includes(other); + } + + /** + * Indicate whether this {@code MediaType} is compatible with the given media type. + * + *

For instance, {@code text/*} is compatible with {@code text/plain}, {@code text/html}, and vice versa. In + * effect, this method is similar to {@link #includes(MediaType)}, except that it is symmetric. + * + * @param other the reference media type with which to compare. + * + * @return {@code true} if this media type is compatible with the given media type; {@code false} otherwise. + */ + public boolean isCompatibleWith(MediaType other) { + return super.isCompatibleWith(other); + } + + /** + * Return a replica of this instance with the quality value of the given MediaType. + * + * @return the same instance if the given MediaType doesn't have a quality value, or a new one otherwise. + */ + public MediaType copyQualityValue(MediaType mediaType) { + if (!mediaType.getParameters().containsKey(PARAM_QUALITY_FACTOR)) { + return this; + } + Map params = new LinkedHashMap<>(getParameters()); + params.put(PARAM_QUALITY_FACTOR, mediaType.getParameters().get(PARAM_QUALITY_FACTOR)); + return new MediaType(this, params); + } + + /** + * Return a replica of this instance with its quality value removed. + * + * @return the same instance if the media type doesn't contain a quality value, or a new one otherwise. + */ + public MediaType removeQualityValue() { + if (!getParameters().containsKey(PARAM_QUALITY_FACTOR)) { + return this; + } + Map params = new LinkedHashMap<>(getParameters()); + params.remove(PARAM_QUALITY_FACTOR); + return new MediaType(this, params); + } + + + /** + * Parse the given String value into a {@code MediaType} object, with this method name following the 'valueOf' + * naming convention. + * + * @param value the string to parse. + * + * @throws InvalidMediaTypeException if the media type value cannot be parsed. + * @see #parseMediaType(String) + */ + public static MediaType valueOf(String value) { + return parseMediaType(value); + } + + /** + * Parse the given String into a single {@code MediaType}. + * + * @param mediaType the string to parse. + * + * @return the media type. + * + * @throws InvalidMediaTypeException if the media type value cannot be parsed. + */ + public static MediaType parseMediaType(String mediaType) { + MimeType type; + try { + type = MimeType.valueOf(mediaType); + } catch (InvalidMimeTypeException ex) { + throw new InvalidMediaTypeException(ex); + } + try { + return new MediaType(type.getType(), type.getSubtype(), type.getParameters()); + } catch (IllegalArgumentException ex) { + throw new InvalidMediaTypeException(mediaType, ex.getMessage()); + } + } + + /** + * Parse the given comma-separated string into a list of {@code MediaType} objects. + * + *

This method can be used to parse an Accept or Content-Type header. + * + * @param mediaTypes the string to parse. + * + * @return the list of media types. + * + * @throws InvalidMediaTypeException if the media type value cannot be parsed. + */ + public static List parseMediaTypes(String mediaTypes) { + if (mediaTypes.isEmpty()) { + return Collections.emptyList(); + } + + StringTokenizer st = new StringTokenizer(mediaTypes, ","); + List tokens = new ArrayList<>(); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + token = token.trim(); + if (token.length() > 0) { + tokens.add(token); + } + } + + List result = new ArrayList<>(tokens.size()); + for (String token: tokens) { + result.add(parseMediaType(token)); + } + return result; + } + + /** + * Parse the given list of (potentially) comma-separated strings into a list of {@code MediaType} objects. + * + *

This method can be used to parse an Accept or Content-Type header. + * + * @param mediaTypes the string to parse. + * + * @return the list of media types. + * + * @throws InvalidMediaTypeException if the media type value cannot be parsed. + */ + public static List parseMediaTypes(List mediaTypes) { + if (mediaTypes == null || mediaTypes.isEmpty()) { + return Collections.emptyList(); + } else if (mediaTypes.size() == 1) { + return parseMediaTypes(mediaTypes.get(0)); + } else { + List result = new ArrayList<>(8); + for (String mediaType: mediaTypes) { + result.addAll(parseMediaTypes(mediaType)); + } + return result; + } + } + + /** + * Sorts the given list of {@code MediaType} objects by specificity. + * + *

Given two media types:

  1. if either media type has a {@linkplain #isWildcardType() wildcard type}, then + * the media type without the wildcard is ordered before the other.
  2. if the two media types have different + * {@linkplain #getType() types}, then they are considered equal and remain their current order.
  3. if either + * media type has a {@linkplain #isWildcardSubtype() wildcard subtype}, then the media type without the wildcard is + * sorted before the other.
  4. if the two media types have different {@linkplain #getSubtype() subtypes}, then + * they are considered equal and remain their current order.
  5. + *
  6. if the two media types have different {@linkplain #getQualityValue() quality value}, then the media type + * with + * the highest quality value is ordered before the other.
  7. if the two media types have a different amount of + * {@linkplain #getParameter(String) parameters}, then the media type with the most parameters is ordered before the + * other.
  8. + *

For example: + *

audio/basic < audio/* < */*
audio/* < audio/*;q=0.7; + * audio/*;q=0.3
audio/basic;level=1 < audio/basic
audio/basic + * == text/html
audio/basic == audio/wave
+ * + * @param mediaTypes the list of media types to be sorted. + * + * @see HTTP 1.1: Semantics and Content, section + * 5.3.2 + */ + public static void sortBySpecificity(List mediaTypes) { + Assert.notNull(mediaTypes, "'mediaTypes' must not be null"); + if (mediaTypes.size() > 1) { + Collections.sort(mediaTypes, SPECIFICITY_COMPARATOR); + } + } + + /** + * Sorts the given list of {@code MediaType} objects by quality value. + * + *

Given two media types:

  1. if the two media types have different {@linkplain #getQualityValue() quality + * value}, then the media type with the highest quality value is ordered before the other.
  2. if either media + * type has a {@linkplain #isWildcardType() wildcard type}, then the media type without the wildcard is ordered + * before the other.
  3. if the two media types have different {@linkplain #getType() types}, then they are + * considered equal and remain their current order.
  4. + *
  5. if either media type has a {@linkplain #isWildcardSubtype() wildcard subtype}, then the media type without + * the wildcard is sorted before the other.
  6. + *
  7. if the two media types have different {@linkplain #getSubtype() subtypes}, then they are considered equal + * and remain their current order.
  8. if the two media types have a different amount of {@linkplain + * #getParameter(String) parameters}, then the media type with the most parameters is ordered before the + * other.
  9. + *
+ * + * @param mediaTypes the list of media types to be sorted. + */ + public static void sortByQualityValue(List mediaTypes) { + Assert.notNull(mediaTypes, "'mediaTypes' must not be null"); + if (mediaTypes.size() > 1) { + Collections.sort(mediaTypes, QUALITY_VALUE_COMPARATOR); + } + } + + /** + * Sorts the given list of {@code MediaType} objects by specificity as the primary criteria and quality value the + * secondary. + */ + public static void sortBySpecificityAndQuality(List mediaTypes) { + Assert.notNull(mediaTypes, "'mediaTypes' must not be null"); + if (mediaTypes.size() > 1) { + Collections.sort(mediaTypes, new CompoundComparator(MediaType.SPECIFICITY_COMPARATOR, + MediaType.QUALITY_VALUE_COMPARATOR)); + } + } + + /** + * Get the {@link MediaType} of the file. + * + * @param fileName file name. + * + * @return {@link MediaType}, cannot be null. + */ + public static MediaType getFileMediaType(String fileName) { + String extension = getUrlExtension(fileName); + if (!MimeTypeMap.getSingleton().hasExtension(extension)) { + return MediaType.APPLICATION_OCTET_STREAM; + } + + String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + try { + return MediaType.parseMediaType(mimeType); + } catch (Exception e) { + return MediaType.APPLICATION_OCTET_STREAM; + } + } + + /** + * Get he extension by url. + * + * @param url url. + * + * @return extension name. + */ + public static String getUrlExtension(String url) { + String extension = MimeTypeMap.getFileExtensionFromUrl(url); + return extension.isEmpty() ? "" : extension; + } + + /** + * Comparator used by {@link #sortByQualityValue(List)}. + */ + public static final Comparator QUALITY_VALUE_COMPARATOR = new Comparator() { + + @Override + public int compare(MediaType mediaType1, MediaType mediaType2) { + double quality1 = mediaType1.getQualityValue(); + double quality2 = mediaType2.getQualityValue(); + int qualityComparison = Double.compare(quality2, quality1); + if (qualityComparison != 0) { + return qualityComparison; // audio/*;q=0.7 < audio/*;q=0.3 + } else if (mediaType1.isWildcardType() && !mediaType2.isWildcardType()) { // */* < audio/* + return 1; + } else if (mediaType2.isWildcardType() && !mediaType1.isWildcardType()) { // audio/* > */* + return -1; + } else if (!mediaType1.getType().equals(mediaType2.getType())) { // audio/basic == text/html + return 0; + } else { // mediaType1.getType().equals(mediaType2.getType()) + if (mediaType1.isWildcardSubtype() && !mediaType2.isWildcardSubtype()) { // audio/* < audio/basic + return 1; + } else if (mediaType2.isWildcardSubtype() && !mediaType1.isWildcardSubtype()) { // audio/basic > audio/* + return -1; + } else if (!mediaType1.getSubtype().equals(mediaType2.getSubtype())) { // audio/basic == audio/wave + return 0; + } else { + int paramsSize1 = mediaType1.getParameters().size(); + int paramsSize2 = mediaType2.getParameters().size(); + // audio/basic;level=1 < audio/basic + return (paramsSize2 < paramsSize1 ? -1 : (paramsSize2 == paramsSize1 ? 0 : 1)); + } + } + } + }; + + + /** + * Comparator used by {@link #sortBySpecificity(List)}. + */ + public static final Comparator SPECIFICITY_COMPARATOR = new SpecificityComparator() { + + @Override + protected int compareParameters(MediaType mediaType1, MediaType mediaType2) { + double quality1 = mediaType1.getQualityValue(); + double quality2 = mediaType2.getQualityValue(); + int qualityComparison = Double.compare(quality2, quality1); + if (qualityComparison != 0) { + return qualityComparison; // audio/*;q=0.7 < audio/*;q=0.3 + } + return super.compareParameters(mediaType1, mediaType2); + } + }; + +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/MimeType.java b/api/src/main/java/com/yanzhenjie/andserver/util/MimeType.java new file mode 100644 index 0000000000000000000000000000000000000000..a92a328fa5c52bad95e1d00716b32c74a1096bd9 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/MimeType.java @@ -0,0 +1,636 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util; + +import com.yanzhenjie.andserver.error.InvalidMimeTypeException; + +import java.io.Serializable; +import java.nio.charset.Charset; +import java.nio.charset.UnsupportedCharsetException; +import java.util.*; + +/** + * Created by Zhenjie Yan on 2018/6/27. + */ +public class MimeType implements Comparable, Serializable { + + protected static final String WILDCARD_TYPE = "*"; + + private static final String PARAM_CHARSET = "charset"; + + private static final BitSet TOKEN; + + static { + // Variable names refer to RFC 2616, section 2.2. + BitSet ctl = new BitSet(128); + for (int i = 0; i <= 31; i++) { + ctl.set(i); + } + ctl.set(127); + + BitSet separators = new BitSet(128); + separators.set('('); + separators.set(')'); + separators.set('<'); + separators.set('>'); + separators.set('@'); + separators.set(','); + separators.set(';'); + separators.set(':'); + separators.set('\\'); + separators.set('\"'); + separators.set('/'); + separators.set('['); + separators.set(']'); + separators.set('?'); + separators.set('='); + separators.set('{'); + separators.set('}'); + separators.set(' '); + separators.set('\t'); + + TOKEN = new BitSet(128); + TOKEN.set(0, 128); + TOKEN.andNot(ctl); + TOKEN.andNot(separators); + } + + + private final String type; + + private final String subtype; + + private final Map parameters; + + /** + * Create a new {@code Mime} for the given primary type. + * + *

The {@linkplain #getSubtype() subtype} is set to {@code "*"}, and the parameters are empty. + * + * @param type the primary type. + * + * @throws IllegalArgumentException if any of the parameters contains illegal characters. + */ + public MimeType(String type) { + this(type, WILDCARD_TYPE); + } + + /** + * Create a new {@code Mime} for the given primary type and subtype. + * + *

The parameters are empty. + * + * @param type the primary type. + * @param subtype the subtype. + * + * @throws IllegalArgumentException if any of the parameters contains illegal characters + */ + public MimeType(String type, String subtype) { + this(type, subtype, Collections.emptyMap()); + } + + /** + * Create a new {@code Mime} for the given type, subtype, and character set. + * + * @param type the primary type. + * @param subtype the subtype. + * @param charset the character set. + * + * @throws IllegalArgumentException if any of the parameters contains illegal characters. + */ + public MimeType(String type, String subtype, Charset charset) { + this(type, subtype, Collections.singletonMap(PARAM_CHARSET, charset.name())); + } + + /** + * Copy-constructor that copies the type, subtype, parameters of the given {@code Mime}, and allows to set the + * specified character set. + * + * @param other the other media type. + * @param charset the character set. + * + * @throws IllegalArgumentException if any of the parameters contains illegal characters. + */ + public MimeType(MimeType other, Charset charset) { + this(other.getType(), other.getSubtype(), addCharsetParameter(charset, other.getParameters())); + } + + /** + * Copy-constructor that copies the type and subtype of the given {@code Mime}, and allows for different parameter. + * + * @param other the other media type. + * @param parameters the parameters, may be null. + * + * @throws IllegalArgumentException if any of the parameters contains illegal characters. + */ + public MimeType(MimeType other, Map parameters) { + this(other.getType(), other.getSubtype(), parameters); + } + + /** + * Create a new {@code Mime} for the given type, subtype, and parameters. + * + * @param type the primary type. + * @param subtype the subtype. + * @param parameters the parameters, may be null. + * + * @throws IllegalArgumentException if any of the parameters contains illegal characters. + */ + public MimeType(String type, String subtype, Map parameters) { + checkToken(type); + checkToken(subtype); + this.type = type.toLowerCase(Locale.ENGLISH); + this.subtype = subtype.toLowerCase(Locale.ENGLISH); + if (parameters != null && !parameters.isEmpty()) { + Map map = new LinkedCaseInsensitiveMap<>(parameters.size(), Locale.ENGLISH); + for (Map.Entry entry: parameters.entrySet()) { + String attribute = entry.getKey(); + String value = entry.getValue(); + checkParameters(attribute, value); + map.put(attribute, value); + } + this.parameters = Collections.unmodifiableMap(map); + } else { + this.parameters = Collections.emptyMap(); + } + } + + /** + * Checks the given token string for illegal characters, as defined in RFC 2616, section 2.2. + * + * @throws IllegalArgumentException in case of illegal characters. + * @see HTTP 1.1, section 2.2 + */ + private void checkToken(String token) { + for (int i = 0; i < token.length(); i++) { + char ch = token.charAt(i); + if (!TOKEN.get(ch)) { + throw new IllegalArgumentException("Invalid token character '" + ch + "' in token \"" + token + "\""); + } + } + } + + protected void checkParameters(String attribute, String value) { + Assert.hasLength(attribute, "'attribute' must not be empty."); + Assert.hasLength(value, "'value' must not be empty."); + checkToken(attribute); + if (PARAM_CHARSET.equals(attribute)) { + value = unquote(value); + Charset.forName(value); + } else if (!isQuotedString(value)) { + checkToken(value); + } + } + + private boolean isQuotedString(String s) { + if (s.length() < 2) { + return false; + } else { + return ((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'"))); + } + } + + protected String unquote(String s) { + if (s == null) { + return null; + } + return isQuotedString(s) ? s.substring(1, s.length() - 1) : s; + } + + /** + * Indicates whether the {@linkplain #getType() type} is the wildcard character * or not. + */ + public boolean isWildcardType() { + return WILDCARD_TYPE.equals(getType()); + } + + /** + * Indicates whether the {@linkplain #getSubtype() subtype} is the wildcard character * or the + * wildcard character followed by a suffix (e.g. *+xml). + * + * @return whether the subtype is a wildcard. + */ + public boolean isWildcardSubtype() { + return WILDCARD_TYPE.equals(getSubtype()) || getSubtype().startsWith("*+"); + } + + /** + * Indicates whether this media type is concrete, i.e. whether neither the type nor the subtype is a wildcard + * character + * *. + * + * @return whether this media type is concrete. + */ + public boolean isConcrete() { + return !isWildcardType() && !isWildcardSubtype(); + } + + /** + * Return the primary type. + */ + public String getType() { + return this.type; + } + + /** + * Return the subtype. + */ + public String getSubtype() { + return this.subtype; + } + + /** + * Return the character set, as indicated by a charset parameter, if any. + * + * @return the character set, or null if not available. + */ + public Charset getCharset() { + String charset = getParameter(PARAM_CHARSET); + return (charset != null ? Charset.forName(unquote(charset)) : null); + } + + /** + * Return a generic parameter value, given a parameter name. + * + * @param name the parameter name. + * + * @return the parameter value, or {@code null} if not present. + */ + public String getParameter(String name) { + return this.parameters.get(name); + } + + /** + * Return all generic parameter values. + * + * @return a read-only map, possibly empty, never null. + */ + public Map getParameters() { + return this.parameters; + } + + /** + * Indicate whether this {@code MediaType} includes the given media type. + * + *

For instance, {@code text/*} includes {@code text/plain} and {@code text/html}, and {@code application/*+xml} + * includes {@code application/soap+xml}, etc. This method is not symmetric. + * + * @param other the reference media type with which to compare. + * + * @return true if this media type includes the given media type, false otherwise. + */ + public boolean includes(MimeType other) { + if (other == null) { + return false; + } + if (this.isWildcardType()) { + // */* includes anything + return true; + } else if (getType().equals(other.getType())) { + if (getSubtype().equals(other.getSubtype())) { + return true; + } + if (this.isWildcardSubtype()) { + // wildcard with suffix, e.g. application/*+xml + int thisPlusIdx = getSubtype().indexOf('+'); + if (thisPlusIdx == -1) { + return true; + } else { + // application/*+xml includes application/soap+xml + int otherPlusIdx = other.getSubtype().indexOf('+'); + if (otherPlusIdx != -1) { + String thisSubtypeNoSuffix = getSubtype().substring(0, thisPlusIdx); + String thisSubtypeSuffix = getSubtype().substring(thisPlusIdx + 1); + String otherSubtypeSuffix = other.getSubtype().substring(otherPlusIdx + 1); + if (thisSubtypeSuffix.equals(otherSubtypeSuffix) && WILDCARD_TYPE.equals(thisSubtypeNoSuffix)) { + return true; + } + } + } + } + } + return false; + } + + /** + * Indicate whether this {@code MediaType} is compatible with the given media type. + * + *

For instance, {@code text/*} is compatible with {@code text/plain}, {@code text/html}, and vice versa. In + * effect, this method is similar to {@link #includes}, except that it is symmetric. + * + * @param other the reference media type with which to compare. + * + * @return true if this media type is compatible with the given media type, false otherwise. + */ + public boolean isCompatibleWith(MimeType other) { + if (other == null) { + return false; + } + if (isWildcardType() || other.isWildcardType()) { + return true; + } else if (getType().equals(other.getType())) { + if (getSubtype().equals(other.getSubtype())) { + return true; + } + // wildcard with suffix? e.g. application/*+xml + if (this.isWildcardSubtype() || other.isWildcardSubtype()) { + + int thisPlusIdx = getSubtype().indexOf('+'); + int otherPlusIdx = other.getSubtype().indexOf('+'); + + if (thisPlusIdx == -1 && otherPlusIdx == -1) { + return true; + } else if (thisPlusIdx != -1 && otherPlusIdx != -1) { + String thisSubtypeNoSuffix = getSubtype().substring(0, thisPlusIdx); + String otherSubtypeNoSuffix = other.getSubtype().substring(0, otherPlusIdx); + + String thisSubtypeSuffix = getSubtype().substring(thisPlusIdx + 1); + String otherSubtypeSuffix = other.getSubtype().substring(otherPlusIdx + 1); + + if (thisSubtypeSuffix.equals(otherSubtypeSuffix) && + (WILDCARD_TYPE.equals(thisSubtypeNoSuffix) || WILDCARD_TYPE.equals(otherSubtypeNoSuffix))) { + return true; + } + } + } + } + return false; + } + + + @Override + public boolean equals(Object other) { + if (!equalsExcludeParameter(other)) { + return false; + } + MimeType otherType = (MimeType) other; + return parametersAreEqual(otherType); + } + + public boolean equalsExcludeParameter(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof MimeType)) { + return false; + } + MimeType otherType = (MimeType) other; + return (this.type.equalsIgnoreCase(otherType.type) && this.subtype.equalsIgnoreCase(otherType.subtype)); + } + + /** + * Determine if the parameters in this {@code Mime} and the supplied {@code Mime} are equal, performing + * case-insensitive comparisons for charsets. + */ + private boolean parametersAreEqual(MimeType other) { + if (this.parameters.size() != other.parameters.size()) { + return false; + } + + for (String key: this.parameters.keySet()) { + if (!other.parameters.containsKey(key)) { + return false; + } + + if (PARAM_CHARSET.equals(key)) { + Charset mCharset = getCharset(); + Charset oCharset = other.getCharset(); + if (mCharset == null || !mCharset.equals(oCharset)) { + return false; + } else { + return true; + } + } + + String mValue = this.parameters.get(key); + String oValue = other.parameters.get(key); + if (mValue == null || !mValue.equals(oValue)) { + return false; + } + } + + return true; + } + + @Override + public int hashCode() { + int result = this.type.hashCode(); + result = 31 * result + this.subtype.hashCode(); + result = 31 * result + this.parameters.hashCode(); + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + appendTo(builder); + return builder.toString(); + } + + protected void appendTo(StringBuilder builder) { + builder.append(this.type); + builder.append('/'); + builder.append(this.subtype); + appendTo(this.parameters, builder); + } + + private void appendTo(Map map, StringBuilder builder) { + for (Map.Entry entry: map.entrySet()) { + builder.append(';'); + builder.append(entry.getKey()); + builder.append('='); + builder.append(entry.getValue()); + } + } + + /** + * Compares this {@code MediaType} to another alphabetically. + * + * @param other media type to compare to. + */ + @Override + public int compareTo(MimeType other) { + int comp = getType().compareToIgnoreCase(other.getType()); + if (comp != 0) { + return comp; + } + comp = getSubtype().compareToIgnoreCase(other.getSubtype()); + if (comp != 0) { + return comp; + } + comp = getParameters().size() - other.getParameters().size(); + if (comp != 0) { + return comp; + } + TreeSet thisAttributes = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + thisAttributes.addAll(getParameters().keySet()); + TreeSet otherAttributes = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + otherAttributes.addAll(other.getParameters().keySet()); + Iterator thisAttributesIterator = thisAttributes.iterator(); + Iterator otherAttributesIterator = otherAttributes.iterator(); + while (thisAttributesIterator.hasNext()) { + String thisAttribute = thisAttributesIterator.next(); + String otherAttribute = otherAttributesIterator.next(); + comp = thisAttribute.compareToIgnoreCase(otherAttribute); + if (comp != 0) { + return comp; + } + String thisValue = getParameters().get(thisAttribute); + String otherValue = other.getParameters().get(otherAttribute); + if (otherValue == null) { + otherValue = ""; + } + comp = thisValue.compareTo(otherValue); + if (comp != 0) { + return comp; + } + } + return 0; + } + + + /** + * Parse the given string value into a {@code Mime} object. + */ + public static MimeType valueOf(String mimeType) { + if (mimeType.isEmpty()) { + throw new InvalidMimeTypeException(mimeType, "[mimeType] must not be empty"); + } + + int index = mimeType.indexOf(';'); + String fullType = (index >= 0 ? mimeType.substring(0, index) : mimeType).trim(); + if (fullType.isEmpty()) { + throw new InvalidMimeTypeException(mimeType, "'contentType' must not be empty"); + } + + // java.net.HttpURLConnection returns a *; q=.2 Accept header + if (MimeType.WILDCARD_TYPE.equals(fullType)) { + fullType = "*/*"; + } + int subIndex = fullType.indexOf('/'); + if (subIndex == -1) { + throw new InvalidMimeTypeException(mimeType, "does not contain '/'"); + } + if (subIndex == fullType.length() - 1) { + throw new InvalidMimeTypeException(mimeType, "does not contain subtype after '/'"); + } + String type = fullType.substring(0, subIndex); + String subtype = fullType.substring(subIndex + 1, fullType.length()); + if (MimeType.WILDCARD_TYPE.equals(type) && !MimeType.WILDCARD_TYPE.equals(subtype)) { + throw new InvalidMimeTypeException(mimeType, "wildcard type is legal only in '*/*' (all mime " + "types)"); + } + + Map parameters = null; + do { + int nextIndex = index + 1; + boolean quoted = false; + while (nextIndex < mimeType.length()) { + char ch = mimeType.charAt(nextIndex); + if (ch == ';') { + if (!quoted) { + break; + } + } else if (ch == '"') { + quoted = !quoted; + } + nextIndex++; + } + String parameter = mimeType.substring(index + 1, nextIndex).trim(); + if (parameter.length() > 0) { + if (parameters == null) { + parameters = new LinkedHashMap<>(4); + } + int eqIndex = parameter.indexOf('='); + if (eqIndex >= 0) { + String attribute = parameter.substring(0, eqIndex); + String value = parameter.substring(eqIndex + 1, parameter.length()); + parameters.put(attribute, value); + } + } + index = nextIndex; + } + while (index < mimeType.length()); + + try { + return new MimeType(type, subtype, parameters); + } catch (UnsupportedCharsetException ex) { + throw new InvalidMimeTypeException(mimeType, "unsupported charset '" + ex.getCharsetName() + "'"); + } catch (IllegalArgumentException ex) { + throw new InvalidMimeTypeException(mimeType, ex.getMessage()); + } + } + + /** + * Return a string representation of the given list of {@code Mime} objects. + * + * @param mimeTypes the string to parse. + * + * @return the list of mime types. + * + * @throws IllegalArgumentException if the String cannot be parsed. + */ + public static String toString(Collection mimeTypes) { + StringBuilder builder = new StringBuilder(); + for (Iterator iterator = mimeTypes.iterator(); iterator.hasNext(); ) { + MimeType mimeType = iterator.next(); + mimeType.appendTo(builder); + if (iterator.hasNext()) { + builder.append(", "); + } + } + return builder.toString(); + } + + private static Map addCharsetParameter(Charset charset, Map parameters) { + Map map = new LinkedHashMap<>(parameters); + map.put(PARAM_CHARSET, charset.name()); + return map; + } + + public static class SpecificityComparator implements Comparator { + + @Override + public int compare(T mimeType1, T mimeType2) { + if (mimeType1.isWildcardType() && !mimeType2.isWildcardType()) { // */* < audio/* + return 1; + } else if (mimeType2.isWildcardType() && !mimeType1.isWildcardType()) { // audio/* > */* + return -1; + } else if (!mimeType1.getType().equals(mimeType2.getType())) { // audio/basic == text/html + return 0; + } else { + // mediaType1.getType().equals(mediaType2.getType()) + if (mimeType1.isWildcardSubtype() && !mimeType2.isWildcardSubtype()) { // audio/* < audio/basic + return 1; + } else if (mimeType2.isWildcardSubtype() && !mimeType1.isWildcardSubtype()) { // audio/basic > audio/* + return -1; + } else if (!mimeType1.getSubtype().equals(mimeType2.getSubtype())) { // audio/basic == audio/wave + return 0; + } else { // mediaType2.getSubtype().equals(mediaType2.getSubtype()) + return compareParameters(mimeType1, mimeType2); + } + } + } + + @SuppressWarnings("UseCompareMethod") + protected int compareParameters(T mimeType1, T mimeType2) { + int paramsSize1 = mimeType1.getParameters().size(); + int paramsSize2 = mimeType2.getParameters().size(); + // audio/basic;level=1 < audio/basic + return (paramsSize2 < paramsSize1 ? -1 : (paramsSize2 == paramsSize1 ? 0 : 1)); + } + } + +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/MimeTypeMap.java b/api/src/main/java/com/yanzhenjie/andserver/util/MimeTypeMap.java new file mode 100644 index 0000000000000000000000000000000000000000..5323b4ab20aabc8bfd21523316783ae3b92eeee7 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/MimeTypeMap.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yanzhenjie.andserver.util; + +import java.util.regex.Pattern; + +public class MimeTypeMap { + + private static final MimeTypeMap sMimeTypeMap = new MimeTypeMap(); + + private MimeTypeMap() { + } + + /** + * Returns the file extension or an empty string if there is no + * extension. This method is a convenience method for obtaining the + * extension of a url and has undefined results for other Strings. + * @param url + * @return The file extension of the given url. + */ + public static String getFileExtensionFromUrl(String url) { + if (!url.isEmpty()) { + int fragment = url.lastIndexOf('#'); + if (fragment > 0) { + url = url.substring(0, fragment); + } + + int query = url.lastIndexOf('?'); + if (query > 0) { + url = url.substring(0, query); + } + + int filenamePos = url.lastIndexOf('/'); + String filename = + 0 <= filenamePos ? url.substring(filenamePos + 1) : url; + + // if the filename contains special characters, we don't + // consider it valid for our matching purposes: + if (!filename.isEmpty() && + Pattern.matches("[a-zA-Z_0-9\\.\\-\\(\\)\\%]+", filename)) { + int dotPos = filename.lastIndexOf('.'); + if (0 <= dotPos) { + return filename.substring(dotPos + 1); + } + } + } + + return ""; + } + + /** + * Return the registered extension for the given MIME type. Note that some + * MIME types map to multiple extensions. This call will return the most + * common extension for the given MIME type. + * @param mimeType A MIME type (i.e. text/plain) + * @return The extension for the given MIME type or {@code null} if there is none. + */ + public String getExtensionFromMimeType(String mimeType) { + return MimeUtils.guessExtensionFromMimeType(mimeType); + } + + + /** + * Return {@code true} if the given extension has a registered MIME type. + * @param extension A file extension without the leading '.' + * @return {@code true} if there is an extension entry in the map. + */ + public boolean hasExtension(String extension) { + return MimeUtils.hasExtension(extension); + } + + /** + * Return the MIME type for the given extension. + * @param extension A file extension without the leading '.' + * @return The MIME type for the given extension or {@code null} if there is none. + */ + public String getMimeTypeFromExtension(String extension) { + return MimeUtils.guessMimeTypeFromExtension(extension); + } + + /** + * Get the singleton instance of MimeTypeMap. + * @return The singleton instance of the MIME-type map. + */ + public static MimeTypeMap getSingleton() { + return sMimeTypeMap; + } +} diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/MimeUtils.java b/api/src/main/java/com/yanzhenjie/andserver/util/MimeUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8ac01a77888692bccae8ba1583da34917aff63d3 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/MimeUtils.java @@ -0,0 +1,496 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yanzhenjie.andserver.util; + + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +public class MimeUtils { + private static final Map mimeTypeToExtensionMap = new HashMap(); + + + private static final Map extensionToMimeTypeMap = new HashMap(); + + + static { + // The following table is based on /etc/mime.types data minus + // chemical/* MIME types and MIME types that don't map to any + // file extensions. We also exclude top-level domain names to + // deal with cases like: + // + // mail.google.com/a/google.com + // + // and "active" MIME types (due to potential security issues). + + + add("application/andrew-inset", "ez"); + add("application/dsptype", "tsp"); + add("application/futuresplash", "spl"); + add("application/hta", "hta"); + add("application/mac-binhex40", "hqx"); + add("application/mac-compactpro", "cpt"); + add("application/mathematica", "nb"); + add("application/msaccess", "mdb"); + add("application/oda", "oda"); + add("application/ogg", "ogg"); + add("application/pdf", "pdf"); + add("application/pgp-keys", "key"); + add("application/pgp-signature", "pgp"); + add("application/pics-rules", "prf"); + add("application/rar", "rar"); + add("application/rdf+xml", "rdf"); + add("application/rss+xml", "rss"); + add("application/zip", "zip"); + add("application/vnd.android.package-archive", "apk"); + add("application/vnd.cinderella", "cdy"); + add("application/vnd.ms-pki.stl", "stl"); + add("application/vnd.oasis.opendocument.database", "odb"); + add("application/vnd.oasis.opendocument.formula", "odf"); + add("application/vnd.oasis.opendocument.graphics", "odg"); + add("application/vnd.oasis.opendocument.graphics-template", "otg"); + add("application/vnd.oasis.opendocument.image", "odi"); + add("application/vnd.oasis.opendocument.spreadsheet", "ods"); + add("application/vnd.oasis.opendocument.spreadsheet-template", "ots"); + add("application/vnd.oasis.opendocument.text", "odt"); + add("application/vnd.oasis.opendocument.text-master", "odm"); + add("application/vnd.oasis.opendocument.text-template", "ott"); + add("application/vnd.oasis.opendocument.text-web", "oth"); + add("application/vnd.google-earth.kml+xml", "kml"); + add("application/vnd.google-earth.kmz", "kmz"); + add("application/msword", "doc"); + add("application/msword", "dot"); + add("application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx"); + add("application/vnd.openxmlformats-officedocument.wordprocessingml.template", "dotx"); + add("application/vnd.ms-excel", "xls"); + add("application/vnd.ms-excel", "xlt"); + add("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx"); + add("application/vnd.openxmlformats-officedocument.spreadsheetml.template", "xltx"); + add("application/vnd.ms-powerpoint", "ppt"); + add("application/vnd.ms-powerpoint", "pot"); + add("application/vnd.ms-powerpoint", "pps"); + add("application/vnd.openxmlformats-officedocument.presentationml.presentation", "pptx"); + add("application/vnd.openxmlformats-officedocument.presentationml.template", "potx"); + add("application/vnd.openxmlformats-officedocument.presentationml.slideshow", "ppsx"); + add("application/vnd.rim.cod", "cod"); + add("application/vnd.smaf", "mmf"); + add("application/vnd.stardivision.calc", "sdc"); + add("application/vnd.stardivision.draw", "sda"); + add("application/vnd.stardivision.impress", "sdd"); + add("application/vnd.stardivision.impress", "sdp"); + add("application/vnd.stardivision.math", "smf"); + add("application/vnd.stardivision.writer", "sdw"); + add("application/vnd.stardivision.writer", "vor"); + add("application/vnd.stardivision.writer-global", "sgl"); + add("application/vnd.sun.xml.calc", "sxc"); + add("application/vnd.sun.xml.calc.template", "stc"); + add("application/vnd.sun.xml.draw", "sxd"); + add("application/vnd.sun.xml.draw.template", "std"); + add("application/vnd.sun.xml.impress", "sxi"); + add("application/vnd.sun.xml.impress.template", "sti"); + add("application/vnd.sun.xml.math", "sxm"); + add("application/vnd.sun.xml.writer", "sxw"); + add("application/vnd.sun.xml.writer.global", "sxg"); + add("application/vnd.sun.xml.writer.template", "stw"); + add("application/vnd.visio", "vsd"); + add("application/x-abiword", "abw"); + add("application/x-apple-diskimage", "dmg"); + add("application/x-bcpio", "bcpio"); + add("application/x-bittorrent", "torrent"); + add("application/x-cdf", "cdf"); + add("application/x-cdlink", "vcd"); + add("application/x-chess-pgn", "pgn"); + add("application/x-cpio", "cpio"); + add("application/x-debian-package", "deb"); + add("application/x-debian-package", "udeb"); + add("application/x-director", "dcr"); + add("application/x-director", "dir"); + add("application/x-director", "dxr"); + add("application/x-dms", "dms"); + add("application/x-doom", "wad"); + add("application/x-dvi", "dvi"); + add("application/x-flac", "flac"); + add("application/x-font", "pfa"); + add("application/x-font", "pfb"); + add("application/x-font", "gsf"); + add("application/x-font", "pcf"); + add("application/x-font", "pcf.Z"); + add("application/x-freemind", "mm"); + add("application/x-futuresplash", "spl"); + add("application/x-gnumeric", "gnumeric"); + add("application/x-go-sgf", "sgf"); + add("application/x-graphing-calculator", "gcf"); + add("application/x-gtar", "gtar"); + add("application/x-gtar", "tgz"); + add("application/x-gtar", "taz"); + add("application/x-hdf", "hdf"); + add("application/x-ica", "ica"); + add("application/x-internet-signup", "ins"); + add("application/x-internet-signup", "isp"); + add("application/x-iphone", "iii"); + add("application/x-iso9660-image", "iso"); + add("application/x-jmol", "jmz"); + add("application/x-kchart", "chrt"); + add("application/x-killustrator", "kil"); + add("application/x-koan", "skp"); + add("application/x-koan", "skd"); + add("application/x-koan", "skt"); + add("application/x-koan", "skm"); + add("application/x-kpresenter", "kpr"); + add("application/x-kpresenter", "kpt"); + add("application/x-kspread", "ksp"); + add("application/x-kword", "kwd"); + add("application/x-kword", "kwt"); + add("application/x-latex", "latex"); + add("application/x-lha", "lha"); + add("application/x-lzh", "lzh"); + add("application/x-lzx", "lzx"); + add("application/x-maker", "frm"); + add("application/x-maker", "maker"); + add("application/x-maker", "frame"); + add("application/x-maker", "fb"); + add("application/x-maker", "book"); + add("application/x-maker", "fbdoc"); + add("application/x-mif", "mif"); + add("application/x-ms-wmd", "wmd"); + add("application/x-ms-wmz", "wmz"); + add("application/x-msi", "msi"); + add("application/x-ns-proxy-autoconfig", "pac"); + add("application/x-nwc", "nwc"); + add("application/x-object", "o"); + add("application/x-oz-application", "oza"); + add("application/x-pkcs12", "p12"); + add("application/x-pkcs7-certreqresp", "p7r"); + add("application/x-pkcs7-crl", "crl"); + add("application/x-quicktimeplayer", "qtl"); + add("application/x-shar", "shar"); + add("application/x-shockwave-flash", "swf"); + add("application/x-stuffit", "sit"); + add("application/x-sv4cpio", "sv4cpio"); + add("application/x-sv4crc", "sv4crc"); + add("application/x-tar", "tar"); + add("application/x-texinfo", "texinfo"); + add("application/x-texinfo", "texi"); + add("application/x-troff", "t"); + add("application/x-troff", "roff"); + add("application/x-troff-man", "man"); + add("application/x-ustar", "ustar"); + add("application/x-wais-source", "src"); + add("application/x-wingz", "wz"); + add("application/x-webarchive", "webarchive"); + add("application/x-webarchive-xml", "webarchivexml"); + add("application/x-x509-ca-cert", "crt"); + add("application/x-x509-user-cert", "crt"); + add("application/x-xcf", "xcf"); + add("application/x-xfig", "fig"); + add("application/xhtml+xml", "xhtml"); + add("audio/3gpp", "3gpp"); + add("audio/amr", "amr"); + add("audio/basic", "snd"); + add("audio/midi", "mid"); + add("audio/midi", "midi"); + add("audio/midi", "kar"); + add("audio/midi", "xmf"); + add("audio/mobile-xmf", "mxmf"); + add("audio/mpeg", "mpga"); + add("audio/mpeg", "mpega"); + add("audio/mpeg", "mp2"); + add("audio/mpeg", "mp3"); + add("audio/mpeg", "m4a"); + add("audio/mpegurl", "m3u"); + add("audio/prs.sid", "sid"); + add("audio/x-aiff", "aif"); + add("audio/x-aiff", "aiff"); + add("audio/x-aiff", "aifc"); + add("audio/x-gsm", "gsm"); + add("audio/x-mpegurl", "m3u"); + add("audio/x-ms-wma", "wma"); + add("audio/x-ms-wax", "wax"); + add("audio/x-pn-realaudio", "ra"); + add("audio/x-pn-realaudio", "rm"); + add("audio/x-pn-realaudio", "ram"); + add("audio/x-realaudio", "ra"); + add("audio/x-scpls", "pls"); + add("audio/x-sd2", "sd2"); + add("audio/x-wav", "wav"); + add("image/bmp", "bmp"); + add("audio/x-qcp", "qcp"); + add("image/gif", "gif"); + add("image/ico", "cur"); + add("image/ico", "ico"); + add("image/ief", "ief"); + add("image/jpeg", "jpeg"); + add("image/jpeg", "jpg"); + add("image/jpeg", "jpe"); + add("image/pcx", "pcx"); + add("image/png", "png"); + add("image/svg+xml", "svg"); + add("image/svg+xml", "svgz"); + add("image/tiff", "tiff"); + add("image/tiff", "tif"); + add("image/vnd.djvu", "djvu"); + add("image/vnd.djvu", "djv"); + add("image/vnd.wap.wbmp", "wbmp"); + add("image/x-cmu-raster", "ras"); + add("image/x-coreldraw", "cdr"); + add("image/x-coreldrawpattern", "pat"); + add("image/x-coreldrawtemplate", "cdt"); + add("image/x-corelphotopaint", "cpt"); + add("image/x-icon", "ico"); + add("image/x-jg", "art"); + add("image/x-jng", "jng"); + add("image/x-ms-bmp", "bmp"); + add("image/x-photoshop", "psd"); + add("image/x-portable-anymap", "pnm"); + add("image/x-portable-bitmap", "pbm"); + add("image/x-portable-graymap", "pgm"); + add("image/x-portable-pixmap", "ppm"); + add("image/x-rgb", "rgb"); + add("image/x-xbitmap", "xbm"); + add("image/x-xpixmap", "xpm"); + add("image/x-xwindowdump", "xwd"); + add("model/iges", "igs"); + add("model/iges", "iges"); + add("model/mesh", "msh"); + add("model/mesh", "mesh"); + add("model/mesh", "silo"); + add("text/calendar", "ics"); + add("text/calendar", "icz"); + add("text/comma-separated-values", "csv"); + add("text/css", "css"); + add("text/html", "htm"); + add("text/html", "html"); + add("text/h323", "323"); + add("text/iuls", "uls"); + add("text/mathml", "mml"); + // add ".txt" first so it will be the default for ExtensionFromMimeType + add("text/plain", "txt"); + add("text/plain", "asc"); + add("text/plain", "text"); + add("text/plain", "diff"); + add("text/plain", "po"); // reserve "pot" for vnd.ms-powerpoint + add("text/richtext", "rtx"); + add("text/rtf", "rtf"); + add("text/texmacs", "ts"); + add("text/text", "phps"); + add("text/tab-separated-values", "tsv"); + add("text/xml", "xml"); + add("text/x-bibtex", "bib"); + add("text/x-boo", "boo"); + add("text/x-c++hdr", "h++"); + add("text/x-c++hdr", "hpp"); + add("text/x-c++hdr", "hxx"); + add("text/x-c++hdr", "hh"); + add("text/x-c++src", "c++"); + add("text/x-c++src", "cpp"); + add("text/x-c++src", "cxx"); + add("text/x-chdr", "h"); + add("text/x-component", "htc"); + add("text/x-csh", "csh"); + add("text/x-csrc", "c"); + add("text/x-dsrc", "d"); + add("text/x-haskell", "hs"); + add("text/x-java", "java"); + add("text/x-literate-haskell", "lhs"); + add("text/x-moc", "moc"); + add("text/x-pascal", "p"); + add("text/x-pascal", "pas"); + add("text/x-pcs-gcd", "gcd"); + add("text/x-setext", "etx"); + add("text/x-tcl", "tcl"); + add("text/x-tex", "tex"); + add("text/x-tex", "ltx"); + add("text/x-tex", "sty"); + add("text/x-tex", "cls"); + add("text/x-vcalendar", "vcs"); + add("text/x-vcard", "vcf"); + add("video/3gpp", "3gpp"); + add("video/3gpp", "3gp"); + add("video/3gpp", "3g2"); + add("video/dl", "dl"); + add("video/dv", "dif"); + add("video/dv", "dv"); + add("video/fli", "fli"); + add("video/m4v", "m4v"); + add("video/mpeg", "mpeg"); + add("video/mpeg", "mpg"); + add("video/mpeg", "mpe"); + add("video/mp4", "mp4"); + add("video/mpeg", "VOB"); + add("video/quicktime", "qt"); + add("video/quicktime", "mov"); + add("video/vnd.mpegurl", "mxu"); + add("video/webm", "webm"); + add("video/x-la-asf", "lsf"); + add("video/x-la-asf", "lsx"); + add("video/x-mng", "mng"); + add("video/x-ms-asf", "asf"); + add("video/x-ms-asf", "asx"); + add("video/x-ms-wm", "wm"); + add("video/x-ms-wmv", "wmv"); + add("video/x-ms-wmx", "wmx"); + add("video/x-ms-wvx", "wvx"); + add("video/x-msvideo", "avi"); + add("video/x-sgi-movie", "movie"); + add("x-conference/x-cooltalk", "ice"); + add("x-epoc/x-sisx-app", "sisx"); + applyOverrides(); + } + + + private static void add(String mimeType, String extension) { + // + // if we have an existing x --> y mapping, we do not want to + // override it with another mapping x --> ? + // this is mostly because of the way the mime-type map below + // is constructed (if a mime type maps to several extensions + // the first extension is considered the most popular and is + // added first; we do not want to overwrite it later). + // + if (!mimeTypeToExtensionMap.containsKey(mimeType)) { + mimeTypeToExtensionMap.put(mimeType, extension); + } + extensionToMimeTypeMap.put(extension, mimeType); + } + + + private static InputStream getContentTypesPropertiesStream() { + // User override? + String userTable = System.getProperty("content.types.user.table"); + if (userTable != null) { + File f = new File(userTable); + if (f.exists()) { + try { + return new FileInputStream(f); + } catch (IOException ignored) { + } + } + } + + + // Standard location? + File f = new File(System.getProperty("java.home"), "lib" + File.separator + "content-types.properties"); + if (f.exists()) { + try { + return new FileInputStream(f); + } catch (IOException ignored) { + } + } + + + return null; + } + + + /** + * This isn't what the RI does. The RI doesn't have hard-coded defaults, so supplying your + * own "content.types.user.table" means you don't get any of the built-ins, and the built-ins + * come from "$JAVA_HOME/lib/content-types.properties". + */ + private static void applyOverrides() { + // Get the appropriate InputStream to read overrides from, if any. + InputStream stream = getContentTypesPropertiesStream(); + if (stream == null) { + return; + } + + + try { + try { + // Read the properties file... + Properties overrides = new Properties(); + overrides.load(stream); + // And translate its mapping to ours... + for (Map.Entry entry : overrides.entrySet()) { + String extension = (String) entry.getKey(); + String mimeType = (String) entry.getValue(); + add(mimeType, extension); + } + } finally { + stream.close(); + } + } catch (IOException ignored) { + } + } + + + private MimeUtils() { + } + + + + + + /** + * Returns the MIME type for the given extension. + * @param extension A file extension without the leading '.' + * @return The MIME type for the given extension or null iff there is none. + */ + public static String guessMimeTypeFromExtension(String extension) { + if (extension == null || extension.isEmpty()) { + return null; + } + return extensionToMimeTypeMap.get(extension); + } + + + //下面3个方法根本没有被调用过,因此,我猜测这个类很可能是从其它项目copy过来的。 + //也有可能是因为本项目是经过裁剪的,毕竟是开源社区版 + /** + * Returns true if the given extension has a registered MIME type. + * @param extension A file extension without the leading '.' + * @return True iff there is an extension entry in the map. + */ + public static boolean hasExtension(String extension) { + if (extension == null || extension.isEmpty()) { + return false; + } + return extensionToMimeTypeMap.containsKey(extension); + } + + + /** + * Returns the registered extension for the given MIME type. Note that some + * MIME types map to multiple extensions. This call will return the most + * common extension for the given MIME type. + * @param mimeType A MIME type (i.e. text/plain) + * @return The extension for the given MIME type or null iff there is none. + */ + public static String guessExtensionFromMimeType(String mimeType) { + if (mimeType == null || mimeType.isEmpty()) { + return null; + } + return mimeTypeToExtensionMap.get(mimeType); + } + + /** + * Returns true if the given MIME type has an entry in the map. + * @param mimeType A MIME type (i.e. text/plain) + * @return True iff there is a mimeType entry in the map. + */ + public static boolean hasMimeType(String mimeType) { + if (mimeType == null || mimeType.isEmpty()) { + return false; + } + return mimeTypeToExtensionMap.containsKey(mimeType); + } +} diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/MultiValueMap.java b/api/src/main/java/com/yanzhenjie/andserver/util/MultiValueMap.java new file mode 100644 index 0000000000000000000000000000000000000000..3dec8b0feee2e86c144d9e4f10d583e1a61449a4 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/MultiValueMap.java @@ -0,0 +1,66 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util; + + +import java.util.List; +import java.util.Map; + +/** + * Created by Zhenjie Yan on 2018/6/21. + */ +public interface MultiValueMap extends Map> { + + /** + * Return the first value for the given key. + * + * @param key the key. + * + * @return the first value for the specified key, or null. + */ + V getFirst(K key); + + /** + * Add the given single value to the current list of values for the given key. + * + * @param key the key. + * @param value the value to be added. + */ + void add(K key, V value); + + /** + * Set the given single value under the given key. + * + * @param key the key. + * @param value the value to set. + */ + void set(K key, V value); + + /** + * Set the given values under. + * + * @param values the values. + */ + void setAll(Map values); + + /** + * Returns the first values contained in this {@code MultiValueMap}. + * + * @return a single value representation of this map. + */ + Map toSingleValueMap(); + +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/ObjectUtils.java b/api/src/main/java/com/yanzhenjie/andserver/util/ObjectUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..4fccd5048d8cbc00cd024785c4cdfb1752e5376d --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/ObjectUtils.java @@ -0,0 +1,957 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util; + +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; + +/** + * Created by Zhenjie Yan on 2018/7/5. + * + * @deprecated use apache commons-lang instead. + */ +@Deprecated +public abstract class ObjectUtils { + + private static final int INITIAL_HASH = 7; + private static final int MULTIPLIER = 31; + + private static final String EMPTY_STRING = ""; + private static final String NULL_STRING = "null"; + private static final String ARRAY_START = "{"; + private static final String ARRAY_END = "}"; + private static final String EMPTY_ARRAY = ARRAY_START + ARRAY_END; + private static final String ARRAY_ELEMENT_SEPARATOR = ", "; + + + /** + * Return whether the given throwable is a checked exception: that is, neither a RuntimeException nor an Error. + * + * @param ex the throwable to check. + * + * @return whether the throwable is a checked exception. + */ + public static boolean isCheckedException(Throwable ex) { + return !(ex instanceof RuntimeException || ex instanceof Error); + } + + /** + * Check whether the given exception is compatible with the specified exception types, as declared in a throws + * clause. + * + * @param ex the exception to check. + * @param declaredExceptions the exception types declared in the throws clause. + * + * @return whether the given exception is compatible. + */ + public static boolean isCompatibleWithThrowsClause(Throwable ex, Class... declaredExceptions) { + if (!isCheckedException(ex)) { + return true; + } + if (declaredExceptions != null) { + for (Class declaredException: declaredExceptions) { + if (declaredException.isInstance(ex)) { + return true; + } + } + } + return false; + } + + /** + * Determine whether the given object is an array: either an Object array or a primitive array. + * + * @param obj the object to check. + */ + public static boolean isArray(Object obj) { + return (obj != null && obj.getClass().isArray()); + } + + /** + * Determine whether the given array is empty: i.e. {@code null} or of zero length. + * + * @param array the array to check. + */ + public static boolean isEmpty(Object[] array) { + return (array == null || array.length == 0); + } + + /** + * Determine whether the given object is empty.

This method supports the following object types.

  • {@code + * Array}: considered empty if its length is zero
  • {@link CharSequence}: considered empty if its length is + * zero
  • {@link Collection}: delegates to {@link Collection#isEmpty()}
  • {@link Map}: delegates to + * {@link Map#isEmpty()}

If the given object is non-null and not one of the aforementioned supported + * types, this method returns {@code false}. + * + * @param obj the object to check. + * + * @return {@code true} if the object is {@code null} or empty. + */ + @SuppressWarnings("rawtypes") + public static boolean isEmpty(Object obj) { + if (obj == null) { + return true; + } + + if (obj instanceof CharSequence) { + return ((CharSequence) obj).length() == 0; + } + if (obj.getClass().isArray()) { + return Array.getLength(obj) == 0; + } + if (obj instanceof Collection) { + return ((Collection) obj).isEmpty(); + } + if (obj instanceof Map) { + return ((Map) obj).isEmpty(); + } + return false; + } + + /** + * Check whether the given array contains the given element. + * + * @param array the array to check (may be {@code null}, in which case the return value will always be {@code + * false}). + * @param element the element to check for. + * + * @return whether the element has been found in the given array. + */ + public static boolean containsElement(Object[] array, Object element) { + if (array == null) { + return false; + } + for (Object arrayEle: array) { + if (nullSafeEquals(arrayEle, element)) { + return true; + } + } + return false; + } + + /** + * Check whether the given array of enum constants contains a constant with the given name, ignoring case when + * determining a match. + * + * @param enumValues the enum values to check, typically the product of a call to MyEnum.values(). + * @param constant the constant name to find (must not be null or empty string). + * + * @return whether the constant has been found in the given array. + */ + public static boolean containsConstant(Enum[] enumValues, String constant) { + return containsConstant(enumValues, constant, false); + } + + /** + * Check whether the given array of enum constants contains a constant with the given name. + * + * @param enumValues the enum values to check, typically the product of a call to MyEnum.values(). + * @param constant the constant name to find (must not be null or empty string). + * @param caseSensitive whether case is significant in determining a match. + * + * @return whether the constant has been found in the given array. + */ + public static boolean containsConstant(Enum[] enumValues, String constant, boolean caseSensitive) { + for (Enum candidate: enumValues) { + if (caseSensitive + ? candidate.toString().equals(constant) + : candidate.toString().equalsIgnoreCase(constant)) { + return true; + } + } + return false; + } + + /** + * Case insensitive alternative to {@link Enum#valueOf(Class, String)}. + * + * @param the concrete Enum type. + * @param enumValues the array of all Enum constants in question, usually per Enum.values(). + * @param constant the constant to get the enum value of. + * + * @throws IllegalArgumentException if the given constant is not found in the given array of enum values. Use + * {@link #containsConstant(Enum[], String)} as a guard to avoid this exception. + */ + public static > E caseInsensitiveValueOf(E[] enumValues, String constant) { + for (E candidate: enumValues) { + if (candidate.toString().equalsIgnoreCase(constant)) { + return candidate; + } + } + String message = String.format("constant [%s] does not exist in enum type %s", constant, + enumValues.getClass().getComponentType().getName()); + throw new IllegalArgumentException(message); + } + + /** + * Append the given object to the given array, returning a new array consisting of the input array contents plus the + * given object. + * + * @param array the array to append to (can be {@code null}). + * @param obj the object to append. + * + * @return the new array (of the same component type; never {@code null}). + */ + public static A[] addObjectToArray(A[] array, O obj) { + Class compType = Object.class; + if (array != null) { + compType = array.getClass().getComponentType(); + } else if (obj != null) { + compType = obj.getClass(); + } + int newArrLength = (array != null ? array.length + 1 : 1); + @SuppressWarnings("unchecked") A[] newArr = (A[]) Array.newInstance(compType, newArrLength); + if (array != null) { + System.arraycopy(array, 0, newArr, 0, array.length); + } + newArr[newArr.length - 1] = obj; + return newArr; + } + + /** + * Convert the given array (which may be a primitive array) to an object array (if necessary of primitive wrapper + * objects).

A {@code null} source value will be converted to an empty Object array. + * + * @param source the (potentially primitive) array. + * + * @return the corresponding object array (never {@code null}). + * + * @throws IllegalArgumentException if the parameter is not an array. + */ + public static Object[] toObjectArray(Object source) { + if (source instanceof Object[]) { + return (Object[]) source; + } + if (source == null) { + return new Object[0]; + } + if (!source.getClass().isArray()) { + throw new IllegalArgumentException("Source is not an array: " + source); + } + int length = Array.getLength(source); + if (length == 0) { + return new Object[0]; + } + Class wrapperType = Array.get(source, 0).getClass(); + Object[] newArray = (Object[]) Array.newInstance(wrapperType, length); + for (int i = 0; i < length; i++) { + newArray[i] = Array.get(source, i); + } + return newArray; + } + + + //--------------------------------------------------------------------- + // Convenience methods for content-based equality/hash-code handling + //--------------------------------------------------------------------- + + /** + * Determine if the given objects are equal, returning {@code true} if both are {@code null} or {@code false} if + * only one is {@code null}.

Compares arrays with {@code Arrays.equals}, performing an equality check based on + * the array elements rather than the array reference. + * + * @param o1 first Object to compare. + * @param o2 second Object to compare. + * + * @return whether the given objects are equal. + * + * @see Object#equals(Object) + * @see Arrays#equals + */ + public static boolean nullSafeEquals(Object o1, Object o2) { + if (o1 == o2) { + return true; + } + if (o1 == null || o2 == null) { + return false; + } + if (o1.equals(o2)) { + return true; + } + if (o1.getClass().isArray() && o2.getClass().isArray()) { + return arrayEquals(o1, o2); + } + return false; + } + + /** + * Compare the given arrays with {@code Arrays.equals}, performing an equality check based on the array elements + * rather than the array reference. + * + * @param o1 first array to compare. + * @param o2 second array to compare. + * + * @return whether the given objects are equal. + * + * @see #nullSafeEquals(Object, Object) + * @see Arrays#equals + */ + private static boolean arrayEquals(Object o1, Object o2) { + if (o1 instanceof Object[] && o2 instanceof Object[]) { + return Arrays.equals((Object[]) o1, (Object[]) o2); + } + if (o1 instanceof boolean[] && o2 instanceof boolean[]) { + return Arrays.equals((boolean[]) o1, (boolean[]) o2); + } + if (o1 instanceof byte[] && o2 instanceof byte[]) { + return Arrays.equals((byte[]) o1, (byte[]) o2); + } + if (o1 instanceof char[] && o2 instanceof char[]) { + return Arrays.equals((char[]) o1, (char[]) o2); + } + if (o1 instanceof double[] && o2 instanceof double[]) { + return Arrays.equals((double[]) o1, (double[]) o2); + } + if (o1 instanceof float[] && o2 instanceof float[]) { + return Arrays.equals((float[]) o1, (float[]) o2); + } + if (o1 instanceof int[] && o2 instanceof int[]) { + return Arrays.equals((int[]) o1, (int[]) o2); + } + if (o1 instanceof long[] && o2 instanceof long[]) { + return Arrays.equals((long[]) o1, (long[]) o2); + } + if (o1 instanceof short[] && o2 instanceof short[]) { + return Arrays.equals((short[]) o1, (short[]) o2); + } + return false; + } + + /** + * Return as hash code for the given object; typically the value of {@code Object#hashCode()}}. If the object is an + * array, this method will delegate to any of the {@code nullSafeHashCode} methods for arrays in this class. If the + * object is {@code null}, this method returns 0. + * + * @see Object#hashCode() + * @see #nullSafeHashCode(Object[]) + * @see #nullSafeHashCode(boolean[]) + * @see #nullSafeHashCode(byte[]) + * @see #nullSafeHashCode(char[]) + * @see #nullSafeHashCode(double[]) + * @see #nullSafeHashCode(float[]) + * @see #nullSafeHashCode(int[]) + * @see #nullSafeHashCode(long[]) + * @see #nullSafeHashCode(short[]) + */ + public static int nullSafeHashCode(Object obj) { + if (obj == null) { + return 0; + } + if (obj.getClass().isArray()) { + if (obj instanceof Object[]) { + return nullSafeHashCode((Object[]) obj); + } + if (obj instanceof boolean[]) { + return nullSafeHashCode((boolean[]) obj); + } + if (obj instanceof byte[]) { + return nullSafeHashCode((byte[]) obj); + } + if (obj instanceof char[]) { + return nullSafeHashCode((char[]) obj); + } + if (obj instanceof double[]) { + return nullSafeHashCode((double[]) obj); + } + if (obj instanceof float[]) { + return nullSafeHashCode((float[]) obj); + } + if (obj instanceof int[]) { + return nullSafeHashCode((int[]) obj); + } + if (obj instanceof long[]) { + return nullSafeHashCode((long[]) obj); + } + if (obj instanceof short[]) { + return nullSafeHashCode((short[]) obj); + } + } + return obj.hashCode(); + } + + /** + * Return a hash code based on the contents of the specified array. If {@code array} is {@code null}, this method + * returns 0. + */ + public static int nullSafeHashCode(Object[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (Object element: array) { + hash = MULTIPLIER * hash + nullSafeHashCode(element); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. If {@code array} is {@code null}, this method + * returns 0. + */ + public static int nullSafeHashCode(boolean[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (boolean element: array) { + hash = MULTIPLIER * hash + hashCode(element); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. If {@code array} is {@code null}, this method + * returns 0. + */ + public static int nullSafeHashCode(byte[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (byte element: array) { + hash = MULTIPLIER * hash + element; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. If {@code array} is {@code null}, this method + * returns 0. + */ + public static int nullSafeHashCode(char[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (char element: array) { + hash = MULTIPLIER * hash + element; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. If {@code array} is {@code null}, this method + * returns 0. + */ + public static int nullSafeHashCode(double[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (double element: array) { + hash = MULTIPLIER * hash + hashCode(element); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. If {@code array} is {@code null}, this method + * returns 0. + */ + public static int nullSafeHashCode(float[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (float element: array) { + hash = MULTIPLIER * hash + hashCode(element); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. If {@code array} is {@code null}, this method + * returns 0. + */ + public static int nullSafeHashCode(int[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (int element: array) { + hash = MULTIPLIER * hash + element; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. If {@code array} is {@code null}, this method + * returns 0. + */ + public static int nullSafeHashCode(long[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (long element: array) { + hash = MULTIPLIER * hash + hashCode(element); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. If {@code array} is {@code null}, this method + * returns 0. + */ + public static int nullSafeHashCode(short[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (short element: array) { + hash = MULTIPLIER * hash + element; + } + return hash; + } + + /** + * Return the same value as {@link Boolean#hashCode()}}. + * + * @see Boolean#hashCode() + */ + public static int hashCode(boolean bool) { + return (bool ? 1231 : 1237); + } + + /** + * Return the same value as {@link Double#hashCode()}}. + * + * @see Double#hashCode() + */ + public static int hashCode(double dbl) { + return hashCode(Double.doubleToLongBits(dbl)); + } + + /** + * Return the same value as {@link Float#hashCode()}}. + * + * @see Float#hashCode() + */ + public static int hashCode(float flt) { + return Float.floatToIntBits(flt); + } + + /** + * Return the same value as {@link Long#hashCode()}}. + * + * @see Long#hashCode() + */ + public static int hashCode(long lng) { + return (int) (lng ^ (lng >>> 32)); + } + + + //--------------------------------------------------------------------- + // Convenience methods for toString output + //--------------------------------------------------------------------- + + /** + * Return a String representation of an object's overall identity. + * + * @param obj the object (may be {@code null}). + * + * @return the object's identity as String representation, or an empty String if the object was {@code null}. + */ + public static String identityToString(Object obj) { + if (obj == null) { + return EMPTY_STRING; + } + return obj.getClass().getName() + "@" + getIdentityHexString(obj); + } + + /** + * Return a hex String form of an object's identity hash code. + * + * @param obj the object. + * + * @return the object's identity code in hex notation. + */ + public static String getIdentityHexString(Object obj) { + return Integer.toHexString(System.identityHashCode(obj)); + } + + /** + * Return a content-based String representation if {@code obj} is not {@code null}; otherwise returns an empty + * String. + *

Differs from {@link #nullSafeToString(Object)} in that it returns an empty String rather than "null" for a + * {@code null} value. + * + * @param obj the object to build a display String for. + * + * @return a display String representation of {@code obj}. + * + * @see #nullSafeToString(Object) + */ + public static String getDisplayString(Object obj) { + if (obj == null) { + return EMPTY_STRING; + } + return nullSafeToString(obj); + } + + /** + * Determine the class name for the given object.

Returns {@code "null"} if {@code obj} is {@code null}. + * + * @param obj the object to introspect (may be {@code null}). + * + * @return the corresponding class name. + */ + public static String nullSafeClassName(Object obj) { + return (obj != null ? obj.getClass().getName() : NULL_STRING); + } + + /** + * Return a String representation of the specified Object. + * + *

Builds a String representation of the contents in case of an array. Returns {@code "null"} if {@code obj} is + * {@code null}. + * + * @param obj the object to build a String representation for. + * + * @return a String representation of {@code obj}. + */ + public static String nullSafeToString(Object obj) { + if (obj == null) { + return NULL_STRING; + } + if (obj instanceof String) { + return (String) obj; + } + if (obj instanceof Object[]) { + return nullSafeToString((Object[]) obj); + } + if (obj instanceof boolean[]) { + return nullSafeToString((boolean[]) obj); + } + if (obj instanceof byte[]) { + return nullSafeToString((byte[]) obj); + } + if (obj instanceof char[]) { + return nullSafeToString((char[]) obj); + } + if (obj instanceof double[]) { + return nullSafeToString((double[]) obj); + } + if (obj instanceof float[]) { + return nullSafeToString((float[]) obj); + } + if (obj instanceof int[]) { + return nullSafeToString((int[]) obj); + } + if (obj instanceof long[]) { + return nullSafeToString((long[]) obj); + } + if (obj instanceof short[]) { + return nullSafeToString((short[]) obj); + } + String str = obj.toString(); + return (str != null ? str : EMPTY_STRING); + } + + /** + * Return a String representation of the contents of the specified array. + * + *

The String representation consists of a list of the array's elements, enclosed in curly braces ({@code + * "{}"}). Adjacent elements are separated by the characters {@code ", "} (a comma followed by a space). Returns + * {@code "null"} if {@code array} is {@code null}. + * + * @param array the array to build a String representation for. + * + * @return a String representation of {@code array}. + */ + public static String nullSafeToString(Object[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(String.valueOf(array[i])); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + * + *

The String representation consists of a list of the array's elements, enclosed in curly braces ({@code + * "{}"}). Adjacent elements are separated by the characters {@code ", "} (a comma followed by a space). Returns + * {@code "null"} if {@code array} is {@code null}. + * + * @param array the array to build a String representation for. + * + * @return a String representation of {@code array}. + */ + public static String nullSafeToString(boolean[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + * + *

The String representation consists of a list of the array's elements, enclosed in curly braces ({@code + * "{}"}). Adjacent elements are separated by the characters {@code ", "} (a comma followed by a space). Returns + * {@code "null"} if {@code array} is {@code null}. + * + * @param array the array to build a String representation for. + * + * @return a String representation of {@code array}. + */ + public static String nullSafeToString(byte[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + * + *

The String representation consists of a list of the array's elements, enclosed in curly braces ({@code + * "{}"}). Adjacent elements are separated by the characters {@code ", "} (a comma followed by a space). Returns + * {@code "null"} if {@code array} is {@code null}. + * + * @param array the array to build a String representation for. + * + * @return a String representation of {@code array}. + */ + public static String nullSafeToString(char[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append("'").append(array[i]).append("'"); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + * + *

The String representation consists of a list of the array's elements, enclosed in curly braces ({@code + * "{}"}). Adjacent elements are separated by the characters {@code ", "} (a comma followed by a space). Returns + * {@code "null"} if {@code array} is {@code null}. + * + * @param array the array to build a String representation for. + * + * @return a String representation of {@code array}. + */ + public static String nullSafeToString(double[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + * + *

The String representation consists of a list of the array's elements, enclosed in curly braces ({@code + * "{}"}). Adjacent elements are separated by the characters {@code ", "} (a comma followed by a space). Returns + * {@code "null"} if {@code array} is {@code null}. + * + * @param array the array to build a String representation for. + * + * @return a String representation of {@code array}. + */ + public static String nullSafeToString(float[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + * + *

The String representation consists of a list of the array's elements, enclosed in curly braces ({@code + * "{}"}). Adjacent elements are separated by the characters {@code ", "} (a comma followed by a space). Returns + * {@code "null"} if {@code array} is {@code null}. + * + * @param array the array to build a String representation for. + * + * @return a String representation of {@code array}. + */ + public static String nullSafeToString(int[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + * + *

The String representation consists of a list of the array's elements, enclosed in curly braces ({@code + * "{}"}). Adjacent elements are separated by the characters {@code ", "} (a comma followed by a space). Returns + * {@code "null"} if {@code array} is {@code null}. + * + * @param array the array to build a String representation for. + * + * @return a String representation of {@code array}. + */ + public static String nullSafeToString(long[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + * + *

The String representation consists of a list of the array's elements, enclosed in curly braces ({@code + * "{}"}). Adjacent elements are separated by the characters {@code ", "} (a comma followed by a space). Returns + * {@code "null"} if {@code array} is {@code null}. + * + * @param array the array to build a String representation for. + * + * @return a String representation of {@code array}. + */ + public static String nullSafeToString(short[] array) { + if (array == null) { + return NULL_STRING; + } + + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/Patterns.java b/api/src/main/java/com/yanzhenjie/andserver/util/Patterns.java new file mode 100644 index 0000000000000000000000000000000000000000..4051c8b6eee8b0452d3581bc9fd4d72d28d86def --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/Patterns.java @@ -0,0 +1,37 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util; + +/** + * Created by Zhenjie Yan on 2018/9/5. + */ +public interface Patterns { + + String WORD = "[a-zA-Z0-9_\\-\\.]%s"; + + String PATH_0 = String.format(WORD, "*"); + String PATH_1 = String.format(WORD, "+"); + String PATH = String.format("((/%s)|((/%s)+))|((/%s)+/)", PATH_0, PATH_1, PATH_1); + + String PAIR_KEY = String.format(WORD, "+"); + String PAIR_VALUE = "(.)*"; + String PAIR_KEY_VALUE = String.format("(%s)(=)(%s)", PAIR_KEY, PAIR_VALUE); + String PAIR_NO_KEY = String.format("!%s", PAIR_KEY); + String PAIR_NO_VALUE = String.format("(%s)(!=)(%s)", PAIR_KEY, PATH_1); + + String FORWARD = "forward:(.)*"; + String REDIRECT = "redirect:(.)*"; +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/StatusCode.java b/api/src/main/java/com/yanzhenjie/andserver/util/StatusCode.java new file mode 100644 index 0000000000000000000000000000000000000000..0aa89ee4294ac90f6ff0fe980eae310503c25b9f --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/StatusCode.java @@ -0,0 +1,25 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util; + +/** + * Created by Zhenjie Yan on 2018/7/26. + * + * @deprecated use {@link com.yanzhenjie.andserver.http.StatusCode} instead. + */ +@Deprecated +public interface StatusCode extends com.yanzhenjie.andserver.http.StatusCode { +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/StringUtils.java b/api/src/main/java/com/yanzhenjie/andserver/util/StringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..a7df420b16cd660097327635ccf4aef3be65785c --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/StringUtils.java @@ -0,0 +1,1232 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util; + +import java.util.*; + +/** + * Created by Zhenjie Yan on 2018/7/10. + * + * @deprecated use apache commons-lang instead. + */ +@Deprecated +public abstract class StringUtils { + + private static final String FOLDER_SEPARATOR = "/"; + private static final String WINDOWS_FOLDER_SEPARATOR = "\\"; + private static final String TOP_PATH = ".."; + private static final String CURRENT_PATH = "."; + private static final char EXTENSION_SEPARATOR = '.'; + + //------------------------------------------------------- + // General convenience methods for working with Strings + //------------------------------------------------------- + + /** + * Check whether the given {@code String} is empty. + * + *

This method accepts any Object as an argument, comparing it to null and the empty String. As a consequence, + * this method will never return true for a non-null non-String object. + * + *

The Object signature is useful for general attribute handling code that commonly deals with Strings but + * generally has to iterate over Objects since attributes may e.g. be primitive value objects as well. + * + * @param str the candidate string. + */ + public static boolean isEmpty(Object str) { + return (str == null || "".equals(str)); + } + + /** + * Check that the given {@code CharSequence} is neither null nor of length 0. + * + *

Note: this method returns true for a {@code CharSequence} that purely consists of whitespace. + * + *

 StringUtils.hasLength(null) = false StringUtils.hasLength("") = false
+     * StringUtils.hasLength(" ") = true StringUtils.hasLength("Hello") = true 
+ * + * @param str the {@code CharSequence} to check (may be null). + * + * @return true if the {@code CharSequence} is not null and has length. + */ + public static boolean hasLength(CharSequence str) { + return (str != null && str.length() > 0); + } + + /** + * Check that the given {@code String} is neither null nor of length 0. + * + *

Note: this method returns true for a {@code String} that purely consists of whitespace. + * + * @param str the {@code String} to check (may be null). + * + * @return true if the {@code String} is not null and has length. + */ + public static boolean hasLength(String str) { + return (str != null && !str.isEmpty()); + } + + /** + * Check whether the given {@code CharSequence} contains actual text. + * + *

More specifically, this method returns true if the {@code CharSequence} is not null, its length is greater + * than 0, and it contains at least one non-whitespace character. + * + *

 StringUtils.hasText(null) = false StringUtils.hasText("") = false StringUtils.hasText(" ")
+     * = false StringUtils.hasText("12345") = true StringUtils.hasText(" 12345 ") = true 
+ * + * @param str the {@code CharSequence} to check (may be null). + * + * @return true if the {@code CharSequence} is not null, its length is greater than 0, and it does not contain + * whitespace only. + */ + public static boolean hasText(CharSequence str) { + return (hasLength(str) && containsText(str)); + } + + /** + * Check whether the given {@code String} contains actual text. + * + *

More specifically, this method returns true if the {@code String} is not null, its length is greater than 0, + * and it contains at least one non-whitespace character. + * + * @param str the {@code String} to check (may be null). + * + * @return true if the {@code String} is not null, its length is greater than 0, and it does not contain whitespace + * only. + */ + public static boolean hasText(String str) { + return (hasLength(str) && containsText(str)); + } + + private static boolean containsText(CharSequence str) { + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + /** + * Check whether the given {@code CharSequence} contains any whitespace characters. + * + * @param str the {@code CharSequence} to check (may be null). + * + * @return true if the {@code CharSequence} is not empty and contains at least 1 whitespace character. + */ + public static boolean containsWhitespace(CharSequence str) { + if (!hasLength(str)) { + return false; + } + + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + /** + * Check whether the given {@code String} contains any whitespace characters. + * + * @param str the {@code String} to check (may be null). + * + * @return true if the {@code String} is not empty and contains at least 1 whitespace character. + */ + public static boolean containsWhitespace(String str) { + return containsWhitespace((CharSequence) str); + } + + /** + * Trim leading and trailing whitespace from the given {@code String}. + * + * @param str the {@code String} to check. + * + * @return the trimmed {@code String}. + */ + public static String trimWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) { + sb.deleteCharAt(0); + } + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1))) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + /** + * Trim all whitespace from the given {@code String}: leading, trailing, and in between characters. + * + * @param str the {@code String} to check. + * + * @return the trimmed {@code String}. + */ + public static String trimAllWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + + int len = str.length(); + StringBuilder sb = new StringBuilder(str.length()); + for (int i = 0; i < len; i++) { + char c = str.charAt(i); + if (!Character.isWhitespace(c)) { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * Trim leading whitespace from the given {@code String}. + * + * @param str the {@code String} to check. + * + * @return the trimmed {@code String}. + */ + public static String trimLeadingWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) { + sb.deleteCharAt(0); + } + return sb.toString(); + } + + /** + * Trim trailing whitespace from the given {@code String}. + * + * @param str the {@code String} to check. + * + * @return the trimmed {@code String}. + */ + public static String trimTrailingWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1))) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + /** + * Trim all occurrences of the supplied leading character from the given {@code String}. + * + * @param str the {@code String} to check. + * @param leadingCharacter the leading character to be trimmed. + * + * @return the trimmed {@code String}. + */ + public static String trimLeadingCharacter(String str, char leadingCharacter) { + if (!hasLength(str)) { + return str; + } + + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && sb.charAt(0) == leadingCharacter) { + sb.deleteCharAt(0); + } + return sb.toString(); + } + + /** + * Trim all occurrences of the supplied trailing character from the given {@code String}. + * + * @param str the {@code String} to check. + * @param trailingCharacter the trailing character to be trimmed. + * + * @return the trimmed {@code String}. + */ + public static String trimTrailingCharacter(String str, char trailingCharacter) { + if (!hasLength(str)) { + return str; + } + + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && sb.charAt(sb.length() - 1) == trailingCharacter) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + /** + * Test if the given {@code String} starts with the specified prefix, ignoring upper/lower case. + * + * @param str the {@code String} to check. + * @param prefix the prefix to look for. + */ + public static boolean startsWithIgnoreCase(String str, String prefix) { + return (str != null && prefix != null && str.length() >= prefix.length() && + str.regionMatches(true, 0, prefix, 0, prefix.length())); + } + + /** + * Test if the given {@code String} ends with the specified suffix, ignoring upper/lower case. + * + * @param str the {@code String} to check. + * @param suffix the suffix to look for. + */ + public static boolean endsWithIgnoreCase(String str, String suffix) { + return (str != null && suffix != null && str.length() >= suffix.length() && + str.regionMatches(true, str.length() - suffix.length(), suffix, 0, suffix.length())); + } + + /** + * Test whether the given string matches the given substring at the given index. + * + * @param str the original string (or StringBuilder). + * @param index the index in the original string to start matching against. + * @param substring the substring to match at the given index. + */ + public static boolean substringMatch(CharSequence str, int index, CharSequence substring) { + if (index + substring.length() > str.length()) { + return false; + } + for (int i = 0; i < substring.length(); i++) { + if (str.charAt(index + i) != substring.charAt(i)) { + return false; + } + } + return true; + } + + /** + * Count the occurrences of the substring {@code sub} in string {@code str}. + * + * @param str string to search in. + * @param sub string to search for. + */ + public static int countOccurrencesOf(String str, String sub) { + if (!hasLength(str) || !hasLength(sub)) { + return 0; + } + + int count = 0; + int pos = 0; + int idx; + while ((idx = str.indexOf(sub, pos)) != -1) { + ++count; + pos = idx + sub.length(); + } + return count; + } + + /** + * Replace all occurrences of a substring within a string with another string. + * + * @param inString {@code String} to examine. + * @param oldPattern {@code String} to replace. + * @param newPattern {@code String} to insert. + * + * @return a {@code String} with the replacements. + */ + public static String replace(String inString, String oldPattern, String newPattern) { + if (!hasLength(inString) || !hasLength(oldPattern) || newPattern == null) { + return inString; + } + int index = inString.indexOf(oldPattern); + if (index == -1) { + // no occurrence -> can return input as-is + return inString; + } + + int capacity = inString.length(); + if (newPattern.length() > oldPattern.length()) { + capacity += 16; + } + StringBuilder sb = new StringBuilder(capacity); + + int pos = 0; // our position in the old string + int patLen = oldPattern.length(); + while (index >= 0) { + sb.append(inString.substring(pos, index)); + sb.append(newPattern); + pos = index + patLen; + index = inString.indexOf(oldPattern, pos); + } + + // append any characters to the right of a match + sb.append(inString.substring(pos)); + return sb.toString(); + } + + /** + * Delete all occurrences of the given substring. + * + * @param inString the original {@code String}. + * @param pattern the pattern to delete all occurrences of. + * + * @return the resulting {@code String}. + */ + public static String delete(String inString, String pattern) { + return replace(inString, pattern, ""); + } + + /** + * Delete any character in a given {@code String}. + * + * @param inString the original {@code String}. + * @param charsToDelete a set of characters to delete. E.g. "az\n" will delete 'a's, 'z's and new lines. + * + * @return the resulting {@code String}. + */ + public static String deleteAny(String inString, String charsToDelete) { + if (!hasLength(inString) || !hasLength(charsToDelete)) { + return inString; + } + + StringBuilder sb = new StringBuilder(inString.length()); + for (int i = 0; i < inString.length(); i++) { + char c = inString.charAt(i); + if (charsToDelete.indexOf(c) == -1) { + sb.append(c); + } + } + return sb.toString(); + } + + + //--------------------------------------------------------- + // Convenience methods for working with formatted Strings + //--------------------------------------------------------- + + /** + * Quote the given {@code String} with single quotes. + * + * @param str the input {@code String} (e.g. "myString"). + * + * @return the quoted {@code String} (e.g. "'myString'"), or null if the input was null. + */ + public static String quote(String str) { + return (str != null ? "'" + str + "'" : null); + } + + /** + * Turn the given Object into a {@code String} with single quotes if it is a {@code String}; keeping the Object + * as-is else. + * + * @param obj the input Object (e.g. "myString"). + * + * @return the quoted {@code String} (e.g. "'myString'"), or the input object as-is if not a {@code String}. + */ + public static Object quoteIfString(Object obj) { + return (obj instanceof String ? quote((String) obj) : obj); + } + + /** + * Unqualify a string qualified by a '.' dot character. For example, "this.name.is.qualified", returns "qualified". + * + * @param qualifiedName the qualified name. + */ + public static String unqualify(String qualifiedName) { + return unqualify(qualifiedName, '.'); + } + + /** + * Unqualify a string qualified by a separator character. For example, "this:name:is:qualified" returns "qualified" + * if using a ':' separator. + * + * @param qualifiedName the qualified name. + * @param separator the separator. + */ + public static String unqualify(String qualifiedName, char separator) { + return qualifiedName.substring(qualifiedName.lastIndexOf(separator) + 1); + } + + /** + * Capitalize a {@code String}, changing the first letter to upper case as per {@link Character#toUpperCase(char)}. + * No other letters are changed. + * + * @param str the {@code String} to capitalize. + * + * @return the capitalized {@code String}. + */ + public static String capitalize(String str) { + return changeFirstCharacterCase(str, true); + } + + /** + * Uncapitalize a {@code String}, changing the first letter to lower case as per {@link + * Character#toLowerCase(char)}. No other letters are changed. + * + * @param str the {@code String} to uncapitalize. + * + * @return the uncapitalized {@code String}. + */ + public static String uncapitalize(String str) { + return changeFirstCharacterCase(str, false); + } + + private static String changeFirstCharacterCase(String str, boolean capitalize) { + if (!hasLength(str)) { + return str; + } + + char baseChar = str.charAt(0); + char updatedChar; + if (capitalize) { + updatedChar = Character.toUpperCase(baseChar); + } else { + updatedChar = Character.toLowerCase(baseChar); + } + if (baseChar == updatedChar) { + return str; + } + + char[] chars = str.toCharArray(); + chars[0] = updatedChar; + return new String(chars, 0, chars.length); + } + + /** + * Extract the filename from the given Java resource path, e.g. {@code "mypath/myfile.txt" -> "myfile.txt"}. + * + * @param path the file path (may be null). + * + * @return the extracted filename, or null if none. + */ + public static String getFilename(String path) { + if (path == null) { + return null; + } + + int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR); + return (separatorIndex != -1 ? path.substring(separatorIndex + 1) : path); + } + + /** + * Extract the filename extension from the given Java resource path, e.g. "mypath/myfile.txt" -> "txt". + * + * @param path the file path (may be null). + * + * @return the extracted filename extension, or null if none. + */ + public static String getFilenameExtension(String path) { + if (path == null) { + return null; + } + + int extIndex = path.lastIndexOf(EXTENSION_SEPARATOR); + if (extIndex == -1) { + return null; + } + + int folderIndex = path.lastIndexOf(FOLDER_SEPARATOR); + if (folderIndex > extIndex) { + return null; + } + + return path.substring(extIndex + 1); + } + + /** + * Strip the filename extension from the given Java resource path, e.g. "mypath/myfile.txt" -> "mypath/myfile". + * + * @param path the file path. + * + * @return the path with stripped filename extension. + */ + public static String stripFilenameExtension(String path) { + if (path == null) { + return null; + } + + int extIndex = path.lastIndexOf(EXTENSION_SEPARATOR); + if (extIndex == -1) { + return path; + } + + int folderIndex = path.lastIndexOf(FOLDER_SEPARATOR); + if (folderIndex > extIndex) { + return path; + } + + return path.substring(0, extIndex); + } + + /** + * Apply the given relative path to the given Java resource path, assuming standard Java folder separation (i.e. "/" + * separators). + * + * @param path the path to start from (usually a full file path). + * @param relativePath the relative path to apply (relative to the full file path above). + * + * @return the full file path that results from applying the relative path. + */ + public static String applyRelativePath(String path, String relativePath) { + int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR); + if (separatorIndex != -1) { + String newPath = path.substring(0, separatorIndex); + if (!relativePath.startsWith(FOLDER_SEPARATOR)) { + newPath += FOLDER_SEPARATOR; + } + return newPath + relativePath; + } else { + return relativePath; + } + } + + /** + * Normalize the path by suppressing sequences like "path/.." and inner simple dots.

The result is convenient for + * path comparison. For other uses, notice that Windows separators ("\") are replaced by simple slashes. + * + * @param path the original path. + * + * @return the normalized path. + */ + public static String cleanPath(String path) { + if (path == null) { + return null; + } + String pathToUse = replace(path, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR); + + // Strip prefix from path to analyze, to not treat it as part of the + // first path element. This is necessary to correctly parse paths like + // "file:core/../core/io/Resource.class", where the ".." should just + // strip the first "core" directory while keeping the "file:" prefix. + int prefixIndex = pathToUse.indexOf(":"); + String prefix = ""; + if (prefixIndex != -1) { + prefix = pathToUse.substring(0, prefixIndex + 1); + if (prefix.contains("/")) { + prefix = ""; + } else { + pathToUse = pathToUse.substring(prefixIndex + 1); + } + } + if (pathToUse.startsWith(FOLDER_SEPARATOR)) { + prefix = prefix + FOLDER_SEPARATOR; + pathToUse = pathToUse.substring(1); + } + + String[] pathArray = delimitedListToStringArray(pathToUse, FOLDER_SEPARATOR); + List pathElements = new LinkedList(); + int tops = 0; + + for (int i = pathArray.length - 1; i >= 0; i--) { + String element = pathArray[i]; + if (CURRENT_PATH.equals(element)) { + // Points to current directory - drop it. + } else if (TOP_PATH.equals(element)) { + // Registering top path found. + tops++; + } else { + if (tops > 0) { + // Merging path element with element corresponding to top path. + tops--; + } else { + // Normal path element found. + pathElements.add(0, element); + } + } + } + + // Remaining top paths need to be retained. + for (int i = 0; i < tops; i++) { + pathElements.add(0, TOP_PATH); + } + + return prefix + collectionToDelimitedString(pathElements, FOLDER_SEPARATOR); + } + + /** + * Compare two paths after normalization of them. + * + * @param path1 first path for comparison. + * @param path2 second path for comparison. + * + * @return whether the two paths are equivalent after normalization. + */ + public static boolean pathEquals(String path1, String path2) { + return cleanPath(path1).equals(cleanPath(path2)); + } + + /** + * Parse the given {@code localeString} value into a {@link Locale}.

This is the inverse operation of {@link + * Locale#toString Locale's toString}. + * + * @param localeString the locale {@code String}, following {@code Locale's} {@code toString()} format ("en", + * "en_UK", etc); also accepts spaces as separators, as an alternative to underscores. + * + * @return a corresponding {@code Locale} instance, or {@code null} if none. + * + * @throws IllegalArgumentException in case of an invalid locale specification. + */ + public static Locale parseLocaleString(String localeString) { + String[] parts = tokenizeToStringArray(localeString, "_ ", false, false); + String language = (parts.length > 0 ? parts[0] : ""); + String country = (parts.length > 1 ? parts[1] : ""); + + validateLocalePart(language); + validateLocalePart(country); + + String variant = ""; + if (parts.length > 2) { + // There is definitely a variant, and it is everything after the country + // code sans the separator between the country code and the variant. + int endIndexOfCountryCode = localeString.indexOf(country, language.length()) + country.length(); + // Strip off any leading '_' and whitespace, what's left is the variant. + variant = trimLeadingWhitespace(localeString.substring(endIndexOfCountryCode)); + if (variant.startsWith("_")) { + variant = trimLeadingCharacter(variant, '_'); + } + } + return (language.length() > 0 ? new Locale(language, country, variant) : null); + } + + private static void validateLocalePart(String localePart) { + for (int i = 0; i < localePart.length(); i++) { + char ch = localePart.charAt(i); + if (ch != ' ' && ch != '_' && ch != '#' && !Character.isLetterOrDigit(ch)) { + String message = "Locale part \"" + localePart + "\" contains invalid characters"; + throw new IllegalArgumentException(message); + } + } + } + + /** + * Determine the RFC 3066 compliant language tag, as used for the HTTP "Accept-Language" header. + * + * @param locale the Locale to transform to a language tag. + * + * @return the RFC 3066 compliant language tag as {@code String}. + */ + public static String toLanguageTag(Locale locale) { + return locale.getLanguage() + (hasText(locale.getCountry()) ? "-" + locale.getCountry() : ""); + } + + /** + * Parse the given {@code timeZoneString} value into a {@link TimeZone}. + * + * @param timeZoneString the time zone {@code String}, following {@link TimeZone#getTimeZone(String)} but + * throwing {@link IllegalArgumentException} in case of an invalid time zone specification. + * + * @return a corresponding {@link TimeZone} instance. + * + * @throws IllegalArgumentException in case of an invalid time zone specification. + */ + public static TimeZone parseTimeZoneString(String timeZoneString) { + TimeZone timeZone = TimeZone.getTimeZone(timeZoneString); + if ("GMT".equals(timeZone.getID()) && !timeZoneString.startsWith("GMT")) { + // We don't want that GMT fallback... + throw new IllegalArgumentException("Invalid time zone specification '" + timeZoneString + "'"); + } + return timeZone; + } + + + //----------------------------------------------------- + // Convenience methods for working with String arrays + //----------------------------------------------------- + + /** + * Append the given {@code String} to the given {@code String} array, returning a new array consisting of the input + * array contents plus the given {@code String}. + * + * @param array the array to append to (can be {@code null}). + * @param str the {@code String} to append. + * + * @return the new array (never {@code null}). + */ + public static String[] addStringToArray(String[] array, String str) { + if (ObjectUtils.isEmpty(array)) { + return new String[] {str}; + } + + String[] newArr = new String[array.length + 1]; + System.arraycopy(array, 0, newArr, 0, array.length); + newArr[array.length] = str; + return newArr; + } + + /** + * Concatenate the given {@code String} arrays into one, with overlapping array elements included twice.

The + * order of elements in the original arrays is preserved. + * + * @param array1 the first array (can be {@code null}). + * @param array2 the second array (can be {@code null}). + * + * @return the new array ({@code null} if both given arrays were {@code null}). + */ + public static String[] concatenateStringArrays(String[] array1, String[] array2) { + if (ObjectUtils.isEmpty(array1)) { + return array2; + } + if (ObjectUtils.isEmpty(array2)) { + return array1; + } + + String[] newArr = new String[array1.length + array2.length]; + System.arraycopy(array1, 0, newArr, 0, array1.length); + System.arraycopy(array2, 0, newArr, array1.length, array2.length); + return newArr; + } + + /** + * Merge the given {@code String} arrays into one, with overlapping array elements only included once.

The order + * of elements in the original arrays is preserved (with the exception of overlapping elements, which are only + * included on their first occurrence). + * + * @param array1 the first array (can be {@code null}). + * @param array2 the second array (can be {@code null}). + * + * @return the new array ({@code null} if both given arrays were {@code null}). + */ + public static String[] mergeStringArrays(String[] array1, String[] array2) { + if (ObjectUtils.isEmpty(array1)) { + return array2; + } + if (ObjectUtils.isEmpty(array2)) { + return array1; + } + + List result = new ArrayList(); + result.addAll(Arrays.asList(array1)); + for (String str: array2) { + if (!result.contains(str)) { + result.add(str); + } + } + return toStringArray(result); + } + + /** + * Turn given source {@code String} array into sorted array. + * + * @param array the source array. + * + * @return the sorted array (never {@code null}). + */ + public static String[] sortStringArray(String[] array) { + if (ObjectUtils.isEmpty(array)) { + return new String[0]; + } + + Arrays.sort(array); + return array; + } + + /** + * Copy the given {@code Collection} into a {@code String} array.

The {@code Collection} must contain {@code + * String} elements only. + * + * @param collection the {@code Collection} to copy. + * + * @return the {@code String} array. + */ + public static String[] toStringArray(Collection collection) { + if (collection == null) { + return null; + } + + return collection.toArray(new String[collection.size()]); + } + + /** + * Copy the given Enumeration into a {@code String} array. The Enumeration must contain {@code String} elements + * only. + * + * @param enumeration the Enumeration to copy. + * + * @return the {@code String} array. + */ + public static String[] toStringArray(Enumeration enumeration) { + if (enumeration == null) { + return null; + } + + List list = Collections.list(enumeration); + return list.toArray(new String[list.size()]); + } + + /** + * Trim the elements of the given {@code String} array, calling {@code String.trim()} on each of them. + * + * @param array the original {@code String} array. + * + * @return the resulting array (of the same size) with trimmed elements. + */ + public static String[] trimArrayElements(String[] array) { + if (ObjectUtils.isEmpty(array)) { + return new String[0]; + } + + String[] result = new String[array.length]; + for (int i = 0; i < array.length; i++) { + String element = array[i]; + result[i] = (element != null ? element.trim() : null); + } + return result; + } + + /** + * Remove duplicate strings from the given array. + * + *

As of 4.2, it preserves the original order, as it uses a {@link LinkedHashSet}. + * + * @param array the {@code String} array. + * + * @return an array without duplicates, in natural sort order. + */ + public static String[] removeDuplicateStrings(String[] array) { + if (ObjectUtils.isEmpty(array)) { + return array; + } + + Set set = new LinkedHashSet<>(); + for (String element: array) { + set.add(element); + } + return toStringArray(set); + } + + /** + * Split a {@code String} at the first occurrence of the delimiter. Does not include the delimiter in the result. + * + * @param toSplit the string to split. + * @param delimiter to split the string up with. + * + * @return a two element array with index 0 being before the delimiter, and index 1 being after the delimiter + * (neither element includes the delimiter); or {@code null} if the delimiter wasn't found in the given input + * {@code String}. + */ + public static String[] split(String toSplit, String delimiter) { + if (!hasLength(toSplit) || !hasLength(delimiter)) { + return null; + } + int offset = toSplit.indexOf(delimiter); + if (offset < 0) { + return null; + } + + String beforeDelimiter = toSplit.substring(0, offset); + String afterDelimiter = toSplit.substring(offset + delimiter.length()); + return new String[] {beforeDelimiter, afterDelimiter}; + } + + /** + * Take an array of strings and split each element based on the given delimiter. StandardCookieProcessor {@code + * Properties} instance is then generated, with the left of the delimiter providing the key, and the right of the + * delimiter providing the value. + * + *

Will trim both the key and value before adding them to the {@code Properties} instance. + * + * @param array the array to process. + * @param delimiter to split each element using (typically the equals symbol). + * + * @return a {@code Properties} instance representing the array contents, or {@code null} if the array to process + * was {@code null} or empty. + */ + public static Properties splitArrayElementsIntoProperties(String[] array, String delimiter) { + return splitArrayElementsIntoProperties(array, delimiter, null); + } + + /** + * Take an array of strings and split each element based on the given delimiter. StandardCookieProcessor {@code + * Properties} instance is then generated, with the left of the delimiter providing the key, and the right of the + * delimiter providing the value. + * + *

Will trim both the key and value before adding them to the {@code Properties} instance. + * + * @param array the array to process. + * @param delimiter to split each element using (typically the equals symbol). + * @param charsToDelete one or more characters to remove from each element prior to attempting the split + * operation (typically the quotation mark symbol), or {@code null} if no removal should occur. + * + * @return a {@code Properties} instance representing the array contents, or {@code null} if the array to process + * was {@code null} or empty. + */ + public static Properties splitArrayElementsIntoProperties(String[] array, String delimiter, String charsToDelete) { + + if (ObjectUtils.isEmpty(array)) { + return null; + } + + Properties result = new Properties(); + for (String element: array) { + if (charsToDelete != null) { + element = deleteAny(element, charsToDelete); + } + String[] splittedElement = split(element, delimiter); + if (splittedElement == null) { + continue; + } + result.setProperty(splittedElement[0].trim(), splittedElement[1].trim()); + } + return result; + } + + /** + * Tokenize the given {@code String} into a {@code String} array via a {@link StringTokenizer}.

Trims tokens and + * omits empty tokens. + *

The given {@code delimiters} string can consist of any number of delimiter characters. Each of those + * characters + * can be used to separate tokens. StandardCookieProcessor delimiter is always a single character; for + * multi-character delimiters, consider using {@link #delimitedListToStringArray}. + * + * @param str the {@code String} to tokenize. + * @param delimiters the delimiter characters, assembled as a {@code String} (each of the characters is + * individually considered as a delimiter). + * + * @return an array of the tokens. + * + * @see StringTokenizer + * @see String#trim() + * @see #delimitedListToStringArray + */ + public static String[] tokenizeToStringArray(String str, String delimiters) { + return tokenizeToStringArray(str, delimiters, true, true); + } + + /** + * Tokenize the given {@code String} into a {@code String} array via a {@link StringTokenizer}.

The given {@code + * delimiters} string can consist of any number of delimiter characters. Each of those characters can be used to + * separate tokens. StandardCookieProcessor delimiter is always a single character; for multi-character delimiters, + * consider using {@link #delimitedListToStringArray}. + * + * @param str the {@code String} to tokenize. + * @param delimiters the delimiter characters, assembled as a {@code String} (each of the characters is + * individually considered as a delimiter). + * @param trimTokens trim the tokens via {@link String#trim()}. + * @param ignoreEmptyTokens omit empty tokens from the result array (only applies to tokens that are empty after + * trimming; StringTokenizer will not consider subsequent delimiters as token in the first place). + * + * @return an array of the tokens. + * + * @see StringTokenizer + * @see String#trim() + * @see #delimitedListToStringArray + */ + public static String[] tokenizeToStringArray(String str, String delimiters, boolean trimTokens, + boolean ignoreEmptyTokens) { + + if (str == null) { + return null; + } + + StringTokenizer st = new StringTokenizer(str, delimiters); + List tokens = new ArrayList<>(); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (trimTokens) { + token = token.trim(); + } + if (!ignoreEmptyTokens || token.length() > 0) { + tokens.add(token); + } + } + return toStringArray(tokens); + } + + /** + * Take a {@code String} that is a delimited list and convert it into a {@code String} array. + *

StandardCookieProcessor single {@code delimiter} may consist of more than one character, but it will still be + * considered as a single delimiter string, rather than as bunch of potential delimiter characters, in contrast to + * {@link #tokenizeToStringArray}. + * + * @param str the input {@code String}. + * @param delimiter the delimiter between elements (this is a single delimiter, rather than a bunch individual + * delimiter characters). + * + * @return an array of the tokens in the list. + * + * @see #tokenizeToStringArray + */ + public static String[] delimitedListToStringArray(String str, String delimiter) { + return delimitedListToStringArray(str, delimiter, null); + } + + /** + * Take a {@code String} that is a delimited list and convert it into a {@code String} array. + *

StandardCookieProcessor single {@code delimiter} may consist of more than one character, but it will still be + * considered as a single delimiter string, rather than as bunch of potential delimiter characters, in contrast to + * {@link #tokenizeToStringArray}. + * + * @param str the input {@code String}. + * @param delimiter the delimiter between elements (this is a single delimiter, rather than a bunch individual + * delimiter characters). + * @param charsToDelete a set of characters to delete; useful for deleting unwanted line breaks: e.g. "\r\n\f" + * will delete all new lines and line feeds in a {@code String}. + * + * @return an array of the tokens in the list. + * + * @see #tokenizeToStringArray + */ + public static String[] delimitedListToStringArray(String str, String delimiter, String charsToDelete) { + if (str == null) { + return new String[0]; + } + if (delimiter == null) { + return new String[] {str}; + } + + List result = new ArrayList(); + if ("".equals(delimiter)) { + for (int i = 0; i < str.length(); i++) { + result.add(deleteAny(str.substring(i, i + 1), charsToDelete)); + } + } else { + int pos = 0; + int delPos; + while ((delPos = str.indexOf(delimiter, pos)) != -1) { + result.add(deleteAny(str.substring(pos, delPos), charsToDelete)); + pos = delPos + delimiter.length(); + } + if (str.length() > 0 && pos <= str.length()) { + // Add rest of String, but not in case of empty input. + result.add(deleteAny(str.substring(pos), charsToDelete)); + } + } + return toStringArray(result); + } + + /** + * Convert a comma delimited list (e.g., a row from a CSV file) into an array of strings. + * + * @param str the input {@code String}. + * + * @return an array of strings, or the empty array in case of empty input. + */ + public static String[] commaDelimitedListToStringArray(String str) { + return delimitedListToStringArray(str, ","); + } + + /** + * Convert a comma delimited list (e.g., a row from a CSV file) into a set.

Note that this will suppress + * duplicates, and as of 4.2, the elements in the returned set will preserve the original order in a {@link + * LinkedHashSet}. + * + * @param str the input {@code String}. + * + * @return a set of {@code String} entries in the list. + * + * @see #removeDuplicateStrings(String[]) + */ + public static Set commaDelimitedListToSet(String str) { + Set set = new LinkedHashSet(); + String[] tokens = commaDelimitedListToStringArray(str); + for (String token: tokens) { + set.add(token); + } + return set; + } + + /** + * Convert a {@link Collection} to a delimited {@code String} (e.g. CSV).

Useful for {@code toString()} + * implementations. + * + * @param coll the {@code Collection} to convert. + * @param delim the delimiter to use (typically a ","). + * @param prefix the {@code String} to start each element with. + * @param suffix the {@code String} to end each element with. + * + * @return the delimited {@code String} + */ + public static String collectionToDelimitedString(Collection coll, String delim, String prefix, String suffix) { + if (coll == null || coll.isEmpty()) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + Iterator it = coll.iterator(); + while (it.hasNext()) { + sb.append(prefix).append(it.next()).append(suffix); + if (it.hasNext()) { + sb.append(delim); + } + } + return sb.toString(); + } + + /** + * Convert a {@code Collection} into a delimited {@code String} (e.g. CSV).

Useful for {@code toString()} + * implementations. + * + * @param coll the {@code Collection} to convert. + * @param delim the delimiter to use (typically a ","). + * + * @return the delimited {@code String}. + */ + public static String collectionToDelimitedString(Collection coll, String delim) { + return collectionToDelimitedString(coll, delim, "", ""); + } + + /** + * Convert a {@code Collection} into a delimited {@code String} (e.g., CSV).

Useful for {@code toString()} + * implementations. + * + * @param coll the {@code Collection} to convert. + * + * @return the delimited {@code String}. + */ + public static String collectionToCommaDelimitedString(Collection coll) { + return collectionToDelimitedString(coll, ","); + } + + /** + * Convert a {@code String} array into a delimited {@code String} (e.g. CSV).

Useful for {@code toString()} + * implementations. + * + * @param arr the array to display. + * @param delim the delimiter to use (typically a ","). + * + * @return the delimited {@code String}. + */ + public static String arrayToDelimitedString(Object[] arr, String delim) { + if (ObjectUtils.isEmpty(arr)) { + return ""; + } + if (arr.length == 1) { + return ObjectUtils.nullSafeToString(arr[0]); + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < arr.length; i++) { + if (i > 0) { + sb.append(delim); + } + sb.append(arr[i]); + } + return sb.toString(); + } + + /** + * Convert a {@code String} array into a comma delimited {@code String} (i.e., CSV).

Useful for {@code + * toString()} implementations. + * + * @param arr the array to display. + * + * @return the delimited {@code String}. + */ + public static String arrayToCommaDelimitedString(Object[] arr) { + return arrayToDelimitedString(arr, ","); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/TextUtils.java b/api/src/main/java/com/yanzhenjie/andserver/util/TextUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d96c69f11822e0e59b1d49ec98604dd9afd3a62a --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/TextUtils.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yanzhenjie.andserver.util; + + + +import java.util.Iterator; + + +public class TextUtils { + + /** + * Returns true if the string is null or 0-length. + * @param str the string to be examined + * @return true if str is null or zero length + */ + public static boolean isEmpty( CharSequence str) { + return str == null || str.length() == 0; + } + + /** + * Returns a string containing the tokens joined by delimiters. + * + * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string + * "null" will be used as the delimiter. + * @param tokens an array objects to be joined. Strings will be formed from the objects by + * calling object.toString(). If tokens is null, a NullPointerException will be thrown. If + * tokens is an empty array, an empty string will be returned. + */ + public static String join(CharSequence delimiter, Object[] tokens) { + final int length = tokens.length; + if (length == 0) { + return ""; + } + final StringBuilder sb = new StringBuilder(); + sb.append(tokens[0]); + for (int i = 1; i < length; i++) { + sb.append(delimiter); + sb.append(tokens[i]); + } + return sb.toString(); + } + + /** + * Returns a string containing the tokens joined by delimiters. + * + * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string + * "null" will be used as the delimiter. + * @param tokens an array objects to be joined. Strings will be formed from the objects by + * calling object.toString(). If tokens is null, a NullPointerException will be thrown. If + * tokens is empty, an empty string will be returned. + */ + public static String join(CharSequence delimiter, Iterable tokens) { + final Iterator it = tokens.iterator(); + if (!it.hasNext()) { + return ""; + } + final StringBuilder sb = new StringBuilder(); + sb.append(it.next()); + while (it.hasNext()) { + sb.append(delimiter); + sb.append(it.next()); + } + return sb.toString(); + } +} diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/ToastUtil.java b/api/src/main/java/com/yanzhenjie/andserver/util/ToastUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..5f5cc1c5f10cbcccee0aa0bc36a46b89cdb1ef9b --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/ToastUtil.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yanzhenjie.andserver.util; + + +import ohos.agp.colors.RgbColor; +import ohos.agp.components.DirectionalLayout; +import ohos.agp.components.Text; +import ohos.agp.components.element.ShapeElement; +import ohos.agp.utils.Color; +import ohos.agp.utils.LayoutAlignment; +import ohos.agp.window.dialog.ToastDialog; +import ohos.agp.window.service.DisplayAttributes; +import ohos.agp.window.service.DisplayManager; +import ohos.app.Context; + +public class ToastUtil { + public static final int LENGTH_LONG = 4000; + public static final int LENGTH_SHORT = 2000; + + public enum ToastLayout { + DEFAULT, + CENTER, + TOP, + BOTTOM, + } + + public static void showShort(Context mContext, String content) { + createTost(mContext, content, LENGTH_SHORT, ToastLayout.DEFAULT); + } + + public static void showLong(Context mContext, String content) { + createTost(mContext, content, LENGTH_LONG, ToastLayout.DEFAULT); + } + + public static void show(Context mContext, String content) { + createTost(mContext, content, LENGTH_SHORT, ToastLayout.DEFAULT); + } + + public static void show(Context mContext, String content, int duration) { + createTost(mContext, content, duration, ToastLayout.DEFAULT); + } + + public static void show(Context mContext, String content, ToastLayout layout) { + createTost(mContext, content, LENGTH_SHORT, layout); + } + + public static void show(Context mContext, String content, int duration, ToastLayout layout) { + createTost(mContext, content, duration, layout); + } + + public static void showShort(Context mContext, int content) { + createTost(mContext, getString(mContext, content), LENGTH_SHORT, ToastLayout.DEFAULT); + } + + public static void showLong(Context mContext, int content) { + createTost(mContext, getString(mContext, content), LENGTH_LONG, ToastLayout.DEFAULT); + } + + public static void show(Context mContext, int content) { + createTost(mContext, getString(mContext, content), LENGTH_SHORT, ToastLayout.DEFAULT); + } + + public static void show(Context mContext, int content, int duration) { + createTost(mContext, getString(mContext, content), duration, ToastLayout.DEFAULT); + } + + public static void show(Context mContext, int content, ToastLayout layout) { + createTost(mContext, getString(mContext, content), LENGTH_SHORT, layout); + } + + public static void show(Context mContext, int content, int duration, ToastLayout layout) { + createTost(mContext, getString(mContext, content), duration, layout); + } + + private static void createTost(Context mContext, String content, int duration, ToastLayout layout) { + DirectionalLayout toastLayout = new DirectionalLayout(mContext); + DirectionalLayout.LayoutConfig textConfig = new DirectionalLayout.LayoutConfig(DirectionalLayout.LayoutConfig.MATCH_CONTENT, DirectionalLayout.LayoutConfig.MATCH_CONTENT); + Text text = new Text(mContext); + text.setText(content); + text.setTextColor(new Color(Color.getIntColor("#ffffff"))); + text.setPadding(vp2px(mContext, 16), vp2px(mContext, 4), vp2px(mContext, 16), vp2px(mContext, 4)); + text.setTextSize(vp2px(mContext, 12)); + text.setBackground(buildDrawableByColorRadius(Color.getIntColor("#70000000"), vp2px(mContext, 20))); + text.setLayoutConfig(textConfig); + toastLayout.addComponent(text); + int mLayout = LayoutAlignment.CENTER; + switch (layout) { + case TOP: + mLayout = LayoutAlignment.TOP; + break; + case BOTTOM: + mLayout = LayoutAlignment.BOTTOM; + break; + case CENTER: + mLayout = LayoutAlignment.CENTER; + break; + } + ToastDialog toastDialog = new ToastDialog(mContext); + toastDialog.setComponent(toastLayout); + toastDialog.setSize(DirectionalLayout.LayoutConfig.MATCH_CONTENT, DirectionalLayout.LayoutConfig.MATCH_CONTENT); + toastDialog.setAlignment(mLayout); + toastDialog.setTransparent(true); + toastDialog.setDuration(duration); + toastDialog.show(); + } + + + private static ohos.agp.components.element.Element buildDrawableByColorRadius(int color, float radius) { + ShapeElement drawable = new ShapeElement(); + drawable.setShape(0); + drawable.setRgbColor(RgbColor.fromArgbInt(color)); + drawable.setCornerRadius(radius); + return drawable; + } + + private static String getString(Context mContent, int resId) { + try { + return mContent.getResourceManager().getElement(resId).getString(); + } catch (Exception e) { + e.printStackTrace(); + } + return ""; + } + + private static int vp2px(Context context, float vp) { + DisplayAttributes attributes = DisplayManager.getInstance().getDefaultDisplay(context).get().getAttributes(); + return (int) (attributes.densityPixels * vp); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/TypeWrapper.java b/api/src/main/java/com/yanzhenjie/andserver/util/TypeWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..dee21880a9dd74dca5b125be36e485f79c9daf7b --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/TypeWrapper.java @@ -0,0 +1,36 @@ +/* + * Copyright © 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * Created by Zhenjie Yan on 2018/9/11. + */ +public abstract class TypeWrapper { + + private final Type mType; + + public TypeWrapper() { + Type superClass = getClass().getGenericSuperclass(); + mType = ((ParameterizedType) superClass).getActualTypeArguments()[0]; + } + + public Type getType() { + return mType; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/URLUtil.java b/api/src/main/java/com/yanzhenjie/andserver/util/URLUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..914402b628182cacf5ba3c112ebbdf8b740fcafe --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/URLUtil.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yanzhenjie.andserver.util; + +public class URLUtil { + /** + * @return {@code true} if the url is an http: url. + */ + public static boolean isHttpUrl(String url) { + return (null != url) && + (url.length() > 6) && + url.substring(0, 7).equalsIgnoreCase("http://"); + } + + /** + * @return {@code true} if the url is an https: url. + */ + public static boolean isHttpsUrl(String url) { + return (null != url) && + (url.length() > 7) && + url.substring(0, 8).equalsIgnoreCase("https://"); + } + + /** + * @return {@code true} if the url is a network url. + */ + public static boolean isNetworkUrl(String url) { + if (url == null || url.length() == 0) { + return false; + } + return isHttpUrl(url) || isHttpsUrl(url); + } +} diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/UrlCoder.java b/api/src/main/java/com/yanzhenjie/andserver/util/UrlCoder.java new file mode 100644 index 0000000000000000000000000000000000000000..e1894bd5984e9ebe8646c939b917626557214bc7 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/UrlCoder.java @@ -0,0 +1,51 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.Charset; + +/** + * Created by Zhenjie Yan on 2018/8/6. + */ +public class UrlCoder { + + public static String urlEncode(String target, String charset) { + try { + return URLEncoder.encode(target, charset); + } catch (UnsupportedEncodingException e) { + return target; + } + } + + public static String urlEncode(String target, Charset charset) { + return urlEncode(target, charset.name()); + } + + public static String urlDecode(String target, String charset) { + try { + return URLDecoder.decode(target, charset); + } catch (UnsupportedEncodingException e) { + return target; + } + } + + public static String urlDecode(String target, Charset charset) { + return urlDecode(target, charset.name()); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/comparator/CompoundComparator.java b/api/src/main/java/com/yanzhenjie/andserver/util/comparator/CompoundComparator.java new file mode 100644 index 0000000000000000000000000000000000000000..9aecd386be7c9ca3418a07f4760c4ebc0792d26b --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/comparator/CompoundComparator.java @@ -0,0 +1,196 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util.comparator; + + +import com.yanzhenjie.andserver.util.Assert; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Created by Zhenjie Yan on 2018/7/11. + */ +public class CompoundComparator implements Comparator, Serializable { + + private final List comparators; + + + /** + * Construct a CompoundComparator with initially no Comparators. Clients must add at least one Comparator before + * calling the compare method or an IllegalStateException is thrown. + */ + public CompoundComparator() { + this.comparators = new ArrayList<>(); + } + + /** + * Construct a CompoundComparator from the Comparators in the provided array. + * + *

All Comparators will default to ascending sort order, unless they are InvertibleComparators. + * + * @param comparators the comparators to build into a compound comparator + * + * @see InvertibleComparator + */ + @SuppressWarnings("unchecked") + public CompoundComparator(Comparator... comparators) { + Assert.notNull(comparators, "Comparators must not be null"); + this.comparators = new ArrayList<>(comparators.length); + for (Comparator comparator : comparators) { + addComparator(comparator); + } + } + + + /** + * Add a Comparator to the end of the chain. + * + *

The Comparator will default to ascending sort order, unless it is a InvertibleComparator. + * + * @param comparator the Comparator to add to the end of the chain. + */ + @SuppressWarnings("unchecked") + public void addComparator(Comparator comparator) { + if (comparator instanceof InvertibleComparator) { + this.comparators.add((InvertibleComparator) comparator); + } else { + this.comparators.add(new InvertibleComparator(comparator)); + } + } + + /** + * Add a Comparator to the end of the chain using the provided sort order. + * + * @param comparator the Comparator to add to the end of the chain + * @param ascending the sort order: ascending (true) or descending (false) + */ + @SuppressWarnings("unchecked") + public void addComparator(Comparator comparator, boolean ascending) { + this.comparators.add(new InvertibleComparator(comparator, ascending)); + } + + /** + * Replace the Comparator at the given index.

The Comparator will default to ascending sort order, unless it is a + * InvertibleComparator. + * + * @param index the index of the Comparator to replace + * @param comparator the Comparator to place at the given index + * + * @see InvertibleComparator + */ + @SuppressWarnings("unchecked") + public void setComparator(int index, Comparator comparator) { + if (comparator instanceof InvertibleComparator) { + this.comparators.set(index, (InvertibleComparator) comparator); + } else { + this.comparators.set(index, new InvertibleComparator(comparator)); + } + } + + /** + * Replace the Comparator at the given index using the given sort order. + * + * @param index the index of the Comparator to replace + * @param comparator the Comparator to place at the given index + * @param ascending the sort order: ascending (true) or descending (false) + */ + public void setComparator(int index, Comparator comparator, boolean ascending) { + this.comparators.set(index, new InvertibleComparator<>(comparator, ascending)); + } + + /** + * Invert the sort order of each sort definition contained by this compound comparator. + */ + public void invertOrder() { + for (InvertibleComparator comparator : this.comparators) { + comparator.invertOrder(); + } + } + + /** + * Invert the sort order of the sort definition at the specified index. + * + * @param index the index of the comparator to invert + */ + public void invertOrder(int index) { + this.comparators.get(index).invertOrder(); + } + + /** + * Change the sort order at the given index to ascending. + * + * @param index the index of the comparator to change + */ + public void setAscendingOrder(int index) { + this.comparators.get(index).setAscending(true); + } + + /** + * Change the sort order at the given index to descending sort. + * + * @param index the index of the comparator to change + */ + public void setDescendingOrder(int index) { + this.comparators.get(index).setAscending(false); + } + + /** + * Returns the number of aggregated comparators. + */ + public int getComparatorCount() { + return this.comparators.size(); + } + + @SuppressWarnings("unchecked") + @Override + public int compare(T o1, T o2) { + String message = "No sort definitions have been added to this CompoundComparator to compare"; + Assert.state(this.comparators.size() > 0, message); + for (InvertibleComparator comparator : this.comparators) { + int result = comparator.compare(o1, o2); + if (result != 0) { + return result; + } + } + return 0; + } + + @SuppressWarnings("unchecked") + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof CompoundComparator)) { + return false; + } + CompoundComparator other = (CompoundComparator) obj; + return this.comparators.equals(other.comparators); + } + + @Override + public int hashCode() { + return this.comparators.hashCode(); + } + + @Override + public String toString() { + return "CompoundComparator: " + this.comparators; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/yanzhenjie/andserver/util/comparator/InvertibleComparator.java b/api/src/main/java/com/yanzhenjie/andserver/util/comparator/InvertibleComparator.java new file mode 100644 index 0000000000000000000000000000000000000000..735056e9f829fb26f5b738705d4c9f7dff3fa0b3 --- /dev/null +++ b/api/src/main/java/com/yanzhenjie/andserver/util/comparator/InvertibleComparator.java @@ -0,0 +1,116 @@ +/* + * Copyright 2018 Zhenjie Yan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yanzhenjie.andserver.util.comparator; + +import com.yanzhenjie.andserver.util.Assert; + +import java.io.Serializable; +import java.util.Comparator; + +/** + * Created by Zhenjie Yan on 2018/7/11. + */ +public class InvertibleComparator implements Comparator, Serializable { + + private final Comparator comparator; + + private boolean ascending = true; + + /** + * Create an InvertibleComparator that sorts ascending by default. For the actual comparison, the specified + * Comparator will be used. + * + * @param comparator the comparator to decorate. + */ + public InvertibleComparator(Comparator comparator) { + Assert.notNull(comparator, "Comparator must not be null."); + this.comparator = comparator; + } + + /** + * Create an InvertibleComparator that sorts based on the provided order. For the actual comparison, the specified + * Comparator will be used. + * + * @param comparator the comparator to decorate. + * @param ascending the sort order: ascending (true) or descending (false). + */ + public InvertibleComparator(Comparator comparator, boolean ascending) { + Assert.notNull(comparator, "Comparator must not be null."); + this.comparator = comparator; + setAscending(ascending); + } + + /** + * Specify the sort order: ascending (true) or descending (false). + */ + public void setAscending(boolean ascending) { + this.ascending = ascending; + } + + /** + * Return the sort order: ascending (true) or descending (false). + */ + public boolean isAscending() { + return this.ascending; + } + + /** + * Invert the sort order: ascending -> descending or descending -> ascending. + */ + public void invertOrder() { + this.ascending = !this.ascending; + } + + @Override + public int compare(T o1, T o2) { + int result = this.comparator.compare(o1, o2); + if (result != 0) { + // Invert the order if it is a reverse sort. + if (!this.ascending) { + if (Integer.MIN_VALUE == result) { + result = Integer.MAX_VALUE; + } else { + result *= -1; + } + } + return result; + } + return 0; + } + + @SuppressWarnings("unchecked") + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof InvertibleComparator)) { + return false; + } + InvertibleComparator other = (InvertibleComparator) obj; + return (this.comparator.equals(other.comparator) && this.ascending == other.ascending); + } + + @Override + public int hashCode() { + return this.comparator.hashCode(); + } + + @Override + public String toString() { + return "InvertibleComparator: [" + this.comparator + "]; ascending=" + this.ascending; + } +} \ No newline at end of file diff --git a/api/src/main/resources/base/element/string.json b/api/src/main/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..18ff170a8b3f23686cf70c7ffd159a645ea95b6b --- /dev/null +++ b/api/src/main/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "api" + } + ] +} diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100755 index 188eced6598858c1aaab8a94ce25efb810a08bb8..0000000000000000000000000000000000000000 --- a/app/build.gradle +++ /dev/null @@ -1,42 +0,0 @@ -apply plugin: 'com.android.application' - -android { - compileSdkVersion 29 - buildToolsVersion "28.0.3" - - defaultConfig { - minSdkVersion 16 - targetSdkVersion 29 - versionCode 7 - versionName "1.3.0" - } - compileOptions { - sourceCompatibility rootProject.ext.sourceCompatibilityVersion - targetCompatibility rootProject.ext.targetCompatibilityVersion - } - dataBinding { - enabled = true - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } -} - -dependencies { - implementation fileTree(dir: 'libs', include: "*.jar") - implementation deps.supportAppCompat - implementation deps.supportDesign - implementation deps.butterknife - implementation deps.swipeRevealLayout - implementation deps.glide - annotationProcessor deps.glideCompiler - annotationProcessor deps.butterknifeCompiler - implementation(deps.rxbus) { - exclude group: 'com.jakewharton.timber', module: 'timber' - } - implementation deps.androidasync - implementation deps.timber -} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro deleted file mode 100755 index fedb955545c835d47ee04bf3664ca1121b89972c..0000000000000000000000000000000000000000 --- a/app/proguard-rules.pro +++ /dev/null @@ -1,47 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in D:\program\AndroidSdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} --keep public class * implements com.bumptech.glide.module.GlideModule --keep public class * extends com.bumptech.glide.module.AppGlideModule --keep public enum com.bumptech.glide.load.ImageHeaderParser$** { - **[] $VALUES; - public *; -} - --keep class com.growingio.android.sdk.** { - *; -} --dontwarn com.growingio.android.sdk.** --keepnames class * extends android.view.View --keep class * extends android.app.Fragment { - public void setUserVisibleHint(boolean); - public void onHiddenChanged(boolean); - public void onResume(); - public void onPause(); -} --keep class android.support.v4.app.Fragment { - public void setUserVisibleHint(boolean); - public void onHiddenChanged(boolean); - public void onResume(); - public void onPause(); -} --keep class * extends android.support.v4.app.Fragment { - public void setUserVisibleHint(boolean); - public void onHiddenChanged(boolean); - public void onResume(); - public void onPause(); -} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml deleted file mode 100755 index 9ddaeaecd922c6e876a76977db23464d6bdfa10d..0000000000000000000000000000000000000000 --- a/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png deleted file mode 100644 index 218cf9ffe42cac7ad60d71e7abce0a45c275d375..0000000000000000000000000000000000000000 Binary files a/app/src/main/ic_launcher-web.png and /dev/null differ diff --git a/app/src/main/java/me/pengtao/filetransfer/Constants.java b/app/src/main/java/me/pengtao/filetransfer/Constants.java deleted file mode 100755 index 04ccbda9bad369c0375792e0f0f0e723863da790..0000000000000000000000000000000000000000 --- a/app/src/main/java/me/pengtao/filetransfer/Constants.java +++ /dev/null @@ -1,20 +0,0 @@ -package me.pengtao.filetransfer; - -import android.os.Environment; - -import java.io.File; - -public class Constants { - public static final int HTTP_PORT = 12345; - public static final String DIR_IN_SDCARD = "FileTransfer"; - public static final int MSG_DIALOG_DISMISS = 0; - public static final File DIR = new File(Environment.getExternalStorageDirectory() + File - .separator + Constants.DIR_IN_SDCARD); - - public static final class RxBusEventType { - public static final String POPUP_MENU_DIALOG_SHOW_DISMISS = "POPUP MENU DIALOG SHOW " + - "DISMISS"; - public static final String WIFI_CONNECT_CHANGE_EVENT = "WIFI CONNECT CHANGE EVENT"; - public static final String LOAD_BOOK_LIST = "LOAD BOOK LIST"; - } -} diff --git a/app/src/main/java/me/pengtao/filetransfer/FileListAdapter.java b/app/src/main/java/me/pengtao/filetransfer/FileListAdapter.java deleted file mode 100644 index e5d5d61b9100c6184bebcbbf15189a851ba6539c..0000000000000000000000000000000000000000 --- a/app/src/main/java/me/pengtao/filetransfer/FileListAdapter.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (c) 2018 CPPAlien - * - * Licensed under the GNU GENERAL PUBLIC LICENSE, Version 3, 29 June 2007 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://github.com/CPPAlien/FileTransfer/blob/master/LICENSE - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package me.pengtao.filetransfer; - -import android.content.Context; -import android.content.Intent; -import android.databinding.DataBindingUtil; -import android.net.Uri; -import android.support.annotation.NonNull; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.bumptech.glide.Glide; -import com.chauthai.swipereveallayout.ViewBinderHelper; -import com.hwangjr.rxbus.RxBus; - -import java.io.File; -import java.util.List; - -import me.pengtao.filetransfer.databinding.LayoutFileItemBinding; -import me.pengtao.filetransfer.util.FileType; -import me.pengtao.filetransfer.util.FileUtils; - -/** - * @author CPPAlien - */ -class FileListAdapter extends RecyclerView.Adapter { - private Context mContext; - private List mFileModelList; - private final ViewBinderHelper viewBinderHelper = new ViewBinderHelper(); - - public FileListAdapter(Context context, List fileModelList) { - mContext = context; - mFileModelList = fileModelList; - viewBinderHelper.setOpenOnlyOne(true); - } - - @Override - public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - if (viewType == 1) { - View view = inflater.inflate(R.layout.empty_view, parent, false); - return new EmptyViewHolder(view); - } else { - return new MyViewHolder(LayoutInflater.from( - mContext).inflate(R.layout.layout_file_item, parent, - false)); - } - } - - @Override - public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) { - if (holder instanceof MyViewHolder) { - ((MyViewHolder) holder).onBindViewHolder(position); - } - } - - class EmptyViewHolder extends RecyclerView.ViewHolder { - public EmptyViewHolder(View itemView) { - super(itemView); - } - } - - @Override - public int getItemCount() { - return mFileModelList.size() > 0 ? mFileModelList.size() : 1; - } - - class MyViewHolder extends RecyclerView.ViewHolder { - LayoutFileItemBinding mBinding; - - public MyViewHolder(View view) { - super(view); - mBinding = DataBindingUtil.bind(view); - } - - public void onBindViewHolder(int position) { - FileModel infoModel = mFileModelList.get(position); - if (infoModel.getFileType() == FileType.TYPE_APK) { - mBinding.tvName.setText(mContext.getString(R.string.app_name_format, - infoModel.getName(), infoModel.getVersion())); - mBinding.tvDelete.setVisibility(infoModel.isInstalled() ? View.VISIBLE : View.GONE); - mBinding.tvDelete.setOnClickListener(v -> - delete(mContext, infoModel.getPackageName())); - } else { - mBinding.tvDelete.setVisibility(View.GONE); - mBinding.tvName.setText(infoModel.getName()); - } - viewBinderHelper.bind(mBinding.swipeRevealLayout, infoModel.getPath()); - - mBinding.tvSize.setText(infoModel.getSize()); - mBinding.tvPath.setText(infoModel.getPath()); - if (infoModel.getFileType() == FileType.TYPE_IMAGE) { - Glide.with(mContext).load(new File(infoModel.getPath())).into(mBinding.ivIcon); - } else { - mBinding.ivIcon.setImageDrawable(infoModel.getIcon()); - } - - mBinding.mainLayout.setOnClickListener(v -> - FileUtils.openFile(infoModel.getPath(), mContext)); - - mBinding.delete.setOnClickListener(view -> { - File file = new File(infoModel.getPath()); - if (file.exists() && file.isFile() && file.delete()) { - RxBus.get().post(Constants.RxBusEventType.LOAD_BOOK_LIST, 0); - } - }); - mBinding.share.setOnClickListener(view -> share(mContext, infoModel.getPath())); - mBinding.executePendingBindings(); - } - } - - @Override - public int getItemViewType(int position) { - if (mFileModelList.size() == 0) { - return 1; - } - return super.getItemViewType(position); - } - - private void delete(Context context, String packageName) { - Uri uri = Uri.fromParts("package", packageName, null); - Intent intent = new Intent(Intent.ACTION_DELETE, uri); - context.startActivity(intent); - } - - private void share(Context context, String filePath) { - Intent shareIntent = new Intent(); - shareIntent.setAction(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_STREAM, FileUtils.getFileUri(context, filePath)); - shareIntent.setType(FileUtils.getShareType(filePath)); - context.startActivity(Intent.createChooser(shareIntent, "")); - } -} \ No newline at end of file diff --git a/app/src/main/java/me/pengtao/filetransfer/FileModel.java b/app/src/main/java/me/pengtao/filetransfer/FileModel.java deleted file mode 100644 index 490546591a18fad512d67696571f47b1b5e9b392..0000000000000000000000000000000000000000 --- a/app/src/main/java/me/pengtao/filetransfer/FileModel.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (c) 2018 CPPAlien - * - * Licensed under the GNU GENERAL PUBLIC LICENSE, Version 3, 29 June 2007 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://github.com/CPPAlien/FileTransfer/blob/master/LICENSE - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package me.pengtao.filetransfer; - -import android.graphics.drawable.Drawable; - -/** - * @author CPPAlien - */ -public class FileModel { - private String path; - private String version; - private String size; - private String name; - private String packageName; - private int fileType; - private boolean installed; - private Drawable icon; - - public Drawable getIcon() { - return icon; - } - - public void setIcon(Drawable icon) { - this.icon = icon; - } - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - - public String getVersion() { - return version; - } - - public void setVersion(String version) { - this.version = version; - } - - public String getSize() { - return size; - } - - public void setSize(String size) { - this.size = size; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getPackageName() { - return packageName; - } - - public void setPackageName(String packageName) { - this.packageName = packageName; - } - - public boolean isInstalled() { - return installed; - } - - public void setInstalled(boolean installed) { - this.installed = installed; - } - - public int getFileType() { - return fileType; - } - - public FileModel setFileType(final int fileType) { - this.fileType = fileType; - return this; - } -} diff --git a/app/src/main/java/me/pengtao/filetransfer/MainActivity.java b/app/src/main/java/me/pengtao/filetransfer/MainActivity.java deleted file mode 100755 index 69bb7cd9580f6a3d3b6cc4774eb60e5b62b66651..0000000000000000000000000000000000000000 --- a/app/src/main/java/me/pengtao/filetransfer/MainActivity.java +++ /dev/null @@ -1,403 +0,0 @@ -package me.pengtao.filetransfer; - -import android.Manifest; -import android.animation.Animator; -import android.animation.ObjectAnimator; -import android.app.Activity; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.provider.Settings; -import android.support.design.widget.FloatingActionButton; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.ContextCompat; -import android.support.v4.widget.SwipeRefreshLayout; -import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.Toolbar; -import android.text.TextUtils; -import android.util.DisplayMetrics; -import android.view.Menu; -import android.view.View; -import android.view.animation.AccelerateInterpolator; -import android.widget.Toast; - -import com.hwangjr.rxbus.RxBus; -import com.hwangjr.rxbus.annotation.Subscribe; -import com.hwangjr.rxbus.annotation.Tag; -import com.hwangjr.rxbus.thread.EventThread; - -import java.io.File; -import java.io.IOException; -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.List; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; -import butterknife.Unbinder; -import me.pengtao.filetransfer.util.FileType; -import me.pengtao.filetransfer.util.FileUtils; -import timber.log.Timber; - -/** - * @author chris - */ -public class MainActivity extends AppCompatActivity implements Animator.AnimatorListener { - private static final int WRITE_PERMISSION_CODE = 1; - private static final int FILE_FETCH_CODE = 2; - private String mAlreadyWrited = ""; - Unbinder mUnbinder; - @BindView(R.id.toolbar) - Toolbar mToolbar; - @BindView(R.id.fab) - FloatingActionButton mFab; - @BindView(R.id.recyclerview) - RecyclerView mAppList; - @BindView(R.id.content_main) - SwipeRefreshLayout mSwipeRefreshLayout; - List mFileModelList = new ArrayList<>(); - FileListAdapter mAppshelfAdapter; - - public synchronized static Drawable getIconFromPackageName(String packageName, Context - context) { - PackageManager pm = context.getPackageManager(); - try { - PackageInfo pi = pm.getPackageInfo(packageName, 0); - Context otherAppCtx = context.createPackageContext(packageName, Context - .CONTEXT_IGNORE_SECURITY); - List displayMetrics = new ArrayList<>(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - displayMetrics.add(DisplayMetrics.DENSITY_XXXHIGH); - } - displayMetrics.add(DisplayMetrics.DENSITY_XXHIGH); - displayMetrics.add(DisplayMetrics.DENSITY_XHIGH); - displayMetrics.add(DisplayMetrics.DENSITY_HIGH); - displayMetrics.add(DisplayMetrics.DENSITY_TV); - for (int displayMetric : displayMetrics) { - try { - Drawable d = otherAppCtx.getResources().getDrawableForDensity(pi - .applicationInfo.icon, displayMetric); - if (d != null) { - return d; - } - } catch (Resources.NotFoundException e) { - // ignore - } - } - } catch (Exception e) { - // Handle Error here - } - ApplicationInfo appInfo; - try { - appInfo = pm.getApplicationInfo(packageName, 0); - } catch (PackageManager.NameNotFoundException e) { - return null; - } - return appInfo.loadIcon(pm); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - mUnbinder = ButterKnife.bind(this); - setSupportActionBar(mToolbar); - - mToolbar.setOnMenuItemClickListener(item -> { - switch (item.getItemId()) { - case R.id.delete_all: - if (!mFileModelList.isEmpty()) { - showDialog(); - } - break; - case R.id.add_files: - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_GET_CONTENT); - intent.setType("*/*"); - intent.addCategory(Intent.CATEGORY_OPENABLE); - startActivityForResult(intent, FILE_FETCH_CODE); - break; - default: - break; - } - return false; - }); - Timber.plant(new Timber.DebugTree()); - RxBus.get().register(this); - initRecyclerView(); - ActivityCompat.requestPermissions(this, - new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_PERMISSION_CODE); - } - - @Override - protected void onResume() { - super.onResume(); - /*ClipboardManager clipboard = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE); - if (clipboard != null && clipboard.hasPrimaryClip() && clipboard.getPrimaryClip() != null) { - ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0); - if (item != null && item.getText() != null && item.getText().length() > 0 && !item.getText().equals(mAlreadyWrited)) { - File file = new File(Constants.DIR, "clipboard_" + String.valueOf(System.currentTimeMillis()) + ".txt"); - try { - FileUtils.writeByteArrayToFile(file, item.getText().toString().getBytes(), false); - Toast.makeText(this, "已把剪切板中内容写入到该文件中", Toast.LENGTH_SHORT).show(); - RxBus.get().post(Constants.RxBusEventType.LOAD_BOOK_LIST, 0); - mAlreadyWrited = item.getText().toString(); - } catch (IOException e) { - e.printStackTrace(); - Toast.makeText(this, "文件写入失败", Toast.LENGTH_SHORT).show(); - } - } - }*/ - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.item_menu, menu); - return true; - } - - @OnClick(R.id.fab) - public void onClick(View view) { - if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) - == PackageManager.PERMISSION_GRANTED) { - ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(mFab, "translationY", 0, mFab - .getHeight() * 2).setDuration(200L); - objectAnimator.setInterpolator(new AccelerateInterpolator()); - objectAnimator.addListener(this); - objectAnimator.start(); - } else { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.permission_setting); - builder.setMessage(R.string.permission_need_des); - builder.setPositiveButton(R.string.permission_go, (dialog, which) -> { - Intent intent = new Intent(); - intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - Uri uri = Uri.fromParts("package", getPackageName(), null); - intent.setData(uri); - startActivity(intent); - }); - builder.setNegativeButton(R.string.cancel, null); - builder.show(); - } - } - - @Override - protected void onDestroy() { - super.onDestroy(); - if (mUnbinder != null) { - mUnbinder.unbind(); - } - RxBus.get().unregister(this); - } - - @SuppressWarnings("unused") - @Subscribe(tags = {@Tag(Constants.RxBusEventType.POPUP_MENU_DIALOG_SHOW_DISMISS)}) - public void onPopupMenuDialogDismiss(Integer type) { - if (type == Constants.MSG_DIALOG_DISMISS) { - ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(mFab, "translationY", mFab - .getHeight() * 2, 0).setDuration(200L); - objectAnimator.setInterpolator(new AccelerateInterpolator()); - objectAnimator.start(); - } - } - - private void showDialog() { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.sure_delete_all); - builder.setPositiveButton(R.string.ok, (dialog, which) -> deleteAll()); - builder.setNegativeButton(R.string.cancel, null); - builder.show(); - } - - @Override - public void onAnimationStart(Animator animation) { - new PopupMenuDialog(this).builder().setCancelable(false) - .setCanceledOnTouchOutside(true).show(); - } - - @Override - public void onAnimationEnd(Animator animation) { - } - - @Override - public void onAnimationCancel(Animator animation) { - } - - @Override - public void onAnimationRepeat(Animator animation) { - } - - void initRecyclerView() { - mAppshelfAdapter = new FileListAdapter(this, mFileModelList); - mAppList.setHasFixedSize(true); - mAppList.setLayoutManager(new LinearLayoutManager(this)); - mAppList.setAdapter(mAppshelfAdapter); - RxBus.get().post(Constants.RxBusEventType.LOAD_BOOK_LIST, 0); - - mSwipeRefreshLayout.setColorSchemeResources( - android.R.color.holo_blue_bright, - android.R.color.holo_green_light, - android.R.color.holo_orange_light, - android.R.color.holo_red_light); - mSwipeRefreshLayout.setOnRefreshListener(() -> - RxBus.get().post(Constants.RxBusEventType.LOAD_BOOK_LIST, 0)); - } - - private void handleFiles(String path, long length) { - FileModel fileModel = new FileModel(); - PackageManager pm = getPackageManager(); - PackageInfo info = pm.getPackageArchiveInfo(path, 0); - - if (info != null) { - ApplicationInfo appInfo = info.applicationInfo; - appInfo.sourceDir = path; - appInfo.publicSourceDir = path; - String packageName = appInfo.packageName; - String version = info.versionName; - Drawable icon = pm.getApplicationIcon(appInfo); - String appName = pm.getApplicationLabel(appInfo).toString(); - if (TextUtils.isEmpty(appName)) { - appName = getApplicationName(packageName); - } - if (icon == null) { - icon = getIconFromPackageName(packageName, this); - } - fileModel.setName(appName); - fileModel.setPackageName(packageName); - fileModel.setPath(path); - fileModel.setSize(getFileSize(length)); - fileModel.setVersion(version); - fileModel.setIcon(icon); - fileModel.setFileType(FileType.TYPE_APK); - fileModel.setInstalled(isAvailable(this, packageName)); - mFileModelList.add(fileModel); - } else { - fileModel.setFileType(FileUtils.getFileType(path)); - fileModel.setPath(path); - String[] pathItems = path.split(File.separator); - fileModel.setName(pathItems[pathItems.length - 1]); - fileModel.setSize(getFileSize(length)); - Drawable icon = ContextCompat.getDrawable(this, FileUtils.getFileTypeIcon(path)); - fileModel.setIcon(icon); - mFileModelList.add(fileModel); - } - } - - private String getFileSize(long length) { - DecimalFormat df = new DecimalFormat("######0.0"); - if (length < 1024.f) { - return (int) length + "B"; - } else if (length < 1024 * 1024.f) { - return df.format(length / 1024.f) + "K"; - } else if (length < 1024 * 1024 * 1024.f) { - return df.format((length / 1024.f / 1024.f)) + "M"; - } - return df.format(length / 1024.f / 1024.f / 1024.f) + "G"; - } - - @SuppressWarnings("unused") - @Subscribe(thread = EventThread.IO, tags = {@Tag(Constants.RxBusEventType.LOAD_BOOK_LIST)}) - public void loadFileList(Integer type) { - File dir = Constants.DIR; - if (dir.exists() && dir.isDirectory()) { - File[] files = dir.listFiles(); - if (files == null) { - return; - } - - FileUtils.sortWithLastModified(files); - mFileModelList.clear(); - for (File file : files) { - handleFiles(file.getAbsolutePath(), file.length()); - } - } - runOnUiThread(() -> { - mSwipeRefreshLayout.setRefreshing(false); - mAppshelfAdapter.notifyDataSetChanged(); - }); - } - - public String getApplicationName(String packageName) { - PackageManager packageManager = null; - ApplicationInfo applicationInfo; - try { - packageManager = getApplicationContext().getPackageManager(); - applicationInfo = packageManager.getApplicationInfo(packageName, 0); - } catch (PackageManager.NameNotFoundException e) { - applicationInfo = null; - } - if (applicationInfo != null) { - return (String) packageManager.getApplicationLabel(applicationInfo); - } - return packageName; - } - - /** - * 判断相对应的APP是否存在 - * - * @param context context - * @param packageName(包名)(若想判断QQ,则改为com.tencent.mobileqq,若想判断微信,则改为com.tencent.mm) - * @return - */ - public boolean isAvailable(Context context, String packageName) { - PackageManager packageManager = context.getPackageManager(); - - //获取手机系统的所有APP包名,然后进行一一比较 - List pinfo = packageManager.getInstalledPackages(0); - for (int i = 0; i < pinfo.size(); i++) { - if ((pinfo.get(i)).packageName - .equalsIgnoreCase(packageName)) { - return true; - } - } - return false; - } - - //删除所有文件 - private void deleteAll() { - File dir = Constants.DIR; - if (dir.exists() && dir.isDirectory()) { - File[] fileNames = dir.listFiles(); - if (fileNames != null) { - for (File fileName : fileNames) { - fileName.delete(); - } - } - } - RxBus.get().post(Constants.RxBusEventType.LOAD_BOOK_LIST, 0); - } - - @Override - protected void onActivityResult(final int requestCode, final int resultCode, final Intent - data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == FILE_FETCH_CODE && resultCode == Activity.RESULT_OK) { - Uri uri = data.getData(); - if (uri != null) { - try { - ContentResolver content = getContentResolver(); - FileUtils.copyFile(content.openInputStream(data.getData()), Constants.DIR - + File.separator + FileUtils.getFileName(this, uri)); - Toast.makeText(this, R.string.please_refresh_web, Toast.LENGTH_LONG).show(); - RxBus.get().post(Constants.RxBusEventType.LOAD_BOOK_LIST, 0); - } catch (IOException e) { - Toast.makeText(this, R.string.read_file_failed, Toast.LENGTH_SHORT).show(); - } - } else { - Toast.makeText(this, R.string.read_file_failed, Toast.LENGTH_SHORT).show(); - } - } - } -} diff --git a/app/src/main/java/me/pengtao/filetransfer/PopupMenuDialog.java b/app/src/main/java/me/pengtao/filetransfer/PopupMenuDialog.java deleted file mode 100755 index 79a31652dc835709f890906fc62a2252af23169c..0000000000000000000000000000000000000000 --- a/app/src/main/java/me/pengtao/filetransfer/PopupMenuDialog.java +++ /dev/null @@ -1,135 +0,0 @@ -package me.pengtao.filetransfer; - -import android.app.Dialog; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.provider.Settings; -import android.view.Display; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; - -import com.hwangjr.rxbus.RxBus; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; -import butterknife.Unbinder; -import me.pengtao.filetransfer.util.WifiUtils; - -/** - * @author chris - */ -public class PopupMenuDialog { - private Unbinder mUnbinder; - @BindView(R.id.popup_menu_title) - TextView mTxtTitle; - @BindView(R.id.popup_menu_subtitle) - TextView mTxtSubTitle; - @BindView(R.id.shared_wifi_state) - ImageView mImgLanState; - @BindView(R.id.shared_wifi_state_hint) - TextView mTxtStateHint; - @BindView(R.id.shared_wifi_address) - TextView mTxtAddress; - @BindView(R.id.shared_wifi_settings) - Button mBtnWifiSettings; - @BindView(R.id.shared_wifi_button_split_line) - View mButtonSplitLine; - private Context context; - private Dialog dialog; - private Display display; - - public PopupMenuDialog(Context context) { - this.context = context; - WindowManager windowManager = (WindowManager) context - .getSystemService(Context.WINDOW_SERVICE); - display = windowManager.getDefaultDisplay(); - RxBus.get().register(this); - } - - public PopupMenuDialog builder() { - View view = LayoutInflater.from(context).inflate( - R.layout.layout_popup_menu_dialog, null); - - view.setMinimumWidth(display.getWidth()); - - dialog = new Dialog(context, R.style.PopupMenuDialogStyle); - dialog.setContentView(view); - mUnbinder = ButterKnife.bind(this, dialog); - dialog.setOnDismissListener(this::onDialogDismiss); - - Window dialogWindow = dialog.getWindow(); - dialogWindow.setGravity(Gravity.LEFT | Gravity.BOTTOM); - WindowManager.LayoutParams lp = dialogWindow.getAttributes(); - lp.x = 0; - lp.y = 0; - dialogWindow.setAttributes(lp); - - return this; - } - - public PopupMenuDialog setCancelable(boolean cancel) { - dialog.setCancelable(cancel); - return this; - } - - public PopupMenuDialog setCanceledOnTouchOutside(boolean cancel) { - dialog.setCanceledOnTouchOutside(cancel); - return this; - } - - public void show() { - dialog.show(); - String ip = WifiUtils.getDeviceIpAddress(); - onWifiConnected(ip); - WebService.start(context); - } - - @OnClick({R.id.shared_wifi_cancel, R.id.shared_wifi_settings}) - public void onClick(View view) { - switch (view.getId()) { - case R.id.shared_wifi_cancel: - dialog.dismiss(); - break; - case R.id.shared_wifi_settings: - context.startActivity(new Intent(Settings.ACTION_WIFI_SETTINGS)); - break; - default: - break; - } - } - - void onWifiConnected(String ipAddr) { - mTxtTitle.setText(R.string.wlan_enabled); - mTxtTitle.setTextColor(context.getResources().getColor(R.color.colorWifiConnected)); - mImgLanState.setImageResource(R.drawable.shared_wifi_enable); - mTxtStateHint.setText(R.string.pls_input_the_following_address_in_pc_browser); - String address = String.format(context.getString(R.string.http_address), ipAddr, Constants.HTTP_PORT); - mTxtAddress.setText(address); - mBtnWifiSettings.setOnClickListener(v -> { - ClipboardManager cm = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - ClipData mClipData = ClipData.newPlainText("Label", address); - cm.setPrimaryClip(mClipData); - Toast.makeText(context, context.getString(R.string.copy_toast), Toast.LENGTH_LONG).show(); - }); - } - - void onDialogDismiss(DialogInterface dialog) { - if (mUnbinder != null) { - mUnbinder.unbind(); - RxBus.get().post(Constants.RxBusEventType.POPUP_MENU_DIALOG_SHOW_DISMISS, Constants.MSG_DIALOG_DISMISS); - //unregisterWifiConnectChangedReceiver(); - RxBus.get().unregister(PopupMenuDialog.this); - } - } -} diff --git a/app/src/main/java/me/pengtao/filetransfer/WebService.java b/app/src/main/java/me/pengtao/filetransfer/WebService.java deleted file mode 100755 index f0e65bfb32970b0955390daafc31cc82dfbc2fae..0000000000000000000000000000000000000000 --- a/app/src/main/java/me/pengtao/filetransfer/WebService.java +++ /dev/null @@ -1,367 +0,0 @@ -package me.pengtao.filetransfer; - -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.IBinder; -import android.text.TextUtils; - -import com.hwangjr.rxbus.RxBus; -import com.koushikdutta.async.AsyncServer; -import com.koushikdutta.async.ByteBufferList; -import com.koushikdutta.async.DataEmitter; -import com.koushikdutta.async.http.body.MultipartFormDataBody; -import com.koushikdutta.async.http.body.Part; -import com.koushikdutta.async.http.body.UrlEncodedFormBody; -import com.koushikdutta.async.http.server.AsyncHttpServer; -import com.koushikdutta.async.http.server.AsyncHttpServerRequest; -import com.koushikdutta.async.http.server.AsyncHttpServerResponse; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.net.URLEncoder; -import java.text.DecimalFormat; - -import timber.log.Timber; - -public class WebService extends Service { - static final String ACTION_START_WEB_SERVICE = "me.pengtao.filetransfer.action" + - ".START_WEB_SERVICE"; - static final String ACTION_STOP_WEB_SERVICE = "me.pengtao.filetransfer.action" + - ".STOP_WEB_SERVICE"; - private static final String TEXT_CONTENT_TYPE = "text/html;charset=utf-8"; - private static final String CSS_CONTENT_TYPE = "text/css;charset=utf-8"; - private static final String BINARY_CONTENT_TYPE = "application/octet-stream"; - private static final String JS_CONTENT_TYPE = "application/javascript"; - private static final String PNG_CONTENT_TYPE = "application/x-png"; - private static final String JPG_CONTENT_TYPE = "application/jpeg"; - private static final String SWF_CONTENT_TYPE = "application/x-shockwave-flash"; - private static final String WOFF_CONTENT_TYPE = "application/x-font-woff"; - private static final String TTF_CONTENT_TYPE = "application/x-font-truetype"; - private static final String SVG_CONTENT_TYPE = "image/svg+xml"; - private static final String EOT_CONTENT_TYPE = "image/vnd.ms-fontobject"; - private static final String MP3_CONTENT_TYPE = "audio/mp3"; - private static final String MP4_CONTENT_TYPE = "video/mpeg4"; - FileUploadHolder fileUploadHolder = new FileUploadHolder(); - private AsyncHttpServer server = new AsyncHttpServer(); - private AsyncServer mAsyncServer = new AsyncServer(); - - public static void start(Context context) { - Intent intent = new Intent(context, WebService.class); - intent.setAction(ACTION_START_WEB_SERVICE); - context.startService(intent); - } - - public static void stop(Context context) { - Intent intent = new Intent(context, WebService.class); - intent.setAction(ACTION_STOP_WEB_SERVICE); - context.startService(intent); - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (intent != null) { - String action = intent.getAction(); - if (ACTION_START_WEB_SERVICE.equals(action)) { - startServer(); - } else if (ACTION_STOP_WEB_SERVICE.equals(action)) { - stopSelf(); - } - } - return super.onStartCommand(intent, flags, startId); - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (server != null) { - server.stop(); - } - if (mAsyncServer != null) { - mAsyncServer.stop(); - } - } - - private void startServer() { - server.get("/images/.*", this::sendResources); - server.get("/scripts/.*", this::sendResources); - server.get("/css/.*", this::sendResources); - //index page - server.get("/", (AsyncHttpServerRequest request, AsyncHttpServerResponse response) -> { - try { - response.send(getIndexContent()); - } catch (IOException e) { - e.printStackTrace(); - response.code(500).end(); - } - }); - //query upload list - server.get("/files", (AsyncHttpServerRequest request, AsyncHttpServerResponse response) -> { - JSONArray array = new JSONArray(); - File dir = Constants.DIR; - if (dir.exists() && dir.isDirectory()) { - String[] fileNames = dir.list(); - if (fileNames != null) { - for (String fileName : fileNames) { - File file = new File(dir, fileName); - if (file.exists() && file.isFile()) { - try { - JSONObject jsonObject = new JSONObject(); - jsonObject.put("name", fileName); - long fileLen = file.length(); - DecimalFormat df = new DecimalFormat("0.00"); - if (fileLen > 1024 * 1024) { - jsonObject.put("size", df.format(fileLen * 1f / 1024 / 1024) - + "MB"); - } else if (fileLen > 1024) { - jsonObject.put("size", df.format(fileLen * 1f / 1024) + "KB"); - } else { - jsonObject.put("size", fileLen + "B"); - } - array.put(jsonObject); - } catch (JSONException e) { - e.printStackTrace(); - } - } - } - } - } - response.send(array.toString()); - }); - //delete - server.post("/files/.*", (AsyncHttpServerRequest request, AsyncHttpServerResponse - response) -> { - final UrlEncodedFormBody body = (UrlEncodedFormBody) request.getBody(); - if ("delete".equalsIgnoreCase(body.get().getString("_method"))) { - String path = request.getPath().replace("/files/", ""); - try { - path = URLDecoder.decode(path, "utf-8"); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - File file = new File(Constants.DIR, path); - if (file.exists() && file.isFile() && file.delete()) { - RxBus.get().post(Constants.RxBusEventType.LOAD_BOOK_LIST, 0); - } - } - response.end(); - }); - //download - server.get("/files/.*", (AsyncHttpServerRequest request, AsyncHttpServerResponse - response) -> { - String path = request.getPath().replace("/files/", ""); - try { - path = URLDecoder.decode(path, "utf-8"); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - File file = new File(Constants.DIR, path); - if (file.exists() && file.isFile()) { - try { - response.getHeaders().add("Content-Disposition", "attachment;filename=" + - URLEncoder.encode(file.getName(), "utf-8")); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - response.sendFile(file); - return; - } - response.code(404).send("Not found!"); - }); - //upload - server.post("/files", (AsyncHttpServerRequest request, AsyncHttpServerResponse response) - -> { - final MultipartFormDataBody body = (MultipartFormDataBody) request.getBody(); - body.setMultipartCallback((Part part) -> { - if (part.isFile()) { - body.setDataCallback((DataEmitter emitter, ByteBufferList bb) -> { - fileUploadHolder.write(bb.getAllByteArray()); - bb.recycle(); - }); - } else { - if (body.getDataCallback() == null) { - body.setDataCallback((DataEmitter emitter, ByteBufferList bb) -> { - try { - String fileName = URLDecoder.decode(new String(bb - .getAllByteArray()), "UTF-8"); - fileUploadHolder.setFileName(fileName); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - bb.recycle(); - }); - } - } - }); - request.setEndCallback((Exception e) -> { - fileUploadHolder.reset(); - response.end(); - RxBus.get().post(Constants.RxBusEventType.LOAD_BOOK_LIST, 0); - }); - } - ); - server.get("/progress/.*", (final AsyncHttpServerRequest request, - final AsyncHttpServerResponse response) -> { - JSONObject res = new JSONObject(); - - String path = request.getPath().replace("/progress/", ""); - - if (path.equals(fileUploadHolder.fileName)) { - try { - res.put("fileName", fileUploadHolder.fileName); - res.put("size", fileUploadHolder.totalSize); - res.put("progress", fileUploadHolder.fileOutPutStream == null ? 1 : - 0.1); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - response.send(res); - } - - ); - server.listen(mAsyncServer, Constants.HTTP_PORT); - } - - private String getIndexContent() throws IOException { - BufferedInputStream bInputStream = null; - try { - bInputStream = new BufferedInputStream(getAssets().open("wifi/index.html")); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - int len = 0; - byte[] tmp = new byte[10240]; - while ((len = bInputStream.read(tmp)) > 0) { - baos.write(tmp, 0, len); - } - return new String(baos.toByteArray(), "utf-8"); - } catch (IOException e) { - e.printStackTrace(); - throw e; - } finally { - if (bInputStream != null) { - try { - bInputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - } - - private void sendResources(final AsyncHttpServerRequest request, final - AsyncHttpServerResponse response) { - try { - String fullPath = request.getPath(); - fullPath = fullPath.replace("%20", " "); - String resourceName = fullPath; - if (resourceName.startsWith("/")) { - resourceName = resourceName.substring(1); - } - if (resourceName.indexOf("?") > 0) { - resourceName = resourceName.substring(0, resourceName.indexOf("?")); - } - if (!TextUtils.isEmpty(getContentTypeByResourceName(resourceName))) { - response.setContentType(getContentTypeByResourceName(resourceName)); - } - BufferedInputStream bInputStream = new BufferedInputStream(getAssets().open("wifi/" + - resourceName)); - response.sendStream(bInputStream, bInputStream.available()); - } catch (IOException e) { - e.printStackTrace(); - response.code(404).end(); - return; - } - } - - private String getContentTypeByResourceName(String resourceName) { - if (resourceName.endsWith(".css")) { - return CSS_CONTENT_TYPE; - } else if (resourceName.endsWith(".js")) { - return JS_CONTENT_TYPE; - } else if (resourceName.endsWith(".swf")) { - return SWF_CONTENT_TYPE; - } else if (resourceName.endsWith(".png")) { - return PNG_CONTENT_TYPE; - } else if (resourceName.endsWith(".jpg") || resourceName.endsWith(".jpeg")) { - return JPG_CONTENT_TYPE; - } else if (resourceName.endsWith(".woff")) { - return WOFF_CONTENT_TYPE; - } else if (resourceName.endsWith(".ttf")) { - return TTF_CONTENT_TYPE; - } else if (resourceName.endsWith(".svg")) { - return SVG_CONTENT_TYPE; - } else if (resourceName.endsWith(".eot")) { - return EOT_CONTENT_TYPE; - } else if (resourceName.endsWith(".mp3")) { - return MP3_CONTENT_TYPE; - } else if (resourceName.endsWith(".mp4")) { - return MP4_CONTENT_TYPE; - } - return ""; - } - - public class FileUploadHolder { - private String fileName; - private File recievedFile; - private BufferedOutputStream fileOutPutStream; - private long totalSize; - - public BufferedOutputStream getFileOutPutStream() { - return fileOutPutStream; - } - - public void setFileName(String fileName) { - this.fileName = fileName; - totalSize = 0; - if (!Constants.DIR.exists()) { - Constants.DIR.mkdirs(); - } - this.recievedFile = new File(Constants.DIR, this.fileName); - Timber.d(recievedFile.getAbsolutePath()); - try { - fileOutPutStream = new BufferedOutputStream(new FileOutputStream(recievedFile)); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } - } - - public void reset() { - if (fileOutPutStream != null) { - try { - fileOutPutStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - fileOutPutStream = null; - } - - public void write(byte[] data) { - if (fileOutPutStream != null) { - try { - fileOutPutStream.write(data); - } catch (IOException e) { - e.printStackTrace(); - } - } - totalSize += data.length; - } - } -} diff --git a/app/src/main/java/me/pengtao/filetransfer/util/FileType.java b/app/src/main/java/me/pengtao/filetransfer/util/FileType.java deleted file mode 100644 index 8316a0c5200e291fcb8173019203f2623d8ebca9..0000000000000000000000000000000000000000 --- a/app/src/main/java/me/pengtao/filetransfer/util/FileType.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2018 CPPAlien - * - * Licensed under the GNU GENERAL PUBLIC LICENSE, Version 3, 29 June 2007 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://github.com/CPPAlien/FileTransfer/blob/master/LICENSE - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package me.pengtao.filetransfer.util; - -/** - * @author CPPAlien - */ -public class FileType { - public static final String FileTypes[][] = { - // File Image - {".png", ".jpg", ".jpeg", ".gif", ".bmp"}, - // File Audio - {".mp3", ".wav", ".ogg", "midi"}, - // File Video - {".mp4", ".rmvb", ".avi", ".flv", ".3gp"}, - // File Web Text - {".jsp", ".html", ".htm", ".js", ".php"}, - // File Text - {".txt", ".c", ".cpp", ".xml", ".py", ".json", ".log"}, - // File Excel - {".xls", ".xlsx"}, - // File Word - {".doc", ".docx"}, - // File PPT - {".ppt", ".pptx"}, - // File PDF - {".pdf"}, - // File Package - {".jar", ".zip", ".rar", ".gz"}, - // APK - {".apk"}}; - public static final int TYPE_IMAGE = 100; - public static final int TYPE_AUDIO = 200; - public static final int TYPE_VIDEO = 300; - public static final int TYPE_WEB = 400; - public static final int TYPE_TEXT = 500; - public static final int TYPE_EXCEL = 600; - public static final int TYPE_WORD = 700; - public static final int TYPE_PPT = 800; - public static final int TYPE_PDF = 900; - public static final int TYPE_PACKAGE = 1000; - public static final int TYPE_APK = 1100; - public static final int TYPE_UNKNOWN = -1; - public static final int TypeStart[] = {TYPE_IMAGE, TYPE_AUDIO, TYPE_VIDEO, - TYPE_WEB, TYPE_TEXT, TYPE_EXCEL, TYPE_WORD, TYPE_PPT, TYPE_PDF, TYPE_PACKAGE, TYPE_APK}; -} diff --git a/app/src/main/java/me/pengtao/filetransfer/util/FileUtils.java b/app/src/main/java/me/pengtao/filetransfer/util/FileUtils.java deleted file mode 100644 index 3e1e1e1a2c6885ddf7ffd8668f899272ddf6cc2f..0000000000000000000000000000000000000000 --- a/app/src/main/java/me/pengtao/filetransfer/util/FileUtils.java +++ /dev/null @@ -1,356 +0,0 @@ -/* - * Copyright (c) 2018 CPPAlien - * - * Licensed under the GNU GENERAL PUBLIC LICENSE, Version 3, 29 June 2007 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://github.com/CPPAlien/FileTransfer/blob/master/LICENSE - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package me.pengtao.filetransfer.util; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.os.Build; -import android.provider.OpenableColumns; -import android.support.v4.content.FileProvider; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Arrays; - -import me.pengtao.filetransfer.R; - -/** - * @author CPPAlien - */ -public class FileUtils { - /** - * 获得文件的 uri - * - * @param context context - * @param filePath 文件路径 - * @return uri - */ - public static Uri getFileUri(Context context, String filePath) { - File file = new File(filePath); - return FileProvider.getUriForFile(context, context.getPackageName() + "" + - ".fileprovider", file); - } - - public static boolean openFile(String filePath, Context context) { - int fileType = getFileType(filePath); - File file = new File(filePath); - - if (file.isFile()) { - Intent intent = null; - Uri contentUri = getFileUri(context, filePath); - switch (fileType) { - case FileType.TYPE_IMAGE: - intent = getImageFileIntent(contentUri); - break; - case FileType.TYPE_AUDIO: - intent = getAudioFileIntent(contentUri); - break; - case FileType.TYPE_VIDEO: - intent = getVideoFileIntent(contentUri); - break; - case FileType.TYPE_WEB: - intent = getHtmlFileIntent(contentUri); - break; - case FileType.TYPE_TEXT: - intent = getTextFileIntent(contentUri); - break; - case FileType.TYPE_EXCEL: - intent = getExcelFileIntent(contentUri); - break; - case FileType.TYPE_WORD: - intent = getWordFileIntent(contentUri); - break; - case FileType.TYPE_PPT: - intent = getPPTFileIntent(contentUri); - break; - case FileType.TYPE_PDF: - intent = getPdfFileIntent(contentUri); - break; - case FileType.TYPE_PACKAGE: - case FileType.TYPE_APK: - intent = getApkFileIntent(context, file); - break; - default: - new AlertDialog.Builder(context) - .setMessage(R.string.no_program_open_it) - .setPositiveButton(R.string.ok, null) - .show(); - break; - } - if (intent != null) { - intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - context.startActivity(intent); - return true; - } - } - return false; - } - - public static int getFileType(String filePath) { - File file = new File(filePath); - String fileName = file.getName(); - - for (int i = 0; i < FileType.FileTypes.length; i++) { - int j = checkStringEnds(fileName, FileType.FileTypes[i]); - if (j == -1) { - continue; - } - return FileType.TypeStart[i]; - } - return FileType.TYPE_UNKNOWN; - } - - private static int checkStringEnds(String item, String[] array) { - for (int i = 0; i < array.length; i++) { - if (item.toLowerCase().endsWith(array[i])) { - return i; - } - } - - return -1; - } - - public static int getFileTypeIcon(String path) { - int fileType = getFileType(path); - switch (fileType) { - case FileType.TYPE_IMAGE: - return R.drawable.image; - case FileType.TYPE_AUDIO: - return R.drawable.audio; - case FileType.TYPE_VIDEO: - return R.drawable.vedio; - case FileType.TYPE_WEB: - return R.drawable.web; - case FileType.TYPE_TEXT: - return R.drawable.text; - case FileType.TYPE_EXCEL: - return R.drawable.excel; - case FileType.TYPE_WORD: - return R.drawable.doc; - case FileType.TYPE_PPT: - return R.drawable.ppt; - case FileType.TYPE_PDF: - return R.drawable.pdf; - case FileType.TYPE_PACKAGE: - return R.drawable.zip; - default: - return R.drawable.other_file; - } - } - - public static String getShareType(String path) { - int fileType = getFileType(path); - switch (fileType) { - case FileType.TYPE_IMAGE: - return "image/*"; - case FileType.TYPE_AUDIO: - return "audio/*"; - case FileType.TYPE_VIDEO: - return "video/*"; - case FileType.TYPE_WEB: - case FileType.TYPE_TEXT: - return "text/*"; - case FileType.TYPE_EXCEL: - case FileType.TYPE_WORD: - case FileType.TYPE_PPT: - case FileType.TYPE_PDF: - case FileType.TYPE_PACKAGE: - case FileType.TYPE_APK: - return "application/*"; - default: - return "*/*"; - } - } - - public static void copyFile(InputStream in, String targetLocation) throws IOException { - OutputStream out = new FileOutputStream(targetLocation); - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); - } - in.close(); - out.close(); - } - - public static String getFileName(Context context, Uri uri) { - String result = null; - if (uri.getScheme().equals("content")) { - try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); - } - } - } - if (result == null) { - result = uri.getPath(); - int cut = result.lastIndexOf('/'); - if (cut != -1) { - result = result.substring(cut + 1); - } - } - return result; - } - - private static Intent getHtmlFileIntent(Uri uri) { - Intent intent = new Intent("android.intent.action.VIEW"); - intent.setDataAndType(uri, "text/html"); - return intent; - } - - private static Intent getImageFileIntent(Uri uri) { - Intent intent = new Intent("android.intent.action.VIEW"); - intent.addCategory("android.intent.category.DEFAULT"); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setDataAndType(uri, "image/*"); - return intent; - } - - private static Intent getPdfFileIntent(Uri uri) { - Intent intent = new Intent("android.intent.action.VIEW"); - intent.addCategory("android.intent.category.DEFAULT"); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setDataAndType(uri, "application/pdf"); - return intent; - } - - private static Intent getTextFileIntent(Uri uri) { - Intent intent = new Intent("android.intent.action.VIEW"); - intent.addCategory("android.intent.category.DEFAULT"); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setDataAndType(uri, "text/plain"); - return intent; - } - - private static Intent getAudioFileIntent(Uri uri) { - Intent intent = new Intent("android.intent.action.VIEW"); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.putExtra("oneshot", 0); - intent.putExtra("configchange", 0); - intent.setDataAndType(uri, "audio/*"); - return intent; - } - - private static Intent getVideoFileIntent(Uri uri) { - Intent intent = new Intent("android.intent.action.VIEW"); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.putExtra("oneshot", 0); - intent.putExtra("configchange", 0); - intent.setDataAndType(uri, "video/*"); - return intent; - } - - private static Intent getWordFileIntent(Uri uri) { - Intent intent = new Intent("android.intent.action.VIEW"); - intent.addCategory("android.intent.category.DEFAULT"); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setDataAndType(uri, "application/msword"); - return intent; - } - - private static Intent getExcelFileIntent(Uri uri) { - Intent intent = new Intent("android.intent.action.VIEW"); - intent.addCategory("android.intent.category.DEFAULT"); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setDataAndType(uri, "application/vnd.ms-excel"); - return intent; - } - - private static Intent getPPTFileIntent(Uri uri) { - Intent intent = new Intent("android.intent.action.VIEW"); - intent.addCategory("android.intent.category.DEFAULT"); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setDataAndType(uri, "application/vnd.ms-powerpoint"); - return intent; - } - - private static Intent getApkFileIntent(Context context, File file) { - Intent intent = new Intent(Intent.ACTION_VIEW); - //兼容7.0 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - Uri contentUri = FileProvider.getUriForFile(context, context.getPackageName() + "" + - ".fileprovider", file); - intent.setDataAndType(contentUri, "application/vnd.android.package-archive"); - } else { - intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive"); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } - return intent; - } - - public static void sortWithLastModified(File[] files) { - Arrays.sort(files, (f1, f2) -> { - long diff = f1.lastModified() - f2.lastModified(); - if (diff > 0) { - return -1; - } else if (diff == 0) { - return 0; - } else { - return 1; - } - }); - } - - /** - * Writes a byte array to a file creating the file if it does not exist. - * - * @param file the file to write to - * @param data the content to write to the file - * @param append if {@code true}, then bytes will be added to the - * end of the file rather than overwriting - * @throws IOException in case of an I/O error - * @since IO 2.1 - */ - public static void writeByteArrayToFile(File file, byte[] data, boolean append) throws IOException { - OutputStream out = null; - try { - out = openOutputStream(file, append); - out.write(data); - out.close(); // don't swallow close Exception if copy completes normally - } finally { - if (out != null) { - out.close(); - } - } - } - - private static FileOutputStream openOutputStream(File file, boolean append) throws IOException { - if (file.exists()) { - if (file.isDirectory()) { - throw new IOException("File '" + file + "' exists but is a directory"); - } - if (!file.canWrite()) { - throw new IOException("File '" + file + "' cannot be written to"); - } - } else { - File parent = file.getParentFile(); - if (parent != null) { - if (!parent.mkdirs() && !parent.isDirectory()) { - throw new IOException("Directory '" + parent + "' could not be created"); - } - } - } - return new FileOutputStream(file, append); - } -} diff --git a/app/src/main/java/me/pengtao/filetransfer/util/WifiUtils.java b/app/src/main/java/me/pengtao/filetransfer/util/WifiUtils.java deleted file mode 100755 index aa4c6e879cde1d71cee10d43f51a9a466c67f160..0000000000000000000000000000000000000000 --- a/app/src/main/java/me/pengtao/filetransfer/util/WifiUtils.java +++ /dev/null @@ -1,39 +0,0 @@ -package me.pengtao.filetransfer.util; - -import android.util.Log; - -import java.net.InetAddress; -import java.net.NetworkInterface; -import java.net.SocketException; -import java.util.Enumeration; - -/** - * @author chris - */ -public class WifiUtils { - private static final String TAG = "WifiUtils"; - - public static String getDeviceIpAddress() { - String deviceIpAddress = "###.###.###.###"; - - try { - for (Enumeration enumeration = - NetworkInterface.getNetworkInterfaces(); enumeration.hasMoreElements(); ) { - NetworkInterface networkInterface = enumeration.nextElement(); - - for (Enumeration enumerationIpAddr = - networkInterface.getInetAddresses(); enumerationIpAddr.hasMoreElements(); ) { - InetAddress inetAddress = enumerationIpAddr.nextElement(); - - if (!inetAddress.isLoopbackAddress() && inetAddress.getAddress().length == 4) { - deviceIpAddress = inetAddress.getHostAddress(); - } - } - } - } catch (SocketException e) { - Log.e(TAG, "SocketException:" + e.getMessage()); - } - - return deviceIpAddress; - } -} diff --git a/app/src/main/res/anim/popup_menu_dialog_in.xml b/app/src/main/res/anim/popup_menu_dialog_in.xml deleted file mode 100755 index cfd58a91fc8536ca38648e14fa334b914b1d3563..0000000000000000000000000000000000000000 --- a/app/src/main/res/anim/popup_menu_dialog_in.xml +++ /dev/null @@ -1,5 +0,0 @@ - - diff --git a/app/src/main/res/anim/popup_menu_dialog_out.xml b/app/src/main/res/anim/popup_menu_dialog_out.xml deleted file mode 100755 index 5439a7a6fb2f9a5be73a480d8ffe10bc02e56656..0000000000000000000000000000000000000000 --- a/app/src/main/res/anim/popup_menu_dialog_out.xml +++ /dev/null @@ -1,5 +0,0 @@ - - diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml deleted file mode 100644 index 045d604f4a307706d2962dfec06560dab35d9482..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/ic_add.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_book_cover.png b/app/src/main/res/drawable/ic_book_cover.png deleted file mode 100755 index ce12d77f33c6ef37a8ccb9e4e87227c5b1dd9bb1..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/drawable/ic_book_cover.png and /dev/null differ diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml deleted file mode 100644 index 5f1996502f6aa1c7f0331ae8ee21b225ebae80b0..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/ic_delete.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 01f0af0add58c1ae4e6d5b296cce73d9f9c99df1..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_wifi.png b/app/src/main/res/drawable/ic_wifi.png deleted file mode 100755 index 52d61b7a2ad7933dffce053d909bcd5100cb2863..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/drawable/ic_wifi.png and /dev/null differ diff --git a/app/src/main/res/drawable/shape_common_bg_frame10.xml b/app/src/main/res/drawable/shape_common_bg_frame10.xml deleted file mode 100644 index 01a8ce1621f16278fa5291d3b55eba68609bd590..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/shape_common_bg_frame10.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - //背景色 - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_red_bg_frame10.xml b/app/src/main/res/drawable/shape_red_bg_frame10.xml deleted file mode 100644 index e163a7318455c672879d17102857b93bd631a7f1..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/shape_red_bg_frame10.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - //背景色 - \ No newline at end of file diff --git a/app/src/main/res/drawable/shared_wifi_enable.png b/app/src/main/res/drawable/shared_wifi_enable.png deleted file mode 100755 index 18e9cf7947c752901a20d337ab38ae259dd9fcdf..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/drawable/shared_wifi_enable.png and /dev/null differ diff --git a/app/src/main/res/drawable/shared_wifi_shut_down.png b/app/src/main/res/drawable/shared_wifi_shut_down.png deleted file mode 100755 index 674da0deee41724041fad24cc3b1b85f7159e211..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/drawable/shared_wifi_shut_down.png and /dev/null differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100755 index cbea46edb38280d673d8600c3f01cfe026032b9a..0000000000000000000000000000000000000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml deleted file mode 100755 index 15a981b4e44098557d6195068a559bfadd39054d..0000000000000000000000000000000000000000 --- a/app/src/main/res/layout/content_main.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - diff --git a/app/src/main/res/layout/empty_view.xml b/app/src/main/res/layout/empty_view.xml deleted file mode 100644 index df276dc2a328dc3bc050b212f951d34c95626c14..0000000000000000000000000000000000000000 --- a/app/src/main/res/layout/empty_view.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/layout_file_item.xml b/app/src/main/res/layout/layout_file_item.xml deleted file mode 100755 index 27eb8cae4a422b20ba85bfa71e96d84dee247187..0000000000000000000000000000000000000000 --- a/app/src/main/res/layout/layout_file_item.xml +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - +

+ + + + + + + + +
+
+
+ + + + + + + + + + + + + + +
文件一览表
文件名文件大小操作
---- +        + +
+
+
+ +
+
+ +
+
+ 以上是存储在手机“/storage/emulated/0/UnboundedSharing/”目录下的文件,按文件修改日期递减排序。 +
+
+
+
5555
+
+
+ + + + \ No newline at end of file diff --git a/entry/src/main/resources/rawfile/web/image/logo.png b/entry/src/main/resources/rawfile/web/image/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..13e98e761ae7acc403d8411143c7fcf4b602152b Binary files /dev/null and b/entry/src/main/resources/rawfile/web/image/logo.png differ diff --git a/entry/src/main/resources/rawfile/web/image/upload_img.png b/entry/src/main/resources/rawfile/web/image/upload_img.png new file mode 100644 index 0000000000000000000000000000000000000000..5b67bc604c88827091243fdcedcc227a1b65c17f Binary files /dev/null and b/entry/src/main/resources/rawfile/web/image/upload_img.png differ diff --git a/app/src/main/assets/wifi/images/bg1_01.jpg b/entry/src/main/resources/rawfile/web/images/bg1_01.jpg old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/assets/wifi/images/bg1_01.jpg rename to entry/src/main/resources/rawfile/web/images/bg1_01.jpg diff --git a/app/src/main/assets/wifi/images/bg1_02.jpg b/entry/src/main/resources/rawfile/web/images/bg1_02.jpg old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/assets/wifi/images/bg1_02.jpg rename to entry/src/main/resources/rawfile/web/images/bg1_02.jpg diff --git a/app/src/main/assets/wifi/images/bg_drag.png b/entry/src/main/resources/rawfile/web/images/bg_drag.png old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/assets/wifi/images/bg_drag.png rename to entry/src/main/resources/rawfile/web/images/bg_drag.png diff --git a/app/src/main/assets/wifi/images/bg_file_list.jpg b/entry/src/main/resources/rawfile/web/images/bg_file_list.jpg old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/assets/wifi/images/bg_file_list.jpg rename to entry/src/main/resources/rawfile/web/images/bg_file_list.jpg diff --git a/app/src/main/assets/wifi/images/bg_files.jpg b/entry/src/main/resources/rawfile/web/images/bg_files.jpg old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/assets/wifi/images/bg_files.jpg rename to entry/src/main/resources/rawfile/web/images/bg_files.jpg diff --git a/app/src/main/assets/wifi/images/bg_files2.jpg b/entry/src/main/resources/rawfile/web/images/bg_files2.jpg old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/assets/wifi/images/bg_files2.jpg rename to entry/src/main/resources/rawfile/web/images/bg_files2.jpg diff --git a/app/src/main/assets/wifi/images/bg_title.jpg b/entry/src/main/resources/rawfile/web/images/bg_title.jpg old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/assets/wifi/images/bg_title.jpg rename to entry/src/main/resources/rawfile/web/images/bg_title.jpg diff --git a/app/src/main/assets/wifi/images/button_status.png b/entry/src/main/resources/rawfile/web/images/button_status.png old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/assets/wifi/images/button_status.png rename to entry/src/main/resources/rawfile/web/images/button_status.png diff --git a/app/src/main/assets/wifi/images/download.gif b/entry/src/main/resources/rawfile/web/images/download.gif old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/assets/wifi/images/download.gif rename to entry/src/main/resources/rawfile/web/images/download.gif diff --git a/app/src/main/assets/wifi/images/progress.jpg b/entry/src/main/resources/rawfile/web/images/progress.jpg old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/assets/wifi/images/progress.jpg rename to entry/src/main/resources/rawfile/web/images/progress.jpg diff --git a/app/src/main/assets/wifi/images/progress_bg.jpg b/entry/src/main/resources/rawfile/web/images/progress_bg.jpg old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/assets/wifi/images/progress_bg.jpg rename to entry/src/main/resources/rawfile/web/images/progress_bg.jpg diff --git a/app/src/main/assets/wifi/images/select_file1.jpg b/entry/src/main/resources/rawfile/web/images/select_file1.jpg old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/assets/wifi/images/select_file1.jpg rename to entry/src/main/resources/rawfile/web/images/select_file1.jpg diff --git a/app/src/main/assets/wifi/images/select_file1_pressed.jpg b/entry/src/main/resources/rawfile/web/images/select_file1_pressed.jpg old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/assets/wifi/images/select_file1_pressed.jpg rename to entry/src/main/resources/rawfile/web/images/select_file1_pressed.jpg diff --git a/app/src/main/assets/wifi/images/select_file1_rollover.jpg b/entry/src/main/resources/rawfile/web/images/select_file1_rollover.jpg old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/assets/wifi/images/select_file1_rollover.jpg rename to entry/src/main/resources/rawfile/web/images/select_file1_rollover.jpg diff --git a/app/src/main/assets/wifi/images/table_header_bg.jpg b/entry/src/main/resources/rawfile/web/images/table_header_bg.jpg old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/assets/wifi/images/table_header_bg.jpg rename to entry/src/main/resources/rawfile/web/images/table_header_bg.jpg diff --git a/app/src/main/assets/wifi/images/table_header_bg2.jpg b/entry/src/main/resources/rawfile/web/images/table_header_bg2.jpg old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/assets/wifi/images/table_header_bg2.jpg rename to entry/src/main/resources/rawfile/web/images/table_header_bg2.jpg diff --git a/app/src/main/assets/wifi/images/trash.gif b/entry/src/main/resources/rawfile/web/images/trash.gif old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/assets/wifi/images/trash.gif rename to entry/src/main/resources/rawfile/web/images/trash.gif diff --git a/app/src/main/assets/wifi/images/trash_white.png b/entry/src/main/resources/rawfile/web/images/trash_white.png old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/assets/wifi/images/trash_white.png rename to entry/src/main/resources/rawfile/web/images/trash_white.png diff --git a/app/src/main/assets/wifi/index.html b/entry/src/main/resources/rawfile/web/index.html old mode 100755 new mode 100644 similarity index 99% rename from app/src/main/assets/wifi/index.html rename to entry/src/main/resources/rawfile/web/index.html index 0b7633fc248e5f33e8ace938e832a7c65bd47343..463d645417c14d7047b57acd2b11442e06b97af2 --- a/app/src/main/assets/wifi/index.html +++ b/entry/src/main/resources/rawfile/web/index.html @@ -1,5 +1,6 @@ + diff --git a/entry/src/main/resources/rawfile/web/index2.html b/entry/src/main/resources/rawfile/web/index2.html new file mode 100644 index 0000000000000000000000000000000000000000..463d645417c14d7047b57acd2b11442e06b97af2 --- /dev/null +++ b/entry/src/main/resources/rawfile/web/index2.html @@ -0,0 +1,58 @@ + + + + + + + + + + + +
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+ + \ No newline at end of file diff --git a/entry/src/main/resources/rawfile/web/js/jquery-1.8.3.min.js b/entry/src/main/resources/rawfile/web/js/jquery-1.8.3.min.js new file mode 100644 index 0000000000000000000000000000000000000000..38837795279c5eb281e98ce6017998b993026518 --- /dev/null +++ b/entry/src/main/resources/rawfile/web/js/jquery-1.8.3.min.js @@ -0,0 +1,2 @@ +/*! jQuery v1.8.3 jquery.com | jquery.org/license */ +(function(e,t){function _(e){var t=M[e]={};return v.each(e.split(y),function(e,n){t[n]=!0}),t}function H(e,n,r){if(r===t&&e.nodeType===1){var i="data-"+n.replace(P,"-$1").toLowerCase();r=e.getAttribute(i);if(typeof r=="string"){try{r=r==="true"?!0:r==="false"?!1:r==="null"?null:+r+""===r?+r:D.test(r)?v.parseJSON(r):r}catch(s){}v.data(e,n,r)}else r=t}return r}function B(e){var t;for(t in e){if(t==="data"&&v.isEmptyObject(e[t]))continue;if(t!=="toJSON")return!1}return!0}function et(){return!1}function tt(){return!0}function ut(e){return!e||!e.parentNode||e.parentNode.nodeType===11}function at(e,t){do e=e[t];while(e&&e.nodeType!==1);return e}function ft(e,t,n){t=t||0;if(v.isFunction(t))return v.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return v.grep(e,function(e,r){return e===t===n});if(typeof t=="string"){var r=v.grep(e,function(e){return e.nodeType===1});if(it.test(t))return v.filter(t,r,!n);t=v.filter(t,r)}return v.grep(e,function(e,r){return v.inArray(e,t)>=0===n})}function lt(e){var t=ct.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}function Lt(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function At(e,t){if(t.nodeType!==1||!v.hasData(e))return;var n,r,i,s=v._data(e),o=v._data(t,s),u=s.events;if(u){delete o.handle,o.events={};for(n in u)for(r=0,i=u[n].length;r").appendTo(i.body),n=t.css("display");t.remove();if(n==="none"||n===""){Pt=i.body.appendChild(Pt||v.extend(i.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!Ht||!Pt.createElement)Ht=(Pt.contentWindow||Pt.contentDocument).document,Ht.write(""),Ht.close();t=Ht.body.appendChild(Ht.createElement(e)),n=Dt(t,"display"),i.body.removeChild(Pt)}return Wt[e]=n,n}function fn(e,t,n,r){var i;if(v.isArray(t))v.each(t,function(t,i){n||sn.test(e)?r(e,i):fn(e+"["+(typeof i=="object"?t:"")+"]",i,n,r)});else if(!n&&v.type(t)==="object")for(i in t)fn(e+"["+i+"]",t[i],n,r);else r(e,t)}function Cn(e){return function(t,n){typeof t!="string"&&(n=t,t="*");var r,i,s,o=t.toLowerCase().split(y),u=0,a=o.length;if(v.isFunction(n))for(;u)[^>]*$|#([\w\-]*)$)/,E=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,S=/^[\],:{}\s]*$/,x=/(?:^|:|,)(?:\s*\[)+/g,T=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,N=/"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,C=/^-ms-/,k=/-([\da-z])/gi,L=function(e,t){return(t+"").toUpperCase()},A=function(){i.addEventListener?(i.removeEventListener("DOMContentLoaded",A,!1),v.ready()):i.readyState==="complete"&&(i.detachEvent("onreadystatechange",A),v.ready())},O={};v.fn=v.prototype={constructor:v,init:function(e,n,r){var s,o,u,a;if(!e)return this;if(e.nodeType)return this.context=this[0]=e,this.length=1,this;if(typeof e=="string"){e.charAt(0)==="<"&&e.charAt(e.length-1)===">"&&e.length>=3?s=[null,e,null]:s=w.exec(e);if(s&&(s[1]||!n)){if(s[1])return n=n instanceof v?n[0]:n,a=n&&n.nodeType?n.ownerDocument||n:i,e=v.parseHTML(s[1],a,!0),E.test(s[1])&&v.isPlainObject(n)&&this.attr.call(e,n,!0),v.merge(this,e);o=i.getElementById(s[2]);if(o&&o.parentNode){if(o.id!==s[2])return r.find(e);this.length=1,this[0]=o}return this.context=i,this.selector=e,this}return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e)}return v.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),v.makeArray(e,this))},selector:"",jquery:"1.8.3",length:0,size:function(){return this.length},toArray:function(){return l.call(this)},get:function(e){return e==null?this.toArray():e<0?this[this.length+e]:this[e]},pushStack:function(e,t,n){var r=v.merge(this.constructor(),e);return r.prevObject=this,r.context=this.context,t==="find"?r.selector=this.selector+(this.selector?" ":"")+n:t&&(r.selector=this.selector+"."+t+"("+n+")"),r},each:function(e,t){return v.each(this,e,t)},ready:function(e){return v.ready.promise().done(e),this},eq:function(e){return e=+e,e===-1?this.slice(e):this.slice(e,e+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(l.apply(this,arguments),"slice",l.call(arguments).join(","))},map:function(e){return this.pushStack(v.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:[].sort,splice:[].splice},v.fn.init.prototype=v.fn,v.extend=v.fn.extend=function(){var e,n,r,i,s,o,u=arguments[0]||{},a=1,f=arguments.length,l=!1;typeof u=="boolean"&&(l=u,u=arguments[1]||{},a=2),typeof u!="object"&&!v.isFunction(u)&&(u={}),f===a&&(u=this,--a);for(;a0)return;r.resolveWith(i,[v]),v.fn.trigger&&v(i).trigger("ready").off("ready")},isFunction:function(e){return v.type(e)==="function"},isArray:Array.isArray||function(e){return v.type(e)==="array"},isWindow:function(e){return e!=null&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return e==null?String(e):O[h.call(e)]||"object"},isPlainObject:function(e){if(!e||v.type(e)!=="object"||e.nodeType||v.isWindow(e))return!1;try{if(e.constructor&&!p.call(e,"constructor")&&!p.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}var r;for(r in e);return r===t||p.call(e,r)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw new Error(e)},parseHTML:function(e,t,n){var r;return!e||typeof e!="string"?null:(typeof t=="boolean"&&(n=t,t=0),t=t||i,(r=E.exec(e))?[t.createElement(r[1])]:(r=v.buildFragment([e],t,n?null:[]),v.merge([],(r.cacheable?v.clone(r.fragment):r.fragment).childNodes)))},parseJSON:function(t){if(!t||typeof t!="string")return null;t=v.trim(t);if(e.JSON&&e.JSON.parse)return e.JSON.parse(t);if(S.test(t.replace(T,"@").replace(N,"]").replace(x,"")))return(new Function("return "+t))();v.error("Invalid JSON: "+t)},parseXML:function(n){var r,i;if(!n||typeof n!="string")return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(s){r=t}return(!r||!r.documentElement||r.getElementsByTagName("parsererror").length)&&v.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&g.test(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(C,"ms-").replace(k,L)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,n,r){var i,s=0,o=e.length,u=o===t||v.isFunction(e);if(r){if(u){for(i in e)if(n.apply(e[i],r)===!1)break}else for(;s0&&e[0]&&e[a-1]||a===0||v.isArray(e));if(f)for(;u-1)a.splice(n,1),i&&(n<=o&&o--,n<=u&&u--)}),this},has:function(e){return v.inArray(e,a)>-1},empty:function(){return a=[],this},disable:function(){return a=f=n=t,this},disabled:function(){return!a},lock:function(){return f=t,n||c.disable(),this},locked:function(){return!f},fireWith:function(e,t){return t=t||[],t=[e,t.slice?t.slice():t],a&&(!r||f)&&(i?f.push(t):l(t)),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!r}};return c},v.extend({Deferred:function(e){var t=[["resolve","done",v.Callbacks("once memory"),"resolved"],["reject","fail",v.Callbacks("once memory"),"rejected"],["notify","progress",v.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return v.Deferred(function(n){v.each(t,function(t,r){var s=r[0],o=e[t];i[r[1]](v.isFunction(o)?function(){var e=o.apply(this,arguments);e&&v.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[s+"With"](this===i?n:this,[e])}:n[s])}),e=null}).promise()},promise:function(e){return e!=null?v.extend(e,r):r}},i={};return r.pipe=r.then,v.each(t,function(e,s){var o=s[2],u=s[3];r[s[1]]=o.add,u&&o.add(function(){n=u},t[e^1][2].disable,t[2][2].lock),i[s[0]]=o.fire,i[s[0]+"With"]=o.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=l.call(arguments),r=n.length,i=r!==1||e&&v.isFunction(e.promise)?r:0,s=i===1?e:v.Deferred(),o=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?l.call(arguments):r,n===u?s.notifyWith(t,n):--i||s.resolveWith(t,n)}},u,a,f;if(r>1){u=new Array(r),a=new Array(r),f=new Array(r);for(;t
a",n=p.getElementsByTagName("*"),r=p.getElementsByTagName("a")[0];if(!n||!r||!n.length)return{};s=i.createElement("select"),o=s.appendChild(i.createElement("option")),u=p.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(r.getAttribute("style")),hrefNormalized:r.getAttribute("href")==="/a",opacity:/^0.5/.test(r.style.opacity),cssFloat:!!r.style.cssFloat,checkOn:u.value==="on",optSelected:o.selected,getSetAttribute:p.className!=="t",enctype:!!i.createElement("form").enctype,html5Clone:i.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",boxModel:i.compatMode==="CSS1Compat",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},u.checked=!0,t.noCloneChecked=u.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!o.disabled;try{delete p.test}catch(d){t.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",h=function(){t.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick"),p.detachEvent("onclick",h)),u=i.createElement("input"),u.value="t",u.setAttribute("type","radio"),t.radioValue=u.value==="t",u.setAttribute("checked","checked"),u.setAttribute("name","t"),p.appendChild(u),a=i.createDocumentFragment(),a.appendChild(p.lastChild),t.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,t.appendChecked=u.checked,a.removeChild(u),a.appendChild(p);if(p.attachEvent)for(l in{submit:!0,change:!0,focusin:!0})f="on"+l,c=f in p,c||(p.setAttribute(f,"return;"),c=typeof p[f]=="function"),t[l+"Bubbles"]=c;return v(function(){var n,r,s,o,u="padding:0;margin:0;border:0;display:block;overflow:hidden;",a=i.getElementsByTagName("body")[0];if(!a)return;n=i.createElement("div"),n.style.cssText="visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px",a.insertBefore(n,a.firstChild),r=i.createElement("div"),n.appendChild(r),r.innerHTML="
t
",s=r.getElementsByTagName("td"),s[0].style.cssText="padding:0;margin:0;border:0;display:none",c=s[0].offsetHeight===0,s[0].style.display="",s[1].style.display="none",t.reliableHiddenOffsets=c&&s[0].offsetHeight===0,r.innerHTML="",r.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",t.boxSizing=r.offsetWidth===4,t.doesNotIncludeMarginInBodyOffset=a.offsetTop!==1,e.getComputedStyle&&(t.pixelPosition=(e.getComputedStyle(r,null)||{}).top!=="1%",t.boxSizingReliable=(e.getComputedStyle(r,null)||{width:"4px"}).width==="4px",o=i.createElement("div"),o.style.cssText=r.style.cssText=u,o.style.marginRight=o.style.width="0",r.style.width="1px",r.appendChild(o),t.reliableMarginRight=!parseFloat((e.getComputedStyle(o,null)||{}).marginRight)),typeof r.style.zoom!="undefined"&&(r.innerHTML="",r.style.cssText=u+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=r.offsetWidth===3,r.style.display="block",r.style.overflow="visible",r.innerHTML="
",r.firstChild.style.width="5px",t.shrinkWrapBlocks=r.offsetWidth!==3,n.style.zoom=1),a.removeChild(n),n=r=s=o=null}),a.removeChild(p),n=r=s=o=u=a=p=null,t}();var D=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;v.extend({cache:{},deletedIds:[],uuid:0,expando:"jQuery"+(v.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(e){return e=e.nodeType?v.cache[e[v.expando]]:e[v.expando],!!e&&!B(e)},data:function(e,n,r,i){if(!v.acceptData(e))return;var s,o,u=v.expando,a=typeof n=="string",f=e.nodeType,l=f?v.cache:e,c=f?e[u]:e[u]&&u;if((!c||!l[c]||!i&&!l[c].data)&&a&&r===t)return;c||(f?e[u]=c=v.deletedIds.pop()||v.guid++:c=u),l[c]||(l[c]={},f||(l[c].toJSON=v.noop));if(typeof n=="object"||typeof n=="function")i?l[c]=v.extend(l[c],n):l[c].data=v.extend(l[c].data,n);return s=l[c],i||(s.data||(s.data={}),s=s.data),r!==t&&(s[v.camelCase(n)]=r),a?(o=s[n],o==null&&(o=s[v.camelCase(n)])):o=s,o},removeData:function(e,t,n){if(!v.acceptData(e))return;var r,i,s,o=e.nodeType,u=o?v.cache:e,a=o?e[v.expando]:v.expando;if(!u[a])return;if(t){r=n?u[a]:u[a].data;if(r){v.isArray(t)||(t in r?t=[t]:(t=v.camelCase(t),t in r?t=[t]:t=t.split(" ")));for(i=0,s=t.length;i1,null,!1))},removeData:function(e){return this.each(function(){v.removeData(this,e)})}}),v.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=v._data(e,t),n&&(!r||v.isArray(n)?r=v._data(e,t,v.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=v.queue(e,t),r=n.length,i=n.shift(),s=v._queueHooks(e,t),o=function(){v.dequeue(e,t)};i==="inprogress"&&(i=n.shift(),r--),i&&(t==="fx"&&n.unshift("inprogress"),delete s.stop,i.call(e,o,s)),!r&&s&&s.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return v._data(e,n)||v._data(e,n,{empty:v.Callbacks("once memory").add(function(){v.removeData(e,t+"queue",!0),v.removeData(e,n,!0)})})}}),v.fn.extend({queue:function(e,n){var r=2;return typeof e!="string"&&(n=e,e="fx",r--),arguments.length1)},removeAttr:function(e){return this.each(function(){v.removeAttr(this,e)})},prop:function(e,t){return v.access(this,v.prop,e,t,arguments.length>1)},removeProp:function(e){return e=v.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,s,o,u;if(v.isFunction(e))return this.each(function(t){v(this).addClass(e.call(this,t,this.className))});if(e&&typeof e=="string"){t=e.split(y);for(n=0,r=this.length;n=0)r=r.replace(" "+n[s]+" "," ");i.className=e?v.trim(r):""}}}return this},toggleClass:function(e,t){var n=typeof e,r=typeof t=="boolean";return v.isFunction(e)?this.each(function(n){v(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if(n==="string"){var i,s=0,o=v(this),u=t,a=e.split(y);while(i=a[s++])u=r?u:!o.hasClass(i),o[u?"addClass":"removeClass"](i)}else if(n==="undefined"||n==="boolean")this.className&&v._data(this,"__className__",this.className),this.className=this.className||e===!1?"":v._data(this,"__className__")||""})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;n=0)return!0;return!1},val:function(e){var n,r,i,s=this[0];if(!arguments.length){if(s)return n=v.valHooks[s.type]||v.valHooks[s.nodeName.toLowerCase()],n&&"get"in n&&(r=n.get(s,"value"))!==t?r:(r=s.value,typeof r=="string"?r.replace(R,""):r==null?"":r);return}return i=v.isFunction(e),this.each(function(r){var s,o=v(this);if(this.nodeType!==1)return;i?s=e.call(this,r,o.val()):s=e,s==null?s="":typeof s=="number"?s+="":v.isArray(s)&&(s=v.map(s,function(e){return e==null?"":e+""})),n=v.valHooks[this.type]||v.valHooks[this.nodeName.toLowerCase()];if(!n||!("set"in n)||n.set(this,s,"value")===t)this.value=s})}}),v.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,s=e.type==="select-one"||i<0,o=s?null:[],u=s?i+1:r.length,a=i<0?u:s?i:0;for(;a=0}),n.length||(e.selectedIndex=-1),n}}},attrFn:{},attr:function(e,n,r,i){var s,o,u,a=e.nodeType;if(!e||a===3||a===8||a===2)return;if(i&&v.isFunction(v.fn[n]))return v(e)[n](r);if(typeof e.getAttribute=="undefined")return v.prop(e,n,r);u=a!==1||!v.isXMLDoc(e),u&&(n=n.toLowerCase(),o=v.attrHooks[n]||(X.test(n)?F:j));if(r!==t){if(r===null){v.removeAttr(e,n);return}return o&&"set"in o&&u&&(s=o.set(e,r,n))!==t?s:(e.setAttribute(n,r+""),r)}return o&&"get"in o&&u&&(s=o.get(e,n))!==null?s:(s=e.getAttribute(n),s===null?t:s)},removeAttr:function(e,t){var n,r,i,s,o=0;if(t&&e.nodeType===1){r=t.split(y);for(;o=0}})});var $=/^(?:textarea|input|select)$/i,J=/^([^\.]*|)(?:\.(.+)|)$/,K=/(?:^|\s)hover(\.\S+|)\b/,Q=/^key/,G=/^(?:mouse|contextmenu)|click/,Y=/^(?:focusinfocus|focusoutblur)$/,Z=function(e){return v.event.special.hover?e:e.replace(K,"mouseenter$1 mouseleave$1")};v.event={add:function(e,n,r,i,s){var o,u,a,f,l,c,h,p,d,m,g;if(e.nodeType===3||e.nodeType===8||!n||!r||!(o=v._data(e)))return;r.handler&&(d=r,r=d.handler,s=d.selector),r.guid||(r.guid=v.guid++),a=o.events,a||(o.events=a={}),u=o.handle,u||(o.handle=u=function(e){return typeof v=="undefined"||!!e&&v.event.triggered===e.type?t:v.event.dispatch.apply(u.elem,arguments)},u.elem=e),n=v.trim(Z(n)).split(" ");for(f=0;f=0&&(y=y.slice(0,-1),a=!0),y.indexOf(".")>=0&&(b=y.split("."),y=b.shift(),b.sort());if((!s||v.event.customEvent[y])&&!v.event.global[y])return;n=typeof n=="object"?n[v.expando]?n:new v.Event(y,n):new v.Event(y),n.type=y,n.isTrigger=!0,n.exclusive=a,n.namespace=b.join("."),n.namespace_re=n.namespace?new RegExp("(^|\\.)"+b.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,h=y.indexOf(":")<0?"on"+y:"";if(!s){u=v.cache;for(f in u)u[f].events&&u[f].events[y]&&v.event.trigger(n,r,u[f].handle.elem,!0);return}n.result=t,n.target||(n.target=s),r=r!=null?v.makeArray(r):[],r.unshift(n),p=v.event.special[y]||{};if(p.trigger&&p.trigger.apply(s,r)===!1)return;m=[[s,p.bindType||y]];if(!o&&!p.noBubble&&!v.isWindow(s)){g=p.delegateType||y,l=Y.test(g+y)?s:s.parentNode;for(c=s;l;l=l.parentNode)m.push([l,g]),c=l;c===(s.ownerDocument||i)&&m.push([c.defaultView||c.parentWindow||e,g])}for(f=0;f=0:v.find(h,this,null,[s]).length),u[h]&&f.push(c);f.length&&w.push({elem:s,matches:f})}d.length>m&&w.push({elem:this,matches:d.slice(m)});for(r=0;r0?this.on(t,null,e,n):this.trigger(t)},Q.test(t)&&(v.event.fixHooks[t]=v.event.keyHooks),G.test(t)&&(v.event.fixHooks[t]=v.event.mouseHooks)}),function(e,t){function nt(e,t,n,r){n=n||[],t=t||g;var i,s,a,f,l=t.nodeType;if(!e||typeof e!="string")return n;if(l!==1&&l!==9)return[];a=o(t);if(!a&&!r)if(i=R.exec(e))if(f=i[1]){if(l===9){s=t.getElementById(f);if(!s||!s.parentNode)return n;if(s.id===f)return n.push(s),n}else if(t.ownerDocument&&(s=t.ownerDocument.getElementById(f))&&u(t,s)&&s.id===f)return n.push(s),n}else{if(i[2])return S.apply(n,x.call(t.getElementsByTagName(e),0)),n;if((f=i[3])&&Z&&t.getElementsByClassName)return S.apply(n,x.call(t.getElementsByClassName(f),0)),n}return vt(e.replace(j,"$1"),t,n,r,a)}function rt(e){return function(t){var n=t.nodeName.toLowerCase();return n==="input"&&t.type===e}}function it(e){return function(t){var n=t.nodeName.toLowerCase();return(n==="input"||n==="button")&&t.type===e}}function st(e){return N(function(t){return t=+t,N(function(n,r){var i,s=e([],n.length,t),o=s.length;while(o--)n[i=s[o]]&&(n[i]=!(r[i]=n[i]))})})}function ot(e,t,n){if(e===t)return n;var r=e.nextSibling;while(r){if(r===t)return-1;r=r.nextSibling}return 1}function ut(e,t){var n,r,s,o,u,a,f,l=L[d][e+" "];if(l)return t?0:l.slice(0);u=e,a=[],f=i.preFilter;while(u){if(!n||(r=F.exec(u)))r&&(u=u.slice(r[0].length)||u),a.push(s=[]);n=!1;if(r=I.exec(u))s.push(n=new m(r.shift())),u=u.slice(n.length),n.type=r[0].replace(j," ");for(o in i.filter)(r=J[o].exec(u))&&(!f[o]||(r=f[o](r)))&&(s.push(n=new m(r.shift())),u=u.slice(n.length),n.type=o,n.matches=r);if(!n)break}return t?u.length:u?nt.error(e):L(e,a).slice(0)}function at(e,t,r){var i=t.dir,s=r&&t.dir==="parentNode",o=w++;return t.first?function(t,n,r){while(t=t[i])if(s||t.nodeType===1)return e(t,n,r)}:function(t,r,u){if(!u){var a,f=b+" "+o+" ",l=f+n;while(t=t[i])if(s||t.nodeType===1){if((a=t[d])===l)return t.sizset;if(typeof a=="string"&&a.indexOf(f)===0){if(t.sizset)return t}else{t[d]=l;if(e(t,r,u))return t.sizset=!0,t;t.sizset=!1}}}else while(t=t[i])if(s||t.nodeType===1)if(e(t,r,u))return t}}function ft(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function lt(e,t,n,r,i){var s,o=[],u=0,a=e.length,f=t!=null;for(;u-1&&(s[f]=!(o[f]=c))}}else g=lt(g===o?g.splice(d,g.length):g),i?i(null,o,g,a):S.apply(o,g)})}function ht(e){var t,n,r,s=e.length,o=i.relative[e[0].type],u=o||i.relative[" "],a=o?1:0,f=at(function(e){return e===t},u,!0),l=at(function(e){return T.call(t,e)>-1},u,!0),h=[function(e,n,r){return!o&&(r||n!==c)||((t=n).nodeType?f(e,n,r):l(e,n,r))}];for(;a1&&ft(h),a>1&&e.slice(0,a-1).join("").replace(j,"$1"),n,a0,s=e.length>0,o=function(u,a,f,l,h){var p,d,v,m=[],y=0,w="0",x=u&&[],T=h!=null,N=c,C=u||s&&i.find.TAG("*",h&&a.parentNode||a),k=b+=N==null?1:Math.E;T&&(c=a!==g&&a,n=o.el);for(;(p=C[w])!=null;w++){if(s&&p){for(d=0;v=e[d];d++)if(v(p,a,f)){l.push(p);break}T&&(b=k,n=++o.el)}r&&((p=!v&&p)&&y--,u&&x.push(p))}y+=w;if(r&&w!==y){for(d=0;v=t[d];d++)v(x,m,a,f);if(u){if(y>0)while(w--)!x[w]&&!m[w]&&(m[w]=E.call(l));m=lt(m)}S.apply(l,m),T&&!u&&m.length>0&&y+t.length>1&&nt.uniqueSort(l)}return T&&(b=k,c=N),x};return o.el=0,r?N(o):o}function dt(e,t,n){var r=0,i=t.length;for(;r2&&(f=u[0]).type==="ID"&&t.nodeType===9&&!s&&i.relative[u[1].type]){t=i.find.ID(f.matches[0].replace($,""),t,s)[0];if(!t)return n;e=e.slice(u.shift().length)}for(o=J.POS.test(e)?-1:u.length-1;o>=0;o--){f=u[o];if(i.relative[l=f.type])break;if(c=i.find[l])if(r=c(f.matches[0].replace($,""),z.test(u[0].type)&&t.parentNode||t,s)){u.splice(o,1),e=r.length&&u.join("");if(!e)return S.apply(n,x.call(r,0)),n;break}}}return a(e,h)(r,t,s,n,z.test(e)),n}function mt(){}var n,r,i,s,o,u,a,f,l,c,h=!0,p="undefined",d=("sizcache"+Math.random()).replace(".",""),m=String,g=e.document,y=g.documentElement,b=0,w=0,E=[].pop,S=[].push,x=[].slice,T=[].indexOf||function(e){var t=0,n=this.length;for(;ti.cacheLength&&delete e[t.shift()],e[n+" "]=r},e)},k=C(),L=C(),A=C(),O="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",_=M.replace("w","w#"),D="([*^$|!~]?=)",P="\\["+O+"*("+M+")"+O+"*(?:"+D+O+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+_+")|)|)"+O+"*\\]",H=":("+M+")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:"+P+")|[^:]|\\\\.)*|.*))\\)|)",B=":(even|odd|eq|gt|lt|nth|first|last)(?:\\("+O+"*((?:-\\d)?\\d*)"+O+"*\\)|)(?=[^-]|$)",j=new RegExp("^"+O+"+|((?:^|[^\\\\])(?:\\\\.)*)"+O+"+$","g"),F=new RegExp("^"+O+"*,"+O+"*"),I=new RegExp("^"+O+"*([\\x20\\t\\r\\n\\f>+~])"+O+"*"),q=new RegExp(H),R=/^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,U=/^:not/,z=/[\x20\t\r\n\f]*[+~]/,W=/:not\($/,X=/h\d/i,V=/input|select|textarea|button/i,$=/\\(?!\\)/g,J={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),NAME:new RegExp("^\\[name=['\"]?("+M+")['\"]?\\]"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+H),POS:new RegExp(B,"i"),CHILD:new RegExp("^:(only|nth|first|last)-child(?:\\("+O+"*(even|odd|(([+-]|)(\\d*)n|)"+O+"*(?:([+-]|)"+O+"*(\\d+)|))"+O+"*\\)|)","i"),needsContext:new RegExp("^"+O+"*[>+~]|"+B,"i")},K=function(e){var t=g.createElement("div");try{return e(t)}catch(n){return!1}finally{t=null}},Q=K(function(e){return e.appendChild(g.createComment("")),!e.getElementsByTagName("*").length}),G=K(function(e){return e.innerHTML="",e.firstChild&&typeof e.firstChild.getAttribute!==p&&e.firstChild.getAttribute("href")==="#"}),Y=K(function(e){e.innerHTML="";var t=typeof e.lastChild.getAttribute("multiple");return t!=="boolean"&&t!=="string"}),Z=K(function(e){return e.innerHTML="",!e.getElementsByClassName||!e.getElementsByClassName("e").length?!1:(e.lastChild.className="e",e.getElementsByClassName("e").length===2)}),et=K(function(e){e.id=d+0,e.innerHTML="
",y.insertBefore(e,y.firstChild);var t=g.getElementsByName&&g.getElementsByName(d).length===2+g.getElementsByName(d+0).length;return r=!g.getElementById(d),y.removeChild(e),t});try{x.call(y.childNodes,0)[0].nodeType}catch(tt){x=function(e){var t,n=[];for(;t=this[e];e++)n.push(t);return n}}nt.matches=function(e,t){return nt(e,null,null,t)},nt.matchesSelector=function(e,t){return nt(t,null,null,[e]).length>0},s=nt.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(i===1||i===9||i===11){if(typeof e.textContent=="string")return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=s(e)}else if(i===3||i===4)return e.nodeValue}else for(;t=e[r];r++)n+=s(t);return n},o=nt.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?t.nodeName!=="HTML":!1},u=nt.contains=y.contains?function(e,t){var n=e.nodeType===9?e.documentElement:e,r=t&&t.parentNode;return e===r||!!(r&&r.nodeType===1&&n.contains&&n.contains(r))}:y.compareDocumentPosition?function(e,t){return t&&!!(e.compareDocumentPosition(t)&16)}:function(e,t){while(t=t.parentNode)if(t===e)return!0;return!1},nt.attr=function(e,t){var n,r=o(e);return r||(t=t.toLowerCase()),(n=i.attrHandle[t])?n(e):r||Y?e.getAttribute(t):(n=e.getAttributeNode(t),n?typeof e[t]=="boolean"?e[t]?t:null:n.specified?n.value:null:null)},i=nt.selectors={cacheLength:50,createPseudo:N,match:J,attrHandle:G?{}:{href:function(e){return e.getAttribute("href",2)},type:function(e){return e.getAttribute("type")}},find:{ID:r?function(e,t,n){if(typeof t.getElementById!==p&&!n){var r=t.getElementById(e);return r&&r.parentNode?[r]:[]}}:function(e,n,r){if(typeof n.getElementById!==p&&!r){var i=n.getElementById(e);return i?i.id===e||typeof i.getAttributeNode!==p&&i.getAttributeNode("id").value===e?[i]:t:[]}},TAG:Q?function(e,t){if(typeof t.getElementsByTagName!==p)return t.getElementsByTagName(e)}:function(e,t){var n=t.getElementsByTagName(e);if(e==="*"){var r,i=[],s=0;for(;r=n[s];s++)r.nodeType===1&&i.push(r);return i}return n},NAME:et&&function(e,t){if(typeof t.getElementsByName!==p)return t.getElementsByName(name)},CLASS:Z&&function(e,t,n){if(typeof t.getElementsByClassName!==p&&!n)return t.getElementsByClassName(e)}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace($,""),e[3]=(e[4]||e[5]||"").replace($,""),e[2]==="~="&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),e[1]==="nth"?(e[2]||nt.error(e[0]),e[3]=+(e[3]?e[4]+(e[5]||1):2*(e[2]==="even"||e[2]==="odd")),e[4]=+(e[6]+e[7]||e[2]==="odd")):e[2]&&nt.error(e[0]),e},PSEUDO:function(e){var t,n;if(J.CHILD.test(e[0]))return null;if(e[3])e[2]=e[3];else if(t=e[4])q.test(t)&&(n=ut(t,!0))&&(n=t.indexOf(")",t.length-n)-t.length)&&(t=t.slice(0,n),e[0]=e[0].slice(0,n)),e[2]=t;return e.slice(0,3)}},filter:{ID:r?function(e){return e=e.replace($,""),function(t){return t.getAttribute("id")===e}}:function(e){return e=e.replace($,""),function(t){var n=typeof t.getAttributeNode!==p&&t.getAttributeNode("id");return n&&n.value===e}},TAG:function(e){return e==="*"?function(){return!0}:(e=e.replace($,"").toLowerCase(),function(t){return t.nodeName&&t.nodeName.toLowerCase()===e})},CLASS:function(e){var t=k[d][e+" "];return t||(t=new RegExp("(^|"+O+")"+e+"("+O+"|$)"))&&k(e,function(e){return t.test(e.className||typeof e.getAttribute!==p&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r,i){var s=nt.attr(r,e);return s==null?t==="!=":t?(s+="",t==="="?s===n:t==="!="?s!==n:t==="^="?n&&s.indexOf(n)===0:t==="*="?n&&s.indexOf(n)>-1:t==="$="?n&&s.substr(s.length-n.length)===n:t==="~="?(" "+s+" ").indexOf(n)>-1:t==="|="?s===n||s.substr(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r){return e==="nth"?function(e){var t,i,s=e.parentNode;if(n===1&&r===0)return!0;if(s){i=0;for(t=s.firstChild;t;t=t.nextSibling)if(t.nodeType===1){i++;if(e===t)break}}return i-=r,i===n||i%n===0&&i/n>=0}:function(t){var n=t;switch(e){case"only":case"first":while(n=n.previousSibling)if(n.nodeType===1)return!1;if(e==="first")return!0;n=t;case"last":while(n=n.nextSibling)if(n.nodeType===1)return!1;return!0}}},PSEUDO:function(e,t){var n,r=i.pseudos[e]||i.setFilters[e.toLowerCase()]||nt.error("unsupported pseudo: "+e);return r[d]?r(t):r.length>1?(n=[e,e,"",t],i.setFilters.hasOwnProperty(e.toLowerCase())?N(function(e,n){var i,s=r(e,t),o=s.length;while(o--)i=T.call(e,s[o]),e[i]=!(n[i]=s[o])}):function(e){return r(e,0,n)}):r}},pseudos:{not:N(function(e){var t=[],n=[],r=a(e.replace(j,"$1"));return r[d]?N(function(e,t,n,i){var s,o=r(e,null,i,[]),u=e.length;while(u--)if(s=o[u])e[u]=!(t[u]=s)}):function(e,i,s){return t[0]=e,r(t,null,s,n),!n.pop()}}),has:N(function(e){return function(t){return nt(e,t).length>0}}),contains:N(function(e){return function(t){return(t.textContent||t.innerText||s(t)).indexOf(e)>-1}}),enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return t==="input"&&!!e.checked||t==="option"&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},parent:function(e){return!i.pseudos.empty(e)},empty:function(e){var t;e=e.firstChild;while(e){if(e.nodeName>"@"||(t=e.nodeType)===3||t===4)return!1;e=e.nextSibling}return!0},header:function(e){return X.test(e.nodeName)},text:function(e){var t,n;return e.nodeName.toLowerCase()==="input"&&(t=e.type)==="text"&&((n=e.getAttribute("type"))==null||n.toLowerCase()===t)},radio:rt("radio"),checkbox:rt("checkbox"),file:rt("file"),password:rt("password"),image:rt("image"),submit:it("submit"),reset:it("reset"),button:function(e){var t=e.nodeName.toLowerCase();return t==="input"&&e.type==="button"||t==="button"},input:function(e){return V.test(e.nodeName)},focus:function(e){var t=e.ownerDocument;return e===t.activeElement&&(!t.hasFocus||t.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},active:function(e){return e===e.ownerDocument.activeElement},first:st(function(){return[0]}),last:st(function(e,t){return[t-1]}),eq:st(function(e,t,n){return[n<0?n+t:n]}),even:st(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:st(function(e,t,n){for(var r=n<0?n+t:n;++r",e.querySelectorAll("[selected]").length||i.push("\\["+O+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),e.querySelectorAll(":checked").length||i.push(":checked")}),K(function(e){e.innerHTML="

",e.querySelectorAll("[test^='']").length&&i.push("[*^$]="+O+"*(?:\"\"|'')"),e.innerHTML="",e.querySelectorAll(":enabled").length||i.push(":enabled",":disabled")}),i=new RegExp(i.join("|")),vt=function(e,r,s,o,u){if(!o&&!u&&!i.test(e)){var a,f,l=!0,c=d,h=r,p=r.nodeType===9&&e;if(r.nodeType===1&&r.nodeName.toLowerCase()!=="object"){a=ut(e),(l=r.getAttribute("id"))?c=l.replace(n,"\\$&"):r.setAttribute("id",c),c="[id='"+c+"'] ",f=a.length;while(f--)a[f]=c+a[f].join("");h=z.test(e)&&r.parentNode||r,p=a.join(",")}if(p)try{return S.apply(s,x.call(h.querySelectorAll(p),0)),s}catch(v){}finally{l||r.removeAttribute("id")}}return t(e,r,s,o,u)},u&&(K(function(t){e=u.call(t,"div");try{u.call(t,"[test!='']:sizzle"),s.push("!=",H)}catch(n){}}),s=new RegExp(s.join("|")),nt.matchesSelector=function(t,n){n=n.replace(r,"='$1']");if(!o(t)&&!s.test(n)&&!i.test(n))try{var a=u.call(t,n);if(a||e||t.document&&t.document.nodeType!==11)return a}catch(f){}return nt(n,null,null,[t]).length>0})}(),i.pseudos.nth=i.pseudos.eq,i.filters=mt.prototype=i.pseudos,i.setFilters=new mt,nt.attr=v.attr,v.find=nt,v.expr=nt.selectors,v.expr[":"]=v.expr.pseudos,v.unique=nt.uniqueSort,v.text=nt.getText,v.isXMLDoc=nt.isXML,v.contains=nt.contains}(e);var nt=/Until$/,rt=/^(?:parents|prev(?:Until|All))/,it=/^.[^:#\[\.,]*$/,st=v.expr.match.needsContext,ot={children:!0,contents:!0,next:!0,prev:!0};v.fn.extend({find:function(e){var t,n,r,i,s,o,u=this;if(typeof e!="string")return v(e).filter(function(){for(t=0,n=u.length;t0)for(i=r;i=0:v.filter(e,this).length>0:this.filter(e).length>0)},closest:function(e,t){var n,r=0,i=this.length,s=[],o=st.test(e)||typeof e!="string"?v(e,t||this.context):0;for(;r-1:v.find.matchesSelector(n,e)){s.push(n);break}n=n.parentNode}}return s=s.length>1?v.unique(s):s,this.pushStack(s,"closest",e)},index:function(e){return e?typeof e=="string"?v.inArray(this[0],v(e)):v.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(e,t){var n=typeof e=="string"?v(e,t):v.makeArray(e&&e.nodeType?[e]:e),r=v.merge(this.get(),n);return this.pushStack(ut(n[0])||ut(r[0])?r:v.unique(r))},addBack:function(e){return this.add(e==null?this.prevObject:this.prevObject.filter(e))}}),v.fn.andSelf=v.fn.addBack,v.each({parent:function(e){var t=e.parentNode;return t&&t.nodeType!==11?t:null},parents:function(e){return v.dir(e,"parentNode")},parentsUntil:function(e,t,n){return v.dir(e,"parentNode",n)},next:function(e){return at(e,"nextSibling")},prev:function(e){return at(e,"previousSibling")},nextAll:function(e){return v.dir(e,"nextSibling")},prevAll:function(e){return v.dir(e,"previousSibling")},nextUntil:function(e,t,n){return v.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return v.dir(e,"previousSibling",n)},siblings:function(e){return v.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return v.sibling(e.firstChild)},contents:function(e){return v.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:v.merge([],e.childNodes)}},function(e,t){v.fn[e]=function(n,r){var i=v.map(this,t,n);return nt.test(e)||(r=n),r&&typeof r=="string"&&(i=v.filter(r,i)),i=this.length>1&&!ot[e]?v.unique(i):i,this.length>1&&rt.test(e)&&(i=i.reverse()),this.pushStack(i,e,l.call(arguments).join(","))}}),v.extend({filter:function(e,t,n){return n&&(e=":not("+e+")"),t.length===1?v.find.matchesSelector(t[0],e)?[t[0]]:[]:v.find.matches(e,t)},dir:function(e,n,r){var i=[],s=e[n];while(s&&s.nodeType!==9&&(r===t||s.nodeType!==1||!v(s).is(r)))s.nodeType===1&&i.push(s),s=s[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)e.nodeType===1&&e!==t&&n.push(e);return n}});var ct="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",ht=/ jQuery\d+="(?:null|\d+)"/g,pt=/^\s+/,dt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,vt=/<([\w:]+)/,mt=/]","i"),Et=/^(?:checkbox|radio)$/,St=/checked\s*(?:[^=]|=\s*.checked.)/i,xt=/\/(java|ecma)script/i,Tt=/^\s*\s*$/g,Nt={option:[1,""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},Ct=lt(i),kt=Ct.appendChild(i.createElement("div"));Nt.optgroup=Nt.option,Nt.tbody=Nt.tfoot=Nt.colgroup=Nt.caption=Nt.thead,Nt.th=Nt.td,v.support.htmlSerialize||(Nt._default=[1,"X
","
"]),v.fn.extend({text:function(e){return v.access(this,function(e){return e===t?v.text(this):this.empty().append((this[0]&&this[0].ownerDocument||i).createTextNode(e))},null,e,arguments.length)},wrapAll:function(e){if(v.isFunction(e))return this.each(function(t){v(this).wrapAll(e.call(this,t))});if(this[0]){var t=v(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&e.firstChild.nodeType===1)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return v.isFunction(e)?this.each(function(t){v(this).wrapInner(e.call(this,t))}):this.each(function(){var t=v(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=v.isFunction(e);return this.each(function(n){v(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){v.nodeName(this,"body")||v(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(e){(this.nodeType===1||this.nodeType===11)&&this.appendChild(e)})},prepend:function(){return this.domManip(arguments,!0,function(e){(this.nodeType===1||this.nodeType===11)&&this.insertBefore(e,this.firstChild)})},before:function(){if(!ut(this[0]))return this.domManip(arguments,!1,function(e){this.parentNode.insertBefore(e,this)});if(arguments.length){var e=v.clean(arguments);return this.pushStack(v.merge(e,this),"before",this.selector)}},after:function(){if(!ut(this[0]))return this.domManip(arguments,!1,function(e){this.parentNode.insertBefore(e,this.nextSibling)});if(arguments.length){var e=v.clean(arguments);return this.pushStack(v.merge(this,e),"after",this.selector)}},remove:function(e,t){var n,r=0;for(;(n=this[r])!=null;r++)if(!e||v.filter(e,[n]).length)!t&&n.nodeType===1&&(v.cleanData(n.getElementsByTagName("*")),v.cleanData([n])),n.parentNode&&n.parentNode.removeChild(n);return this},empty:function(){var e,t=0;for(;(e=this[t])!=null;t++){e.nodeType===1&&v.cleanData(e.getElementsByTagName("*"));while(e.firstChild)e.removeChild(e.firstChild)}return this},clone:function(e,t){return e=e==null?!1:e,t=t==null?e:t,this.map(function(){return v.clone(this,e,t)})},html:function(e){return v.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return n.nodeType===1?n.innerHTML.replace(ht,""):t;if(typeof e=="string"&&!yt.test(e)&&(v.support.htmlSerialize||!wt.test(e))&&(v.support.leadingWhitespace||!pt.test(e))&&!Nt[(vt.exec(e)||["",""])[1].toLowerCase()]){e=e.replace(dt,"<$1>");try{for(;r1&&typeof f=="string"&&St.test(f))return this.each(function(){v(this).domManip(e,n,r)});if(v.isFunction(f))return this.each(function(i){var s=v(this);e[0]=f.call(this,i,n?s.html():t),s.domManip(e,n,r)});if(this[0]){i=v.buildFragment(e,this,l),o=i.fragment,s=o.firstChild,o.childNodes.length===1&&(o=s);if(s){n=n&&v.nodeName(s,"tr");for(u=i.cacheable||c-1;a0?this.clone(!0):this).get(),v(o[i])[t](r),s=s.concat(r);return this.pushStack(s,e,o.selector)}}),v.extend({clone:function(e,t,n){var r,i,s,o;v.support.html5Clone||v.isXMLDoc(e)||!wt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(kt.innerHTML=e.outerHTML,kt.removeChild(o=kt.firstChild));if((!v.support.noCloneEvent||!v.support.noCloneChecked)&&(e.nodeType===1||e.nodeType===11)&&!v.isXMLDoc(e)){Ot(e,o),r=Mt(e),i=Mt(o);for(s=0;r[s];++s)i[s]&&Ot(r[s],i[s])}if(t){At(e,o);if(n){r=Mt(e),i=Mt(o);for(s=0;r[s];++s)At(r[s],i[s])}}return r=i=null,o},clean:function(e,t,n,r){var s,o,u,a,f,l,c,h,p,d,m,g,y=t===i&&Ct,b=[];if(!t||typeof t.createDocumentFragment=="undefined")t=i;for(s=0;(u=e[s])!=null;s++){typeof u=="number"&&(u+="");if(!u)continue;if(typeof u=="string")if(!gt.test(u))u=t.createTextNode(u);else{y=y||lt(t),c=t.createElement("div"),y.appendChild(c),u=u.replace(dt,"<$1>"),a=(vt.exec(u)||["",""])[1].toLowerCase(),f=Nt[a]||Nt._default,l=f[0],c.innerHTML=f[1]+u+f[2];while(l--)c=c.lastChild;if(!v.support.tbody){h=mt.test(u),p=a==="table"&&!h?c.firstChild&&c.firstChild.childNodes:f[1]===""&&!h?c.childNodes:[];for(o=p.length-1;o>=0;--o)v.nodeName(p[o],"tbody")&&!p[o].childNodes.length&&p[o].parentNode.removeChild(p[o])}!v.support.leadingWhitespace&&pt.test(u)&&c.insertBefore(t.createTextNode(pt.exec(u)[0]),c.firstChild),u=c.childNodes,c.parentNode.removeChild(c)}u.nodeType?b.push(u):v.merge(b,u)}c&&(u=c=y=null);if(!v.support.appendChecked)for(s=0;(u=b[s])!=null;s++)v.nodeName(u,"input")?_t(u):typeof u.getElementsByTagName!="undefined"&&v.grep(u.getElementsByTagName("input"),_t);if(n){m=function(e){if(!e.type||xt.test(e.type))return r?r.push(e.parentNode?e.parentNode.removeChild(e):e):n.appendChild(e)};for(s=0;(u=b[s])!=null;s++)if(!v.nodeName(u,"script")||!m(u))n.appendChild(u),typeof u.getElementsByTagName!="undefined"&&(g=v.grep(v.merge([],u.getElementsByTagName("script")),m),b.splice.apply(b,[s+1,0].concat(g)),s+=g.length)}return b},cleanData:function(e,t){var n,r,i,s,o=0,u=v.expando,a=v.cache,f=v.support.deleteExpando,l=v.event.special;for(;(i=e[o])!=null;o++)if(t||v.acceptData(i)){r=i[u],n=r&&a[r];if(n){if(n.events)for(s in n.events)l[s]?v.event.remove(i,s):v.removeEvent(i,s,n.handle);a[r]&&(delete a[r],f?delete i[u]:i.removeAttribute?i.removeAttribute(u):i[u]=null,v.deletedIds.push(r))}}}}),function(){var e,t;v.uaMatch=function(e){e=e.toLowerCase();var t=/(chrome)[ \/]([\w.]+)/.exec(e)||/(webkit)[ \/]([\w.]+)/.exec(e)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(e)||/(msie) ([\w.]+)/.exec(e)||e.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(e)||[];return{browser:t[1]||"",version:t[2]||"0"}},e=v.uaMatch(o.userAgent),t={},e.browser&&(t[e.browser]=!0,t.version=e.version),t.chrome?t.webkit=!0:t.webkit&&(t.safari=!0),v.browser=t,v.sub=function(){function e(t,n){return new e.fn.init(t,n)}v.extend(!0,e,this),e.superclass=this,e.fn=e.prototype=this(),e.fn.constructor=e,e.sub=this.sub,e.fn.init=function(r,i){return i&&i instanceof v&&!(i instanceof e)&&(i=e(i)),v.fn.init.call(this,r,i,t)},e.fn.init.prototype=e.fn;var t=e(i);return e}}();var Dt,Pt,Ht,Bt=/alpha\([^)]*\)/i,jt=/opacity=([^)]*)/,Ft=/^(top|right|bottom|left)$/,It=/^(none|table(?!-c[ea]).+)/,qt=/^margin/,Rt=new RegExp("^("+m+")(.*)$","i"),Ut=new RegExp("^("+m+")(?!px)[a-z%]+$","i"),zt=new RegExp("^([-+])=("+m+")","i"),Wt={BODY:"block"},Xt={position:"absolute",visibility:"hidden",display:"block"},Vt={letterSpacing:0,fontWeight:400},$t=["Top","Right","Bottom","Left"],Jt=["Webkit","O","Moz","ms"],Kt=v.fn.toggle;v.fn.extend({css:function(e,n){return v.access(this,function(e,n,r){return r!==t?v.style(e,n,r):v.css(e,n)},e,n,arguments.length>1)},show:function(){return Yt(this,!0)},hide:function(){return Yt(this)},toggle:function(e,t){var n=typeof e=="boolean";return v.isFunction(e)&&v.isFunction(t)?Kt.apply(this,arguments):this.each(function(){(n?e:Gt(this))?v(this).show():v(this).hide()})}}),v.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Dt(e,"opacity");return n===""?"1":n}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":v.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(!e||e.nodeType===3||e.nodeType===8||!e.style)return;var s,o,u,a=v.camelCase(n),f=e.style;n=v.cssProps[a]||(v.cssProps[a]=Qt(f,a)),u=v.cssHooks[n]||v.cssHooks[a];if(r===t)return u&&"get"in u&&(s=u.get(e,!1,i))!==t?s:f[n];o=typeof r,o==="string"&&(s=zt.exec(r))&&(r=(s[1]+1)*s[2]+parseFloat(v.css(e,n)),o="number");if(r==null||o==="number"&&isNaN(r))return;o==="number"&&!v.cssNumber[a]&&(r+="px");if(!u||!("set"in u)||(r=u.set(e,r,i))!==t)try{f[n]=r}catch(l){}},css:function(e,n,r,i){var s,o,u,a=v.camelCase(n);return n=v.cssProps[a]||(v.cssProps[a]=Qt(e.style,a)),u=v.cssHooks[n]||v.cssHooks[a],u&&"get"in u&&(s=u.get(e,!0,i)),s===t&&(s=Dt(e,n)),s==="normal"&&n in Vt&&(s=Vt[n]),r||i!==t?(o=parseFloat(s),r||v.isNumeric(o)?o||0:s):s},swap:function(e,t,n){var r,i,s={};for(i in t)s[i]=e.style[i],e.style[i]=t[i];r=n.call(e);for(i in t)e.style[i]=s[i];return r}}),e.getComputedStyle?Dt=function(t,n){var r,i,s,o,u=e.getComputedStyle(t,null),a=t.style;return u&&(r=u.getPropertyValue(n)||u[n],r===""&&!v.contains(t.ownerDocument,t)&&(r=v.style(t,n)),Ut.test(r)&&qt.test(n)&&(i=a.width,s=a.minWidth,o=a.maxWidth,a.minWidth=a.maxWidth=a.width=r,r=u.width,a.width=i,a.minWidth=s,a.maxWidth=o)),r}:i.documentElement.currentStyle&&(Dt=function(e,t){var n,r,i=e.currentStyle&&e.currentStyle[t],s=e.style;return i==null&&s&&s[t]&&(i=s[t]),Ut.test(i)&&!Ft.test(t)&&(n=s.left,r=e.runtimeStyle&&e.runtimeStyle.left,r&&(e.runtimeStyle.left=e.currentStyle.left),s.left=t==="fontSize"?"1em":i,i=s.pixelLeft+"px",s.left=n,r&&(e.runtimeStyle.left=r)),i===""?"auto":i}),v.each(["height","width"],function(e,t){v.cssHooks[t]={get:function(e,n,r){if(n)return e.offsetWidth===0&&It.test(Dt(e,"display"))?v.swap(e,Xt,function(){return tn(e,t,r)}):tn(e,t,r)},set:function(e,n,r){return Zt(e,n,r?en(e,t,r,v.support.boxSizing&&v.css(e,"boxSizing")==="border-box"):0)}}}),v.support.opacity||(v.cssHooks.opacity={get:function(e,t){return jt.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=v.isNumeric(t)?"alpha(opacity="+t*100+")":"",s=r&&r.filter||n.filter||"";n.zoom=1;if(t>=1&&v.trim(s.replace(Bt,""))===""&&n.removeAttribute){n.removeAttribute("filter");if(r&&!r.filter)return}n.filter=Bt.test(s)?s.replace(Bt,i):s+" "+i}}),v(function(){v.support.reliableMarginRight||(v.cssHooks.marginRight={get:function(e,t){return v.swap(e,{display:"inline-block"},function(){if(t)return Dt(e,"marginRight")})}}),!v.support.pixelPosition&&v.fn.position&&v.each(["top","left"],function(e,t){v.cssHooks[t]={get:function(e,n){if(n){var r=Dt(e,t);return Ut.test(r)?v(e).position()[t]+"px":r}}}})}),v.expr&&v.expr.filters&&(v.expr.filters.hidden=function(e){return e.offsetWidth===0&&e.offsetHeight===0||!v.support.reliableHiddenOffsets&&(e.style&&e.style.display||Dt(e,"display"))==="none"},v.expr.filters.visible=function(e){return!v.expr.filters.hidden(e)}),v.each({margin:"",padding:"",border:"Width"},function(e,t){v.cssHooks[e+t]={expand:function(n){var r,i=typeof n=="string"?n.split(" "):[n],s={};for(r=0;r<4;r++)s[e+$t[r]+t]=i[r]||i[r-2]||i[0];return s}},qt.test(e)||(v.cssHooks[e+t].set=Zt)});var rn=/%20/g,sn=/\[\]$/,on=/\r?\n/g,un=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,an=/^(?:select|textarea)/i;v.fn.extend({serialize:function(){return v.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?v.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||an.test(this.nodeName)||un.test(this.type))}).map(function(e,t){var n=v(this).val();return n==null?null:v.isArray(n)?v.map(n,function(e,n){return{name:t.name,value:e.replace(on,"\r\n")}}):{name:t.name,value:n.replace(on,"\r\n")}}).get()}}),v.param=function(e,n){var r,i=[],s=function(e,t){t=v.isFunction(t)?t():t==null?"":t,i[i.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};n===t&&(n=v.ajaxSettings&&v.ajaxSettings.traditional);if(v.isArray(e)||e.jquery&&!v.isPlainObject(e))v.each(e,function(){s(this.name,this.value)});else for(r in e)fn(r,e[r],n,s);return i.join("&").replace(rn,"+")};var ln,cn,hn=/#.*$/,pn=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,dn=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,vn=/^(?:GET|HEAD)$/,mn=/^\/\//,gn=/\?/,yn=/)<[^<]*)*<\/script>/gi,bn=/([?&])_=[^&]*/,wn=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,En=v.fn.load,Sn={},xn={},Tn=["*/"]+["*"];try{cn=s.href}catch(Nn){cn=i.createElement("a"),cn.href="",cn=cn.href}ln=wn.exec(cn.toLowerCase())||[],v.fn.load=function(e,n,r){if(typeof e!="string"&&En)return En.apply(this,arguments);if(!this.length)return this;var i,s,o,u=this,a=e.indexOf(" ");return a>=0&&(i=e.slice(a,e.length),e=e.slice(0,a)),v.isFunction(n)?(r=n,n=t):n&&typeof n=="object"&&(s="POST"),v.ajax({url:e,type:s,dataType:"html",data:n,complete:function(e,t){r&&u.each(r,o||[e.responseText,t,e])}}).done(function(e){o=arguments,u.html(i?v("
").append(e.replace(yn,"")).find(i):e)}),this},v.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(e,t){v.fn[t]=function(e){return this.on(t,e)}}),v.each(["get","post"],function(e,n){v[n]=function(e,r,i,s){return v.isFunction(r)&&(s=s||i,i=r,r=t),v.ajax({type:n,url:e,data:r,success:i,dataType:s})}}),v.extend({getScript:function(e,n){return v.get(e,t,n,"script")},getJSON:function(e,t,n){return v.get(e,t,n,"json")},ajaxSetup:function(e,t){return t?Ln(e,v.ajaxSettings):(t=e,e=v.ajaxSettings),Ln(e,t),e},ajaxSettings:{url:cn,isLocal:dn.test(ln[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":Tn},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":e.String,"text html":!0,"text json":v.parseJSON,"text xml":v.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:Cn(Sn),ajaxTransport:Cn(xn),ajax:function(e,n){function T(e,n,s,a){var l,y,b,w,S,T=n;if(E===2)return;E=2,u&&clearTimeout(u),o=t,i=a||"",x.readyState=e>0?4:0,s&&(w=An(c,x,s));if(e>=200&&e<300||e===304)c.ifModified&&(S=x.getResponseHeader("Last-Modified"),S&&(v.lastModified[r]=S),S=x.getResponseHeader("Etag"),S&&(v.etag[r]=S)),e===304?(T="notmodified",l=!0):(l=On(c,w),T=l.state,y=l.data,b=l.error,l=!b);else{b=T;if(!T||e)T="error",e<0&&(e=0)}x.status=e,x.statusText=(n||T)+"",l?d.resolveWith(h,[y,T,x]):d.rejectWith(h,[x,T,b]),x.statusCode(g),g=t,f&&p.trigger("ajax"+(l?"Success":"Error"),[x,c,l?y:b]),m.fireWith(h,[x,T]),f&&(p.trigger("ajaxComplete",[x,c]),--v.active||v.event.trigger("ajaxStop"))}typeof e=="object"&&(n=e,e=t),n=n||{};var r,i,s,o,u,a,f,l,c=v.ajaxSetup({},n),h=c.context||c,p=h!==c&&(h.nodeType||h instanceof v)?v(h):v.event,d=v.Deferred(),m=v.Callbacks("once memory"),g=c.statusCode||{},b={},w={},E=0,S="canceled",x={readyState:0,setRequestHeader:function(e,t){if(!E){var n=e.toLowerCase();e=w[n]=w[n]||e,b[e]=t}return this},getAllResponseHeaders:function(){return E===2?i:null},getResponseHeader:function(e){var n;if(E===2){if(!s){s={};while(n=pn.exec(i))s[n[1].toLowerCase()]=n[2]}n=s[e.toLowerCase()]}return n===t?null:n},overrideMimeType:function(e){return E||(c.mimeType=e),this},abort:function(e){return e=e||S,o&&o.abort(e),T(0,e),this}};d.promise(x),x.success=x.done,x.error=x.fail,x.complete=m.add,x.statusCode=function(e){if(e){var t;if(E<2)for(t in e)g[t]=[g[t],e[t]];else t=e[x.status],x.always(t)}return this},c.url=((e||c.url)+"").replace(hn,"").replace(mn,ln[1]+"//"),c.dataTypes=v.trim(c.dataType||"*").toLowerCase().split(y),c.crossDomain==null&&(a=wn.exec(c.url.toLowerCase()),c.crossDomain=!(!a||a[1]===ln[1]&&a[2]===ln[2]&&(a[3]||(a[1]==="http:"?80:443))==(ln[3]||(ln[1]==="http:"?80:443)))),c.data&&c.processData&&typeof c.data!="string"&&(c.data=v.param(c.data,c.traditional)),kn(Sn,c,n,x);if(E===2)return x;f=c.global,c.type=c.type.toUpperCase(),c.hasContent=!vn.test(c.type),f&&v.active++===0&&v.event.trigger("ajaxStart");if(!c.hasContent){c.data&&(c.url+=(gn.test(c.url)?"&":"?")+c.data,delete c.data),r=c.url;if(c.cache===!1){var N=v.now(),C=c.url.replace(bn,"$1_="+N);c.url=C+(C===c.url?(gn.test(c.url)?"&":"?")+"_="+N:"")}}(c.data&&c.hasContent&&c.contentType!==!1||n.contentType)&&x.setRequestHeader("Content-Type",c.contentType),c.ifModified&&(r=r||c.url,v.lastModified[r]&&x.setRequestHeader("If-Modified-Since",v.lastModified[r]),v.etag[r]&&x.setRequestHeader("If-None-Match",v.etag[r])),x.setRequestHeader("Accept",c.dataTypes[0]&&c.accepts[c.dataTypes[0]]?c.accepts[c.dataTypes[0]]+(c.dataTypes[0]!=="*"?", "+Tn+"; q=0.01":""):c.accepts["*"]);for(l in c.headers)x.setRequestHeader(l,c.headers[l]);if(!c.beforeSend||c.beforeSend.call(h,x,c)!==!1&&E!==2){S="abort";for(l in{success:1,error:1,complete:1})x[l](c[l]);o=kn(xn,c,n,x);if(!o)T(-1,"No Transport");else{x.readyState=1,f&&p.trigger("ajaxSend",[x,c]),c.async&&c.timeout>0&&(u=setTimeout(function(){x.abort("timeout")},c.timeout));try{E=1,o.send(b,T)}catch(k){if(!(E<2))throw k;T(-1,k)}}return x}return x.abort()},active:0,lastModified:{},etag:{}});var Mn=[],_n=/\?/,Dn=/(=)\?(?=&|$)|\?\?/,Pn=v.now();v.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Mn.pop()||v.expando+"_"+Pn++;return this[e]=!0,e}}),v.ajaxPrefilter("json jsonp",function(n,r,i){var s,o,u,a=n.data,f=n.url,l=n.jsonp!==!1,c=l&&Dn.test(f),h=l&&!c&&typeof a=="string"&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Dn.test(a);if(n.dataTypes[0]==="jsonp"||c||h)return s=n.jsonpCallback=v.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,o=e[s],c?n.url=f.replace(Dn,"$1"+s):h?n.data=a.replace(Dn,"$1"+s):l&&(n.url+=(_n.test(f)?"&":"?")+n.jsonp+"="+s),n.converters["script json"]=function(){return u||v.error(s+" was not called"),u[0]},n.dataTypes[0]="json",e[s]=function(){u=arguments},i.always(function(){e[s]=o,n[s]&&(n.jsonpCallback=r.jsonpCallback,Mn.push(s)),u&&v.isFunction(o)&&o(u[0]),u=o=t}),"script"}),v.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(e){return v.globalEval(e),e}}}),v.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),v.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=i.head||i.getElementsByTagName("head")[0]||i.documentElement;return{send:function(s,o){n=i.createElement("script"),n.async="async",e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,i){if(i||!n.readyState||/loaded|complete/.test(n.readyState))n.onload=n.onreadystatechange=null,r&&n.parentNode&&r.removeChild(n),n=t,i||o(200,"success")},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(0,1)}}}});var Hn,Bn=e.ActiveXObject?function(){for(var e in Hn)Hn[e](0,1)}:!1,jn=0;v.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&Fn()||In()}:Fn,function(e){v.extend(v.support,{ajax:!!e,cors:!!e&&"withCredentials"in e})}(v.ajaxSettings.xhr()),v.support.ajax&&v.ajaxTransport(function(n){if(!n.crossDomain||v.support.cors){var r;return{send:function(i,s){var o,u,a=n.xhr();n.username?a.open(n.type,n.url,n.async,n.username,n.password):a.open(n.type,n.url,n.async);if(n.xhrFields)for(u in n.xhrFields)a[u]=n.xhrFields[u];n.mimeType&&a.overrideMimeType&&a.overrideMimeType(n.mimeType),!n.crossDomain&&!i["X-Requested-With"]&&(i["X-Requested-With"]="XMLHttpRequest");try{for(u in i)a.setRequestHeader(u,i[u])}catch(f){}a.send(n.hasContent&&n.data||null),r=function(e,i){var u,f,l,c,h;try{if(r&&(i||a.readyState===4)){r=t,o&&(a.onreadystatechange=v.noop,Bn&&delete Hn[o]);if(i)a.readyState!==4&&a.abort();else{u=a.status,l=a.getAllResponseHeaders(),c={},h=a.responseXML,h&&h.documentElement&&(c.xml=h);try{c.text=a.responseText}catch(p){}try{f=a.statusText}catch(p){f=""}!u&&n.isLocal&&!n.crossDomain?u=c.text?200:404:u===1223&&(u=204)}}}catch(d){i||s(-1,d)}c&&s(u,f,c,l)},n.async?a.readyState===4?setTimeout(r,0):(o=++jn,Bn&&(Hn||(Hn={},v(e).unload(Bn)),Hn[o]=r),a.onreadystatechange=r):r()},abort:function(){r&&r(0,1)}}}});var qn,Rn,Un=/^(?:toggle|show|hide)$/,zn=new RegExp("^(?:([-+])=|)("+m+")([a-z%]*)$","i"),Wn=/queueHooks$/,Xn=[Gn],Vn={"*":[function(e,t){var n,r,i=this.createTween(e,t),s=zn.exec(t),o=i.cur(),u=+o||0,a=1,f=20;if(s){n=+s[2],r=s[3]||(v.cssNumber[e]?"":"px");if(r!=="px"&&u){u=v.css(i.elem,e,!0)||n||1;do a=a||".5",u/=a,v.style(i.elem,e,u+r);while(a!==(a=i.cur()/o)&&a!==1&&--f)}i.unit=r,i.start=u,i.end=s[1]?u+(s[1]+1)*n:n}return i}]};v.Animation=v.extend(Kn,{tweener:function(e,t){v.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;r-1,f={},l={},c,h;a?(l=i.position(),c=l.top,h=l.left):(c=parseFloat(o)||0,h=parseFloat(u)||0),v.isFunction(t)&&(t=t.call(e,n,s)),t.top!=null&&(f.top=t.top-s.top+c),t.left!=null&&(f.left=t.left-s.left+h),"using"in t?t.using.call(e,f):i.css(f)}},v.fn.extend({position:function(){if(!this[0])return;var e=this[0],t=this.offsetParent(),n=this.offset(),r=er.test(t[0].nodeName)?{top:0,left:0}:t.offset();return n.top-=parseFloat(v.css(e,"marginTop"))||0,n.left-=parseFloat(v.css(e,"marginLeft"))||0,r.top+=parseFloat(v.css(t[0],"borderTopWidth"))||0,r.left+=parseFloat(v.css(t[0],"borderLeftWidth"))||0,{top:n.top-r.top,left:n.left-r.left}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||i.body;while(e&&!er.test(e.nodeName)&&v.css(e,"position")==="static")e=e.offsetParent;return e||i.body})}}),v.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);v.fn[e]=function(i){return v.access(this,function(e,i,s){var o=tr(e);if(s===t)return o?n in o?o[n]:o.document.documentElement[i]:e[i];o?o.scrollTo(r?v(o).scrollLeft():s,r?s:v(o).scrollTop()):e[i]=s},e,i,arguments.length,null)}}),v.each({Height:"height",Width:"width"},function(e,n){v.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){v.fn[i]=function(i,s){var o=arguments.length&&(r||typeof i!="boolean"),u=r||(i===!0||s===!0?"margin":"border");return v.access(this,function(n,r,i){var s;return v.isWindow(n)?n.document.documentElement["client"+e]:n.nodeType===9?(s=n.documentElement,Math.max(n.body["scroll"+e],s["scroll"+e],n.body["offset"+e],s["offset"+e],s["client"+e])):i===t?v.css(n,r,i,u):v.style(n,r,i,u)},n,o?i:t,o,null)}})}),e.jQuery=e.$=v,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return v})})(window); \ No newline at end of file diff --git a/entry/src/main/resources/rawfile/web/js/jquery-2.1.1.min.js b/entry/src/main/resources/rawfile/web/js/jquery-2.1.1.min.js new file mode 100644 index 0000000000000000000000000000000000000000..e5ace116b6f53cfec96eb5175ecd36f727ad4047 --- /dev/null +++ b/entry/src/main/resources/rawfile/web/js/jquery-2.1.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v2.1.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.1",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="
",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+Math.random()}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c)},removeData:function(a,b){M.remove(a,b) +},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthx",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|&#?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*\s*$/g,ib={option:[1,""],thead:[1,"
","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n("