Compare commits
246 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a68fd4394 | |||
| 54996900b5 | |||
| bb08abc0b9 | |||
| 1dfbebf98e | |||
| 2e0f40ff51 | |||
| bdecdea568 | |||
| a88b8de820 | |||
| bb6f75c0a8 | |||
| 57e92e8a56 | |||
| 93b9b53acb | |||
| edb35b5a6d | |||
| bb7bc33a8a | |||
| 64c617d8ab | |||
| bcccb0c620 | |||
| e1de55cc82 | |||
| 4b501a5b63 | |||
| dc494c85f2 | |||
| 624a8d370c | |||
| 5814d666ab | |||
| 53e3a12441 | |||
| dd770ef303 | |||
| 8b3fa5c91e | |||
| 43bf47ea7b | |||
| 4be84fbf12 | |||
| e9b46e5cf6 | |||
| 6a57a3c452 | |||
| cdb49eaa1f | |||
| cb11486f66 | |||
| 0e9b446a81 | |||
| 2cf79a1adb | |||
| 5550ec8f2c | |||
| 70f3785b47 | |||
| 8c4c22c72a | |||
| cd4a316a91 | |||
| 654d489c01 | |||
| f3acf9002e | |||
| e03bd0a40d | |||
| 1167da2027 | |||
| 7cde444ffe | |||
| 67e97a89ce | |||
| 892ffc8ce4 | |||
| c5e83b1079 | |||
| fbf7f1c3f9 | |||
| 68bc57ae85 | |||
| 10d31ea0a3 | |||
| df1536ea77 | |||
| 6aff54be76 | |||
| e64ebdfaee | |||
| 7e15ff930d | |||
| b83610a653 | |||
| 9cb14838e8 | |||
| 24a0e8956c | |||
| 6fe66ad79c | |||
| 9946d15f23 | |||
| b032dcca16 | |||
| 01d6ae55a4 | |||
| f1b377bf07 | |||
| 03c5208d47 | |||
| 3ace9348f6 | |||
| e01678da93 | |||
| 3a14017729 | |||
| 98b971b4de | |||
| 2b6a683198 | |||
| 1f58d8ee27 | |||
| 4ec3d6d064 | |||
| fb4527d91b | |||
| 8c9e416175 | |||
| a83bbcaf1d | |||
| 431f3337d9 | |||
| c9b164b983 | |||
| 54d32ad910 | |||
| 0efd9fcac0 | |||
| c0a39570c9 | |||
| 9ac4b570e6 | |||
| 6689827278 | |||
| 86a24ebd0f | |||
| d2a7d18067 | |||
| b679672c0e | |||
| 90f83db9c9 | |||
| 32d74f8623 | |||
| 53cc381c76 | |||
| add107583f | |||
| e86d811fc0 | |||
| 5cff144a22 | |||
| a021adb7fd | |||
| e2a090bbba | |||
| a70c8536f9 | |||
| e52d4fc003 | |||
| 3753e3f353 | |||
| 979d8e66f2 | |||
| c5b37cc7e7 | |||
| e3989bcc2b | |||
| 2f9c512b59 | |||
| 43172cf530 | |||
| e518eacbde | |||
| c3b8b279b0 | |||
| 0dd4951be7 | |||
| 12d06bd727 | |||
| bc52345f05 | |||
| c28a313374 | |||
| c509c8e7e5 | |||
| fb89cef256 | |||
| 9ac9fd4fe9 | |||
| dfb89df4f1 | |||
| 0e572aea36 | |||
| 420cee4ee6 | |||
| 403297d967 | |||
| abba254a38 | |||
| 048107e378 | |||
| 78efe2a960 | |||
| 8077091b34 | |||
| 683e32cffb | |||
| 1c9d76e637 | |||
| ee58107caa | |||
| 01946dd4d6 | |||
| c9e16b2c39 | |||
| 77eada7dbd | |||
| f38a2ae14a | |||
| 227733d195 | |||
| 13148728b3 | |||
| 8f91ebf194 | |||
| 230c635a54 | |||
| e1f930282c | |||
| cb07fe395c | |||
| 3917284f71 | |||
| 8bfd45c095 | |||
| a001d8cfb7 | |||
| 867358ea81 | |||
| 62c3e46529 | |||
| 8a95ed8ee6 | |||
| 20caac1b6e | |||
| 48b8c69ba0 | |||
| 35d53a0684 | |||
| dbeb252fee | |||
| 21614ea891 | |||
| 4b38ccef80 | |||
| 2d4d18fd75 | |||
| 27cd9dde38 | |||
| 373da39ac2 | |||
| b7990b7df8 | |||
| 4b9047b7d8 | |||
| d226818be3 | |||
| 1ee9da79f8 | |||
| 070eb6b947 | |||
| cbb0bd5ee7 | |||
| 553a25cea7 | |||
| 53982272d6 | |||
| a32cf2344f | |||
| 934cf32163 | |||
| a17c7b9bbf | |||
| a03df3fad0 | |||
| 86cd20dcc3 | |||
| 71402b658e | |||
| 5d8d8e77f8 | |||
| 4dee30ed84 | |||
| 025676d6e7 | |||
| aad396ae38 | |||
| c798d131bb | |||
| bf3e024648 | |||
| 191f31baa7 | |||
| d3b7903af8 | |||
| 99e7d1e161 | |||
| b13166f5d9 | |||
| 80fe4458c6 | |||
| 791574c26e | |||
| ac61ebf2d5 | |||
| 4599e9897d | |||
| cb99ed0a01 | |||
| 4864d52966 | |||
| b2d61679fb | |||
| 383b3dadd5 | |||
| c65c259123 | |||
| e7e2730929 | |||
| bb9615eed0 | |||
| 18877bf80e | |||
| 778b784eb6 | |||
| 5ff900f7ec | |||
| ba31424604 | |||
| 1782111d45 | |||
| 1fa63300aa | |||
| b42c918973 | |||
| f12262ea96 | |||
| 334cf45d6d | |||
| b7bb72294a | |||
| 64ff204371 | |||
| 282367c6d5 | |||
| 36c33a5889 | |||
| 5ee7e81ff9 | |||
| f4859444dd | |||
| 73c7994cd1 | |||
| 0c74090953 | |||
| f60d54eae4 | |||
| 31bc015cdd | |||
| b028ad3604 | |||
| d2e4e35c37 | |||
| 1f37200bb6 | |||
| 77178daad3 | |||
| 1648f161d9 | |||
| 9fa6cea69b | |||
| 66b827e502 | |||
| 7755a902dd | |||
| d25b73f47d | |||
| ac2969c3f8 | |||
| ee38b2ef03 | |||
| 0f70e04e6e | |||
| 9fc1e7272e | |||
| 0d4d0b8a37 | |||
| fcd2c989a2 | |||
| e0d0d6c455 | |||
| ce1a961b99 | |||
| c71a08a9d9 | |||
| 17bfcea65a | |||
| 5e8959bab1 | |||
| 9924dc0f92 | |||
| 7388c036d7 | |||
| ad4512801f | |||
| 409b9af398 | |||
| 7cc8fd7e87 | |||
| 42c8105f13 | |||
| 9a71cc5b4e | |||
| c543b7469d | |||
| 7caac3265a | |||
| 5d68d5a246 | |||
| 7187f87eae | |||
| fb81f3e77e | |||
| 0ae49ed098 | |||
| b8e5aaf754 | |||
| aa4820c3b7 | |||
| 5d79d6e134 | |||
| fb1455a17b | |||
| c668eeba0f | |||
| 62421ee49d | |||
| e791f29ce1 | |||
| 06947cc453 | |||
| 8c97474524 | |||
| 77a8568b28 | |||
| e2dadbd7fe | |||
| 17b668a115 | |||
| a30c2b6a75 | |||
| 2660eebec2 | |||
| f415664b6d | |||
| 5092b29312 | |||
| d5be901bc2 | |||
| fdb3679cf5 | |||
| 11beb37c50 | |||
| 2bf6509e1d |
@@ -30,3 +30,6 @@ Desktop.ini
|
|||||||
*.swp
|
*.swp
|
||||||
*.class
|
*.class
|
||||||
*.jar
|
*.jar
|
||||||
|
# IntelliJ IDEA files
|
||||||
|
*.iml
|
||||||
|
.idea
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#
|
||||||
|
# Settings for post-review (used for uploading diffs to reviews.apache.org).
|
||||||
|
#
|
||||||
|
GUESS_FIELDS = True
|
||||||
|
OPEN_BROWSER = True
|
||||||
|
TARGET_GROUPS = 'cordova'
|
||||||
|
REVIEWBOARD_URL = 'http://reviews.apache.org'
|
||||||
|
|
||||||
@@ -1,5 +1,17 @@
|
|||||||
Apache Cordova
|
Apache Cordova
|
||||||
Copyright 2012 The Apache Software Foundation
|
Copyright 2012 The Apache Software Foundation
|
||||||
|
|
||||||
This product includes software developed by
|
This product includes software developed at
|
||||||
The Apache Software Foundation (http://www.apache.org)
|
The Apache Software Foundation (http://www.apache.org)
|
||||||
|
|
||||||
|
=========================================================================
|
||||||
|
== NOTICE file corresponding to the section 4 d of ==
|
||||||
|
== the Apache License, Version 2.0, ==
|
||||||
|
== in this case for the Android-specific code. ==
|
||||||
|
=========================================================================
|
||||||
|
|
||||||
|
Android Code
|
||||||
|
Copyright 2005-2008 The Android Open Source Project
|
||||||
|
|
||||||
|
This product includes software developed as part of
|
||||||
|
The Android Open Source Project (http://source.android.com).
|
||||||
|
|||||||
@@ -1,3 +1,23 @@
|
|||||||
|
<!--
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
-->
|
||||||
Cordova Android
|
Cordova Android
|
||||||
===
|
===
|
||||||
|
|
||||||
@@ -14,25 +34,8 @@ Requires
|
|||||||
- Java JDK 1.5 or greater
|
- Java JDK 1.5 or greater
|
||||||
- Apache ANT 1.8.0 or greater
|
- Apache ANT 1.8.0 or greater
|
||||||
- Android SDK [http://developer.android.com](http://developer.android.com)
|
- Android SDK [http://developer.android.com](http://developer.android.com)
|
||||||
- Apache Commons Codec [http://commons.apache.org/codec/](http://commons.apache.org/codec/)
|
|
||||||
|
|
||||||
Test Requirements
|
|
||||||
---
|
|
||||||
- JUnit - [https://github.com/KentBeck/junit](https://github.com/KentBeck/junit)
|
|
||||||
|
|
||||||
Building
|
|
||||||
---
|
|
||||||
|
|
||||||
To create your `cordova.jar` file, copy the commons codec:
|
|
||||||
|
|
||||||
mv commons-codec-1.7.jar framework/libs
|
|
||||||
|
|
||||||
then run in the framework directory:
|
|
||||||
|
|
||||||
android update project -p . -t android-17
|
|
||||||
ant jar
|
|
||||||
|
|
||||||
|
|
||||||
Cordova Android Developer Tools
|
Cordova Android Developer Tools
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -49,7 +52,6 @@ the dependencies:
|
|||||||
General Commands
|
General Commands
|
||||||
|
|
||||||
./bin/create [path package activity] ... create the ./example app or a cordova android project
|
./bin/create [path package activity] ... create the ./example app or a cordova android project
|
||||||
./bin/bench ............................ generate a bench proj
|
|
||||||
./bin/autotest ......................... test the cli tools
|
./bin/autotest ......................... test the cli tools
|
||||||
./bin/test ............................. run mobile-spec
|
./bin/test ............................. run mobile-spec
|
||||||
|
|
||||||
@@ -93,6 +95,18 @@ Importing a Cordova Android Project into Eclipse
|
|||||||
5. Right click on the project root: Run as > Run Configurations
|
5. Right click on the project root: Run as > Run Configurations
|
||||||
6. Click on the Target tab and select Manual (this way you can choose the emulator or device to build to)
|
6. Click on the Target tab and select Manual (this way you can choose the emulator or device to build to)
|
||||||
|
|
||||||
|
Building without the Tooling
|
||||||
|
---
|
||||||
|
Note: The Developer Tools handle this. This is only to be done if the tooling fails, or if
|
||||||
|
you are developing directly against the framework.
|
||||||
|
|
||||||
|
|
||||||
|
To create your `cordova.jar` file, run in the framework directory:
|
||||||
|
|
||||||
|
android update project -p . -t android-17
|
||||||
|
ant jar
|
||||||
|
|
||||||
|
|
||||||
Running Tests
|
Running Tests
|
||||||
----
|
----
|
||||||
Please see details under test/README.md.
|
Please see details under test/README.md.
|
||||||
@@ -101,5 +115,5 @@ Further Reading
|
|||||||
---
|
---
|
||||||
|
|
||||||
- [http://developer.android.com](http://developer.android.com)
|
- [http://developer.android.com](http://developer.android.com)
|
||||||
- [http://incubator.apache.org/cordova/](http://incubator.apache.org/cordova/)
|
- [http://cordova.apache.org/](http://cordova.apache.org)
|
||||||
- [http://wiki.apache.org/cordova/](http://wiki.apache.org/cordova/)
|
- [http://wiki.apache.org/cordova/](http://wiki.apache.org/cordova/)
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
ROOT="$( cd "$( dirname "$0" )/.." && pwd )"
|
||||||
|
cmd=`android list target`
|
||||||
|
if [[ $? != 0 ]]; then
|
||||||
|
echo "The command \"android\" failed. Make sure you have the latest Android SDK installed, and the \"android\" command (inside the tools/ folder) added to your path."
|
||||||
|
exit 2
|
||||||
|
elif [[ ! $cmd =~ "android-18" ]]; then
|
||||||
|
echo "Please install Android target 18 (the Android 4.3 SDK). Make sure you have the latest Android tools installed as well. Run \"android\" from your command-line to install/update any missing SDKs or tools."
|
||||||
|
exit 2
|
||||||
|
else
|
||||||
|
cmd="android update project -p $ROOT -t android-18 1> /dev/null 2>&1"
|
||||||
|
eval $cmd
|
||||||
|
if [[ $? != 0 ]]; then
|
||||||
|
echo "Error updating the Cordova library to work with your Android environment."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
:: 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.
|
||||||
|
|
||||||
|
@ECHO OFF
|
||||||
|
SET full_path=%~dp0
|
||||||
|
IF EXIST %full_path%check_reqs.js (
|
||||||
|
cscript "%full_path%check_reqs.js" //nologo
|
||||||
|
) ELSE (
|
||||||
|
ECHO.
|
||||||
|
ECHO ERROR: Could not find 'check_reqs.js' in 'bin' folder, aborting...>&2
|
||||||
|
EXIT /B 1
|
||||||
|
)
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
var ROOT = WScript.ScriptFullName.split('\\bin\\check_reqs.js').join(''),
|
||||||
|
shell = WScript.CreateObject("WScript.Shell"),
|
||||||
|
fso = WScript.CreateObject('Scripting.FileSystemObject');
|
||||||
|
|
||||||
|
|
||||||
|
// executes a command in the shell, returns stdout or stderr if error
|
||||||
|
function exec_out(command) {
|
||||||
|
var oExec=shell.Exec(command);
|
||||||
|
var output = new String();
|
||||||
|
while (oExec.Status == 0) {
|
||||||
|
if (!oExec.StdOut.AtEndOfStream) {
|
||||||
|
var line = oExec.StdOut.ReadAll();
|
||||||
|
// XXX: Change to verbose mode
|
||||||
|
// WScript.StdOut.WriteLine(line);
|
||||||
|
output += line;
|
||||||
|
}
|
||||||
|
WScript.sleep(100);
|
||||||
|
}
|
||||||
|
//Check to make sure our scripts did not encounter an error
|
||||||
|
if (!oExec.StdErr.AtEndOfStream) {
|
||||||
|
var line = oExec.StdErr.ReadAll();
|
||||||
|
return {'error' : true, 'output' : line};
|
||||||
|
} else if (!oExec.StdOut.AtEndOfStream) {
|
||||||
|
var line = oExec.StdOut.ReadAll();
|
||||||
|
// XXX: Change to verbose mode
|
||||||
|
// WScript.StdOut.WriteLine(line);
|
||||||
|
output += line;
|
||||||
|
}
|
||||||
|
return {'error' : false, 'output' : output};
|
||||||
|
}
|
||||||
|
|
||||||
|
// log to stdout or stderr
|
||||||
|
function Log(msg, error) {
|
||||||
|
if (error) {
|
||||||
|
WScript.StdErr.WriteLine(msg);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WScript.StdOut.WriteLine(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checks that android requirements are met
|
||||||
|
function check_requirements() {
|
||||||
|
var target = get_target();
|
||||||
|
if(target==null) {
|
||||||
|
Log('Unable to find android target in project.properties');
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
var result = exec_out('%comspec% /c android list target');
|
||||||
|
if(result.error) {
|
||||||
|
Log('The command `android` failed. Make sure you have the latest Android SDK installed, and the `android` command (inside the tools/ folder) added to your path. Output: ' + result.output, true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
else if(result.output.indexOf(target) == -1) {
|
||||||
|
Log(result.output.indexOf(target));
|
||||||
|
Log('Please install the latest Android target (' + target + '). Make sure you have the latest Android tools installed as well. Run `android` from your command-line to install/update any missing SDKs or tools.', true);
|
||||||
|
Log(result.output);
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var cmd = '%comspec% /c android update project -p ' + ROOT + '\\framework -t ' + target;
|
||||||
|
result = exec_out(cmd);
|
||||||
|
if(result.error) {
|
||||||
|
Log('Error updating the Cordova library to work with your Android environment. Command run: "' + cmd + '", output: ' + result.output, true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_target() {
|
||||||
|
var fso=WScript.CreateObject("Scripting.FileSystemObject");
|
||||||
|
var f=fso.OpenTextFile(ROOT + '\\framework\\project.properties', 1);
|
||||||
|
var s=f.ReadAll();
|
||||||
|
var lines = s.split('\n');
|
||||||
|
for (var line in lines) {
|
||||||
|
if(lines[line].match(/target=/))
|
||||||
|
{
|
||||||
|
return lines[line].split('=')[1].replace(' ', '').replace('\r', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
check_requirements();
|
||||||
|
|
||||||
@@ -25,8 +25,11 @@ set -e
|
|||||||
|
|
||||||
if [ -z "$1" ] || [ "$1" == "-h" ]
|
if [ -z "$1" ] || [ "$1" == "-h" ]
|
||||||
then
|
then
|
||||||
echo 'usage: create path package activity'
|
echo "Usage: $0 <path_to_new_project> <package_name> <project_name>"
|
||||||
echo "Make sure the Android SDK tools folder is in your PATH!"
|
echo "Make sure the Android SDK tools folder is in your PATH!"
|
||||||
|
echo " <path_to_new_project>: Path to your new Cordova iOS project"
|
||||||
|
echo " <package_name>: Package name, following reverse-domain style convention"
|
||||||
|
echo " <project_name>: Project name"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -35,7 +38,8 @@ VERSION=$(cat "$BUILD_PATH"/VERSION)
|
|||||||
|
|
||||||
PROJECT_PATH="${1:-'./example'}"
|
PROJECT_PATH="${1:-'./example'}"
|
||||||
PACKAGE=${2:-"org.apache.cordova.example"}
|
PACKAGE=${2:-"org.apache.cordova.example"}
|
||||||
ACTIVITY=${3:-"cordovaExample"}
|
ACTIVITY=$(echo ${3:-"cordovaExample"} | tr -d '[:blank:][:punct:]')
|
||||||
|
APP_LABEL=${3:-"Cordova Example"};
|
||||||
|
|
||||||
# clobber any existing example
|
# clobber any existing example
|
||||||
if [ -d "$PROJECT_PATH" ]
|
if [ -d "$PROJECT_PATH" ]
|
||||||
@@ -44,25 +48,11 @@ then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# cleanup after exit and/or on error
|
|
||||||
function on_exit {
|
|
||||||
# [ -f "$BUILD_PATH"/framework/libs/commons-codec-1.6.jar ] && rm "$BUILD_PATH"/framework/libs/commons-codec-1.6.jar
|
|
||||||
# [ -d "$BUILD_PATH"/framework/libs ] && rmdir "$BUILD_PATH"/framework/libs
|
|
||||||
if [ -f "$BUILD_PATH"/framework/assets/www/cordova-$VERSION.js ]
|
|
||||||
then
|
|
||||||
rm "$BUILD_PATH"/framework/assets/www/cordova-$VERSION.js
|
|
||||||
fi
|
|
||||||
if [ -f "$BUILD_PATH"/framework/cordova-$VERSION.jar ]
|
|
||||||
then
|
|
||||||
rm "$BUILD_PATH"/framework/cordova-$VERSION.jar
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function createAppInfoJar {
|
function createAppInfoJar {
|
||||||
(cd "$BUILD_PATH"/bin/templates/cordova/ApplicationInfo &&
|
pushd "$BUILD_PATH"/bin/templates/cordova/ApplicationInfo > /dev/null
|
||||||
javac ApplicationInfo.java &&
|
javac ApplicationInfo.java
|
||||||
jar -cfe ../appinfo.jar ApplicationInfo ApplicationInfo.class
|
jar -cfe ../appinfo.jar ApplicationInfo ApplicationInfo.class
|
||||||
)
|
popd > /dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
function on_error {
|
function on_error {
|
||||||
@@ -78,7 +68,7 @@ function replace {
|
|||||||
# Mac OS X requires -i argument
|
# Mac OS X requires -i argument
|
||||||
if [[ "$OSTYPE" =~ "darwin" ]]
|
if [[ "$OSTYPE" =~ "darwin" ]]
|
||||||
then
|
then
|
||||||
/usr/bin/sed -i '' -e $pattern "$filename"
|
/usr/bin/sed -i '' -e "$pattern" "$filename"
|
||||||
elif [[ "$OSTYPE" =~ "linux" ]]
|
elif [[ "$OSTYPE" =~ "linux" ]]
|
||||||
then
|
then
|
||||||
/bin/sed -i -e $pattern "$filename"
|
/bin/sed -i -e $pattern "$filename"
|
||||||
@@ -88,12 +78,12 @@ function replace {
|
|||||||
# we do not want the script to silently fail
|
# we do not want the script to silently fail
|
||||||
trap 'previous_command=$this_command; this_command=$BASH_COMMAND' DEBUG
|
trap 'previous_command=$this_command; this_command=$BASH_COMMAND' DEBUG
|
||||||
trap on_error ERR
|
trap on_error ERR
|
||||||
trap on_exit EXIT
|
|
||||||
|
|
||||||
ANDROID_BIN="${ANDROID_BIN:=$( which android )}"
|
ANDROID_BIN="${ANDROID_BIN:=$( which android )}"
|
||||||
PACKAGE_AS_PATH=$(echo $PACKAGE | sed 's/\./\//g')
|
PACKAGE_AS_PATH=$(echo $PACKAGE | sed 's/\./\//g')
|
||||||
ACTIVITY_PATH="$PROJECT_PATH"/src/$PACKAGE_AS_PATH/$ACTIVITY.java
|
ACTIVITY_PATH="$PROJECT_PATH"/src/$PACKAGE_AS_PATH/$ACTIVITY.java
|
||||||
MANIFEST_PATH="$PROJECT_PATH"/AndroidManifest.xml
|
MANIFEST_PATH="$PROJECT_PATH"/AndroidManifest.xml
|
||||||
|
STRINGS_PATH="$PROJECT_PATH"/res/values/strings.xml
|
||||||
|
|
||||||
TARGET=$("$ANDROID_BIN" list targets | grep id: | tail -1 | cut -f 2 -d ' ' )
|
TARGET=$("$ANDROID_BIN" list targets | grep id: | tail -1 | cut -f 2 -d ' ' )
|
||||||
API_LEVEL=$("$ANDROID_BIN" list target | grep "API level:" | tail -n 1 | cut -f 2 -d ':' | tr -d ' ')
|
API_LEVEL=$("$ANDROID_BIN" list target | grep "API level:" | tail -n 1 | cut -f 2 -d ':' | tr -d ' ')
|
||||||
@@ -108,21 +98,13 @@ fi
|
|||||||
# if this a distribution release no need to build a jar
|
# if this a distribution release no need to build a jar
|
||||||
if [ ! -e "$BUILD_PATH"/cordova-$VERSION.jar ] && [ -d "$BUILD_PATH"/framework ]
|
if [ ! -e "$BUILD_PATH"/cordova-$VERSION.jar ] && [ -d "$BUILD_PATH"/framework ]
|
||||||
then
|
then
|
||||||
# update the cordova-android framework for the desired target
|
# update the cordova-android framework for the desired target
|
||||||
"$ANDROID_BIN" update project --target $TARGET --path "$BUILD_PATH"/framework &> /dev/null
|
"$ANDROID_BIN" update project --target $TARGET --path "$BUILD_PATH"/framework &> /dev/null
|
||||||
|
|
||||||
if [ ! -e "$BUILD_PATH"/framework/libs/commons-codec-1.7.jar ]; then
|
# compile cordova.js and cordova.jar
|
||||||
# Use curl to get the jar (TODO: Support Apache Mirrors)
|
pushd "$BUILD_PATH"/framework > /dev/null
|
||||||
curl -OL http://archive.apache.org/dist/commons/codec/binaries/commons-codec-1.7-bin.zip &> /dev/null
|
ant jar > /dev/null
|
||||||
unzip commons-codec-1.7-bin.zip &> /dev/null
|
popd > /dev/null
|
||||||
mkdir -p "$BUILD_PATH"/framework/libs
|
|
||||||
cp commons-codec-1.7/commons-codec-1.7.jar "$BUILD_PATH"/framework/libs
|
|
||||||
# cleanup yo
|
|
||||||
rm commons-codec-1.7-bin.zip && rm -rf commons-codec-1.7
|
|
||||||
fi
|
|
||||||
|
|
||||||
# compile cordova.js and cordova.jar
|
|
||||||
(cd "$BUILD_PATH"/framework && ant jar &> /dev/null )
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# create new android project
|
# create new android project
|
||||||
@@ -136,11 +118,11 @@ cp -r "$BUILD_PATH"/bin/templates/project/res "$PROJECT_PATH"
|
|||||||
if [ -d "$BUILD_PATH"/framework ]
|
if [ -d "$BUILD_PATH"/framework ]
|
||||||
then
|
then
|
||||||
cp -r "$BUILD_PATH"/framework/res/xml "$PROJECT_PATH"/res
|
cp -r "$BUILD_PATH"/framework/res/xml "$PROJECT_PATH"/res
|
||||||
cp "$BUILD_PATH"/framework/assets/www/cordova-$VERSION.js "$PROJECT_PATH"/assets/www/cordova-$VERSION.js
|
cp "$BUILD_PATH"/framework/assets/www/cordova.js "$PROJECT_PATH"/assets/www/cordova.js
|
||||||
cp "$BUILD_PATH"/framework/cordova-$VERSION.jar "$PROJECT_PATH"/libs/cordova-$VERSION.jar
|
cp "$BUILD_PATH"/framework/cordova-$VERSION.jar "$PROJECT_PATH"/libs/cordova-$VERSION.jar
|
||||||
else
|
else
|
||||||
cp -r "$BUILD_PATH"/xml "$PROJECT_PATH"/res/xml
|
cp -r "$BUILD_PATH"/xml "$PROJECT_PATH"/res/xml
|
||||||
cp "$BUILD_PATH"/cordova-$VERSION.js "$PROJECT_PATH"/assets/www/cordova-$VERSION.js
|
cp "$BUILD_PATH"/cordova.js "$PROJECT_PATH"/assets/www/cordova.js
|
||||||
cp "$BUILD_PATH"/cordova-$VERSION.jar "$PROJECT_PATH"/libs/cordova-$VERSION.jar
|
cp "$BUILD_PATH"/cordova-$VERSION.jar "$PROJECT_PATH"/libs/cordova-$VERSION.jar
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -149,6 +131,9 @@ cp "$BUILD_PATH"/bin/templates/project/Activity.java "$ACTIVITY_PATH"
|
|||||||
replace "s/__ACTIVITY__/${ACTIVITY}/g" "$ACTIVITY_PATH"
|
replace "s/__ACTIVITY__/${ACTIVITY}/g" "$ACTIVITY_PATH"
|
||||||
replace "s/__ID__/${PACKAGE}/g" "$ACTIVITY_PATH"
|
replace "s/__ID__/${PACKAGE}/g" "$ACTIVITY_PATH"
|
||||||
|
|
||||||
|
# interpolate the app name into strings.xml
|
||||||
|
replace "s/>${ACTIVITY}</>${APP_LABEL}</g" "$STRINGS_PATH"
|
||||||
|
|
||||||
cp "$BUILD_PATH"/bin/templates/project/AndroidManifest.xml "$MANIFEST_PATH"
|
cp "$BUILD_PATH"/bin/templates/project/AndroidManifest.xml "$MANIFEST_PATH"
|
||||||
replace "s/__ACTIVITY__/${ACTIVITY}/g" "$MANIFEST_PATH"
|
replace "s/__ACTIVITY__/${ACTIVITY}/g" "$MANIFEST_PATH"
|
||||||
replace "s/__PACKAGE__/${PACKAGE}/g" "$MANIFEST_PATH"
|
replace "s/__PACKAGE__/${PACKAGE}/g" "$MANIFEST_PATH"
|
||||||
@@ -156,11 +141,18 @@ replace "s/__APILEVEL__/${API_LEVEL}/g" "$MANIFEST_PATH"
|
|||||||
|
|
||||||
# creating cordova folder and copying run/build/log/launch scripts
|
# creating cordova folder and copying run/build/log/launch scripts
|
||||||
mkdir "$PROJECT_PATH"/cordova
|
mkdir "$PROJECT_PATH"/cordova
|
||||||
|
mkdir "$PROJECT_PATH"/cordova/lib
|
||||||
createAppInfoJar
|
createAppInfoJar
|
||||||
cp "$BUILD_PATH"/bin/templates/cordova/appinfo.jar "$PROJECT_PATH"/cordova/appinfo.jar
|
cp "$BUILD_PATH"/bin/templates/cordova/appinfo.jar "$PROJECT_PATH"/cordova/appinfo.jar
|
||||||
cp "$BUILD_PATH"/bin/templates/cordova/cordova "$PROJECT_PATH"/cordova/cordova
|
|
||||||
cp "$BUILD_PATH"/bin/templates/cordova/build "$PROJECT_PATH"/cordova/build
|
cp "$BUILD_PATH"/bin/templates/cordova/build "$PROJECT_PATH"/cordova/build
|
||||||
cp "$BUILD_PATH"/bin/templates/cordova/release "$PROJECT_PATH"/cordova/release
|
|
||||||
cp "$BUILD_PATH"/bin/templates/cordova/clean "$PROJECT_PATH"/cordova/clean
|
cp "$BUILD_PATH"/bin/templates/cordova/clean "$PROJECT_PATH"/cordova/clean
|
||||||
cp "$BUILD_PATH"/bin/templates/cordova/log "$PROJECT_PATH"/cordova/log
|
cp "$BUILD_PATH"/bin/templates/cordova/log "$PROJECT_PATH"/cordova/log
|
||||||
cp "$BUILD_PATH"/bin/templates/cordova/run "$PROJECT_PATH"/cordova/run
|
cp "$BUILD_PATH"/bin/templates/cordova/run "$PROJECT_PATH"/cordova/run
|
||||||
|
cp "$BUILD_PATH"/bin/templates/cordova/version "$PROJECT_PATH"/cordova/version
|
||||||
|
cp "$BUILD_PATH"/bin/templates/cordova/lib/install-device "$PROJECT_PATH"/cordova/lib/install-device
|
||||||
|
cp "$BUILD_PATH"/bin/templates/cordova/lib/install-emulator "$PROJECT_PATH"/cordova/lib/install-emulator
|
||||||
|
cp "$BUILD_PATH"/bin/templates/cordova/lib/list-devices "$PROJECT_PATH"/cordova/lib/list-devices
|
||||||
|
cp "$BUILD_PATH"/bin/templates/cordova/lib/list-emulator-images "$PROJECT_PATH"/cordova/lib/list-emulator-images
|
||||||
|
cp "$BUILD_PATH"/bin/templates/cordova/lib/list-started-emulators "$PROJECT_PATH"/cordova/lib/list-started-emulators
|
||||||
|
cp "$BUILD_PATH"/bin/templates/cordova/lib/start-emulator "$PROJECT_PATH"/cordova/lib/start-emulator
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
GOTO BEGIN
|
||||||
:: Licensed to the Apache Software Foundation (ASF) under one
|
:: Licensed to the Apache Software Foundation (ASF) under one
|
||||||
:: or more contributor license agreements. See the NOTICE file
|
:: or more contributor license agreements. See the NOTICE file
|
||||||
:: distributed with this work for additional information
|
:: distributed with this work for additional information
|
||||||
@@ -15,18 +17,38 @@
|
|||||||
:: specific language governing permissions and limitations
|
:: specific language governing permissions and limitations
|
||||||
:: under the License.
|
:: under the License.
|
||||||
|
|
||||||
@ECHO OFF
|
:BEGIN
|
||||||
IF NOT DEFINED JAVA_HOME GOTO MISSING
|
IF NOT DEFINED JAVA_HOME GOTO MISSING_JAVA_HOME
|
||||||
FOR %%X in (java.exe javac.exe ant.bat android.bat) do (
|
|
||||||
SET FOUND=%%~$PATH:X
|
FOR %%X in (java.exe javac.exe ant.bat android.bat) do (
|
||||||
IF NOT DEFINED FOUND GOTO MISSING
|
IF [%%~$PATH:X]==[] (
|
||||||
)
|
ECHO Cannot locate %%X using the PATH environment variable.
|
||||||
cscript "%~dp0\create.js" %*
|
ECHO Retry after adding directory containing %%X to the PATH variable.
|
||||||
GOTO END
|
ECHO Remember to open a new command window after updating the PATH variable.
|
||||||
:MISSING
|
IF "%%X"=="java.exe" GOTO GET_JAVA
|
||||||
ECHO Missing one of the following:
|
IF "%%X"=="javac.exe" GOTO GET_JAVA
|
||||||
ECHO JDK: http://java.oracle.com
|
IF "%%X"=="ant.bat" GOTO GET_ANT
|
||||||
ECHO Android SDK: http://developer.android.com
|
IF "%%X"=="android.bat" GOTO GET_ANDROID
|
||||||
ECHO Apache ant: http://ant.apache.org
|
GOTO ERROR
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cscript "%~dp0\create.js" %* //nologo
|
||||||
|
GOTO END
|
||||||
|
:MISSING_JAVA_HOME
|
||||||
|
ECHO The JAVA_HOME environment variable is not set.
|
||||||
|
ECHO Set JAVA_HOME to an existing JRE directory.
|
||||||
|
ECHO Remember to also add JAVA_HOME to the PATH variable.
|
||||||
|
ECHO After updating system variables, open a new command window and retry.
|
||||||
|
GOTO ERROR
|
||||||
|
:GET_JAVA
|
||||||
|
ECHO Visit http://java.oracle.com if you need to install Java (JDK).
|
||||||
|
GOTO ERROR
|
||||||
|
:GET_ANT
|
||||||
|
ECHO Visit http://ant.apache.org if you need to install Apache Ant.
|
||||||
|
GOTO ERROR
|
||||||
|
:GET_ANDROID
|
||||||
|
ECHO Visit http://developer.android.com if you need to install the Android SDK.
|
||||||
|
GOTO ERROR
|
||||||
|
:ERROR
|
||||||
EXIT /B 1
|
EXIT /B 1
|
||||||
:END
|
:END
|
||||||
|
|||||||
@@ -1,224 +1,214 @@
|
|||||||
/*
|
/*
|
||||||
Licensed to the Apache Software Foundation (ASF) under one
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
or more contributor license agreements. See the NOTICE file
|
or more contributor license agreements. See the NOTICE file
|
||||||
distributed with this work for additional information
|
distributed with this work for additional information
|
||||||
regarding copyright ownership. The ASF licenses this file
|
regarding copyright ownership. The ASF licenses this file
|
||||||
to you under the Apache License, Version 2.0 (the
|
to you under the Apache License, Version 2.0 (the
|
||||||
"License"); you may not use this file except in compliance
|
"License"); you may not use this file except in compliance
|
||||||
with the License. You may obtain a copy of the License at
|
with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing,
|
Unless required by applicable law or agreed to in writing,
|
||||||
software distributed under the License is distributed on an
|
software distributed under the License is distributed on an
|
||||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
KIND, either express or implied. See the License for the
|
KIND, either express or implied. See the License for the
|
||||||
specific language governing permissions and limitations
|
specific language governing permissions and limitations
|
||||||
under the License.
|
under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* create a cordova/android project
|
* create a cordova/android project
|
||||||
*
|
*
|
||||||
* USAGE
|
* USAGE
|
||||||
* ./create [path package activity]
|
* ./create [path package activity]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var fso = WScript.CreateObject('Scripting.FileSystemObject');
|
var args = WScript.Arguments, PROJECT_PATH="example",
|
||||||
|
PACKAGE="org.apache.cordova.example", ACTIVITY="cordovaExample",
|
||||||
function read(filename) {
|
shell=WScript.CreateObject("WScript.Shell"),
|
||||||
var fso=WScript.CreateObject("Scripting.FileSystemObject");
|
fso = WScript.CreateObject('Scripting.FileSystemObject');
|
||||||
var f=fso.OpenTextFile(filename, 1);
|
|
||||||
var s=f.ReadAll();
|
function Usage() {
|
||||||
f.Close();
|
Log("Usage: create PathTONewProject [ PackageName AppName ]");
|
||||||
return s;
|
Log(" PathTONewProject : The path to where you wish to create the project");
|
||||||
}
|
Log(" PackageName : The package for the project (default is org.apache.cordova.example)")
|
||||||
|
Log(" AppName : The name of the application/activity (default is cordovaExample)");
|
||||||
function checkTargets(targets) {
|
Log("examples:");
|
||||||
if(!targets) {
|
Log(" create C:\\Users\\anonymous\\Desktop\\MyProject");
|
||||||
WScript.Echo("You do not have any android targets setup. Please create at least one target with the `android` command");
|
Log(" create C:\\Users\\anonymous\\Desktop\\MyProject io.Cordova.Example AnApp");
|
||||||
WScript.Quit(69);
|
}
|
||||||
}
|
|
||||||
}
|
// logs messaged to stdout and stderr
|
||||||
|
function Log(msg, error) {
|
||||||
function setTarget() {
|
if (error) {
|
||||||
var targets = shell.Exec('android.bat list targets').StdOut.ReadAll().match(/id:\s\d+/g);
|
WScript.StdErr.WriteLine(msg);
|
||||||
checkTargets(targets);
|
}
|
||||||
return targets[targets.length - 1].replace(/id: /, ""); // TODO: give users the option to set their target
|
else {
|
||||||
}
|
WScript.StdOut.WriteLine(msg);
|
||||||
function setApiLevel() {
|
}
|
||||||
var targets = shell.Exec('android.bat list targets').StdOut.ReadAll().match(/API level:\s\d+/g);
|
}
|
||||||
checkTargets(targets);
|
|
||||||
return targets[targets.length - 1].replace(/API level: /, "");
|
function read(filename) {
|
||||||
}
|
var fso=WScript.CreateObject("Scripting.FileSystemObject");
|
||||||
function write(filename, contents) {
|
var f=fso.OpenTextFile(filename, 1);
|
||||||
var fso=WScript.CreateObject("Scripting.FileSystemObject");
|
var s=f.ReadAll();
|
||||||
var f=fso.OpenTextFile(filename, 2, true);
|
f.Close();
|
||||||
f.Write(contents);
|
return s;
|
||||||
f.Close();
|
}
|
||||||
}
|
|
||||||
function replaceInFile(filename, regexp, replacement) {
|
function checkTargets(targets) {
|
||||||
write(filename, read(filename).replace(regexp, replacement));
|
if(!targets) {
|
||||||
}
|
Log("You do not have any android targets setup. Please create at least one target with the `android` command", true);
|
||||||
function exec(command) {
|
WScript.Quit(69);
|
||||||
var oShell=shell.Exec(command);
|
}
|
||||||
while (oShell.Status == 0) {
|
}
|
||||||
if(!oShell.StdOut.AtEndOfStream) {
|
|
||||||
var line = oShell.StdOut.ReadLine();
|
function setTarget() {
|
||||||
// XXX: Change to verbose mode
|
var targets = shell.Exec('android.bat list targets').StdOut.ReadAll().match(/id:\s\d+/g);
|
||||||
// WScript.StdOut.WriteLine(line);
|
checkTargets(targets);
|
||||||
}
|
return targets[targets.length - 1].replace(/id: /, ""); // TODO: give users the option to set their target
|
||||||
WScript.sleep(100);
|
}
|
||||||
}
|
function setApiLevel() {
|
||||||
}
|
var targets = shell.Exec('android.bat list targets').StdOut.ReadAll().match(/API level:\s\d+/g);
|
||||||
|
checkTargets(targets);
|
||||||
function createAppInfoJar() {
|
return targets[targets.length - 1].replace(/API level: /, "");
|
||||||
if(!fso.FileExists(ROOT+"\\bin\\templates\\cordova\\appinfo.jar")) {
|
}
|
||||||
WScript.Echo("Creating appinfo.jar...");
|
function write(filename, contents) {
|
||||||
var cur = shell.CurrentDirectory;
|
var fso=WScript.CreateObject("Scripting.FileSystemObject");
|
||||||
shell.CurrentDirectory = ROOT+"\\bin\\templates\\cordova\\ApplicationInfo";
|
var f=fso.OpenTextFile(filename, 2, true);
|
||||||
exec("javac ApplicationInfo.java");
|
f.Write(contents);
|
||||||
exec("jar -cfe ..\\appinfo.jar ApplicationInfo ApplicationInfo.class");
|
f.Close();
|
||||||
shell.CurrentDirectory = cur;
|
}
|
||||||
}
|
function replaceInFile(filename, regexp, replacement) {
|
||||||
}
|
write(filename, read(filename).replace(regexp, replacement));
|
||||||
|
}
|
||||||
function cleanup() {
|
function exec(command) {
|
||||||
// Cleanup
|
var oShell=shell.Exec(command);
|
||||||
// if(fso.FileExists(ROOT + '\\framework\\libs\\commons-codec-1.6.jar')) {
|
while (oShell.Status == 0) {
|
||||||
// fso.DeleteFile(ROOT + '\\framework\\libs\\commons-codec-1.6.jar');
|
if(!oShell.StdOut.AtEndOfStream) {
|
||||||
// fso.DeleteFolder(ROOT + '\\framework\\libs', true);
|
var line = oShell.StdOut.ReadLine();
|
||||||
// }
|
// XXX: Change to verbose mode
|
||||||
if(fso.FileExists(ROOT + '\\framework\\cordova-'+VERSION+'.jar')) {
|
// WScript.StdOut.WriteLine(line);
|
||||||
fso.DeleteFile(ROOT + '\\framework\\cordova-'+VERSION+'.jar');
|
}
|
||||||
}
|
WScript.sleep(100);
|
||||||
if(fso.FileExists(ROOT + '\\framework\\assets\\www\\cordova-'+VERSION+'.js')) {
|
}
|
||||||
fso.DeleteFile(ROOT + '\\framework\\assets\\www\\cordova-'+VERSION+'.js');
|
}
|
||||||
}
|
|
||||||
}
|
function createAppInfoJar() {
|
||||||
|
if(!fso.FileExists(ROOT+"\\bin\\templates\\cordova\\appinfo.jar")) {
|
||||||
function downloadCommonsCodec() {
|
Log("Creating appinfo.jar...");
|
||||||
if (!fso.FileExists(ROOT + '\\framework\\libs\\commons-codec-1.7.jar')) {
|
var cur = shell.CurrentDirectory;
|
||||||
// We need the .jar
|
shell.CurrentDirectory = ROOT+"\\bin\\templates\\cordova\\ApplicationInfo";
|
||||||
var url = 'http://archive.apache.org/dist/commons/codec/binaries/commons-codec-1.7-bin.zip';
|
exec("javac ApplicationInfo.java");
|
||||||
var libsPath = ROOT + '\\framework\\libs';
|
exec("jar -cfe ..\\appinfo.jar ApplicationInfo ApplicationInfo.class");
|
||||||
var savePath = libsPath + '\\commons-codec-1.7-bin.zip';
|
shell.CurrentDirectory = cur;
|
||||||
if (!fso.FileExists(savePath)) {
|
}
|
||||||
if(!fso.FolderExists(ROOT + '\\framework\\libs')) {
|
}
|
||||||
fso.CreateFolder(libsPath);
|
|
||||||
}
|
// working dir
|
||||||
// We need the zip to get the jar
|
var ROOT = WScript.ScriptFullName.split('\\bin\\create.js').join('');
|
||||||
var xhr = WScript.CreateObject('MSXML2.XMLHTTP');
|
if (args.Count() > 0) {
|
||||||
xhr.open('GET', url, false);
|
// support help flags
|
||||||
xhr.send();
|
if (args(0) == "--help" || args(0) == "/?" ||
|
||||||
if (xhr.status == 200) {
|
args(0) == "help" || args(0) == "-help" || args(0) == "/help" || args(0) == "-h") {
|
||||||
var stream = WScript.CreateObject('ADODB.Stream');
|
Usage();
|
||||||
stream.Open();
|
WScript.Quit(2);
|
||||||
stream.Type = 1;
|
}
|
||||||
stream.Write(xhr.ResponseBody);
|
|
||||||
stream.Position = 0;
|
PROJECT_PATH=args(0);
|
||||||
stream.SaveToFile(savePath);
|
if (args.Count() > 1) {
|
||||||
stream.Close();
|
PACKAGE = args(1);
|
||||||
} else {
|
}
|
||||||
WScript.Echo('Could not retrieve the commons-codec. Please download it yourself and put into the framework/libs directory. This process may fail now. Sorry.');
|
if (args.Count() > 2) {
|
||||||
}
|
ACTIVITY = args(2);
|
||||||
}
|
}
|
||||||
var app = WScript.CreateObject('Shell.Application');
|
}
|
||||||
var source = app.NameSpace(savePath).Items();
|
else {
|
||||||
var target = app.NameSpace(ROOT + '\\framework\\libs');
|
Log("Error : No project path provided.");
|
||||||
target.CopyHere(source, 256);
|
Usage();
|
||||||
|
WScript.Quit(2);
|
||||||
// Move the jar into libs
|
}
|
||||||
fso.MoveFile(ROOT + '\\framework\\libs\\commons-codec-1.7\\commons-codec-1.7.jar', ROOT + '\\framework\\libs\\commons-codec-1.7.jar');
|
|
||||||
|
if(fso.FolderExists(PROJECT_PATH)) {
|
||||||
// Clean up
|
Log("Project path already exists!", true);
|
||||||
fso.DeleteFile(ROOT + '\\framework\\libs\\commons-codec-1.7-bin.zip');
|
WScript.Quit(2);
|
||||||
fso.DeleteFolder(ROOT + '\\framework\\libs\\commons-codec-1.7', true);
|
}
|
||||||
}
|
|
||||||
}
|
var PACKAGE_AS_PATH=PACKAGE.replace(/\./g, '\\');
|
||||||
|
var ACTIVITY_DIR=PROJECT_PATH + '\\src\\' + PACKAGE_AS_PATH;
|
||||||
var args = WScript.Arguments, PROJECT_PATH="example",
|
var SAFE_ACTIVITY = ACTIVITY.replace(/\W/g, '');
|
||||||
PACKAGE="org.apache.cordova.example", ACTIVITY="cordovaExample",
|
var ACTIVITY_PATH=ACTIVITY_DIR+'\\'+SAFE_ACTIVITY+'.java';
|
||||||
shell=WScript.CreateObject("WScript.Shell");
|
var MANIFEST_PATH=PROJECT_PATH+'\\AndroidManifest.xml';
|
||||||
|
var STRINGS_PATH=PROJECT_PATH+'\\res\\values\\strings.xml';
|
||||||
// working dir
|
var TARGET=setTarget();
|
||||||
var ROOT = WScript.ScriptFullName.split('\\bin\\create.js').join('');
|
var API_LEVEL=setApiLevel();
|
||||||
|
var VERSION=read(ROOT+'\\VERSION').replace(/\r\n/,'').replace(/\n/,'');
|
||||||
if (args.Count() == 3) {
|
// create the project
|
||||||
PROJECT_PATH=args(0);
|
Log("Creating new android project...");
|
||||||
PACKAGE=args(1);
|
exec('android.bat create project --target "'+TARGET+'" --path "'+PROJECT_PATH+'" --package "'+PACKAGE+'" --activity "'+SAFE_ACTIVITY+'"');
|
||||||
ACTIVITY=args(2);
|
|
||||||
}
|
// build from source. distro should have these files
|
||||||
|
if (!fso.FileExists(ROOT+'\\cordova-'+VERSION+'.jar') &&
|
||||||
if(fso.FolderExists(PROJECT_PATH)) {
|
!fso.FileExists(ROOT+'\\cordova.js')) {
|
||||||
WScript.Echo("Project already exists!");
|
Log("Building jar and js files...");
|
||||||
WScript.Quit(1);
|
// update the cordova framework project to a target that exists on this machine
|
||||||
}
|
exec('android.bat update project --target "'+TARGET+'" --path "'+ROOT+'\\framework"');
|
||||||
|
exec('ant.bat -f "'+ ROOT +'\\framework\\build.xml" jar');
|
||||||
var PACKAGE_AS_PATH=PACKAGE.replace(/\./g, '\\');
|
}
|
||||||
var ACTIVITY_PATH=PROJECT_PATH+'\\src\\'+PACKAGE_AS_PATH+'\\'+ACTIVITY+'.java';
|
|
||||||
var MANIFEST_PATH=PROJECT_PATH+'\\AndroidManifest.xml';
|
// copy in the project template
|
||||||
var TARGET=setTarget();
|
Log("Copying template files...");
|
||||||
var API_LEVEL=setApiLevel();
|
exec('%comspec% /c xcopy "'+ ROOT + '\\bin\\templates\\project\\res" "'+PROJECT_PATH+'\\res\\" /E /Y');
|
||||||
var VERSION=read(ROOT+'\\VERSION').replace(/\r\n/,'').replace(/\n/,'');
|
exec('%comspec% /c xcopy "'+ ROOT + '\\bin\\templates\\project\\assets" "'+PROJECT_PATH+'\\assets\\" /E /Y');
|
||||||
// create the project
|
exec('%comspec% /c copy "'+ROOT+'\\bin\\templates\\project\\AndroidManifest.xml" "' + PROJECT_PATH + '\\AndroidManifest.xml" /Y');
|
||||||
WScript.Echo("Creating new android project...");
|
exec('%comspec% /c mkdir "' + ACTIVITY_DIR + '"');
|
||||||
exec('android.bat create project --target '+TARGET+' --path '+PROJECT_PATH+' --package '+PACKAGE+' --activity '+ACTIVITY);
|
exec('%comspec% /c copy "' + ROOT + '"\\bin\\templates\\project\\Activity.java "' + ACTIVITY_PATH + '" /Y');
|
||||||
|
|
||||||
// build from source. distro should have these files
|
// check if we have the source or the distro files
|
||||||
if (!fso.FileExists(ROOT+'\\cordova-'+VERSION+'.jar') &&
|
Log("Copying js, jar & config.xml files...");
|
||||||
!fso.FileExists(ROOT+'\\cordova-'+VERSION+'.js')) {
|
if(fso.FolderExists(ROOT + '\\framework')) {
|
||||||
WScript.Echo("Building jar and js files...");
|
exec('%comspec% /c copy "'+ROOT+'\\framework\\assets\\www\\cordova.js" "'+PROJECT_PATH+'\\assets\\www\\cordova.js" /Y');
|
||||||
// update the cordova framework project to a target that exists on this machine
|
exec('%comspec% /c copy "'+ROOT+'\\framework\\cordova-'+VERSION+'.jar" "'+PROJECT_PATH+'\\libs\\cordova-'+VERSION+'.jar" /Y');
|
||||||
exec('android.bat update project --target '+TARGET+' --path '+ROOT+'\\framework');
|
fso.CreateFolder(PROJECT_PATH + '\\res\\xml');
|
||||||
// pull down commons codec if necessary
|
exec('%comspec% /c copy "'+ROOT+'\\framework\\res\\xml\\config.xml" "' + PROJECT_PATH + '\\res\\xml\\config.xml" /Y');
|
||||||
downloadCommonsCodec();
|
} else {
|
||||||
exec('ant.bat -f \"'+ ROOT +'\\framework\\build.xml\" jar');
|
// copy in cordova.js
|
||||||
}
|
exec('%comspec% /c copy "'+ROOT+'\\cordova.js" "'+PROJECT_PATH+'\\assets\\www\\cordova.js" /Y');
|
||||||
|
// copy in cordova.jar
|
||||||
// copy in the project template
|
exec('%comspec% /c copy "'+ROOT+'\\cordova-'+VERSION+'.jar" "'+PROJECT_PATH+'\\libs\\cordova-'+VERSION+'.jar" /Y');
|
||||||
WScript.Echo("Copying template files...");
|
// copy in xml
|
||||||
exec('%comspec% /c xcopy "'+ ROOT + '"\\bin\\templates\\project\\res '+PROJECT_PATH+'\\res\\ /E /Y');
|
fso.CreateFolder(PROJECT_PATH + '\\res\\xml');
|
||||||
exec('%comspec% /c xcopy "'+ ROOT + '"\\bin\\templates\\project\\assets '+PROJECT_PATH+'\\assets\\ /E /Y');
|
exec('%comspec% /c copy "'+ROOT+'\\xml\\config.xml" "' + PROJECT_PATH + '\\res\\xml\\config.xml" /Y');
|
||||||
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\project\\AndroidManifest.xml ' + PROJECT_PATH + '\\AndroidManifest.xml /Y');
|
}
|
||||||
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\project\\Activity.java '+ ACTIVITY_PATH +' /Y');
|
|
||||||
|
// copy cordova scripts
|
||||||
// check if we have the source or the distro files
|
fso.CreateFolder(PROJECT_PATH + '\\cordova');
|
||||||
WScript.Echo("Copying js, jar & config.xml files...");
|
fso.CreateFolder(PROJECT_PATH + '\\cordova\\lib');
|
||||||
if(fso.FolderExists(ROOT + '\\framework')) {
|
createAppInfoJar();
|
||||||
exec('%comspec% /c copy "'+ROOT+'"\\framework\\assets\\www\\cordova-'+VERSION+'.js '+PROJECT_PATH+'\\assets\\www\\cordova-'+VERSION+'.js /Y');
|
Log("Copying cordova command tools...");
|
||||||
exec('%comspec% /c copy "'+ROOT+'"\\framework\\cordova-'+VERSION+'.jar '+PROJECT_PATH+'\\libs\\cordova-'+VERSION+'.jar /Y');
|
exec('%comspec% /c copy "'+ROOT+'\\bin\\templates\\cordova\\appinfo.jar" "' + PROJECT_PATH + '\\cordova\\appinfo.jar" /Y');
|
||||||
fso.CreateFolder(PROJECT_PATH + '\\res\\xml');
|
exec('%comspec% /c copy "'+ROOT+'\\bin\\templates\\cordova\\lib\\cordova.js" "' + PROJECT_PATH + '\\cordova\\lib\\cordova.js" /Y');
|
||||||
exec('%comspec% /c copy "'+ROOT+'"\\framework\\res\\xml\\config.xml ' + PROJECT_PATH + '\\res\\xml\\config.xml /Y');
|
exec('%comspec% /c copy "'+ROOT+'\\bin\\templates\\cordova\\lib\\install-device.bat" "' + PROJECT_PATH + '\\cordova\\lib\\install-device.bat" /Y');
|
||||||
} else {
|
exec('%comspec% /c copy "'+ROOT+'\\bin\\templates\\cordova\\lib\\install-emulator.bat" "' + PROJECT_PATH + '\\cordova\\lib\\install-emulator.bat" /Y');
|
||||||
// copy in cordova.js
|
exec('%comspec% /c copy "'+ROOT+'\\bin\\templates\\cordova\\lib\\list-emulator-images.bat" "' + PROJECT_PATH + '\\cordova\\lib\\list-emulator-images.bat" /Y');
|
||||||
exec('%comspec% /c copy "'+ROOT+'"\\cordova-'+VERSION+'.js '+PROJECT_PATH+'\\assets\\www\\cordova-'+VERSION+'.js /Y');
|
exec('%comspec% /c copy "'+ROOT+'\\bin\\templates\\cordova\\lib\\list-devices.bat" "' + PROJECT_PATH + '\\cordova\\lib\\list-devices.bat" /Y');
|
||||||
// copy in cordova.jar
|
exec('%comspec% /c copy "'+ROOT+'\\bin\\templates\\cordova\\lib\\list-started-emulators.bat" "' + PROJECT_PATH + '\\cordova\\lib\\list-started-emulators.bat" /Y');
|
||||||
exec('%comspec% /c copy "'+ROOT+'"\\cordova-'+VERSION+'.jar '+PROJECT_PATH+'\\libs\\cordova-'+VERSION+'.jar /Y');
|
exec('%comspec% /c copy "'+ROOT+'\\bin\\templates\\cordova\\lib\\start-emulator.bat" "' + PROJECT_PATH + '\\cordova\\lib\\start-emulator.bat" /Y');
|
||||||
// copy in xml
|
exec('%comspec% /c copy "'+ROOT+'\\bin\\templates\\cordova\\cordova.bat" "' + PROJECT_PATH + '\\cordova\\cordova.bat" /Y');
|
||||||
fso.CreateFolder(PROJECT_PATH + '\\res\\xml');
|
exec('%comspec% /c copy "'+ROOT+'\\bin\\templates\\cordova\\clean.bat" "' + PROJECT_PATH + '\\cordova\\clean.bat" /Y');
|
||||||
exec('%comspec% /c copy "'+ROOT+'"\\xml\\config.xml ' + PROJECT_PATH + '\\res\\xml\\config.xml /Y');
|
exec('%comspec% /c copy "'+ROOT+'\\bin\\templates\\cordova\\build.bat" "' + PROJECT_PATH + '\\cordova\\build.bat" /Y');
|
||||||
}
|
exec('%comspec% /c copy "'+ROOT+'\\bin\\templates\\cordova\\log.bat" "' + PROJECT_PATH + '\\cordova\\log.bat" /Y');
|
||||||
|
exec('%comspec% /c copy "'+ROOT+'\\bin\\templates\\cordova\\run.bat" "' + PROJECT_PATH + '\\cordova\\run.bat" /Y');
|
||||||
// copy cordova scripts
|
exec('%comspec% /c copy "'+ROOT+'\\bin\\templates\\cordova\\version.bat" "' + PROJECT_PATH + '\\cordova\\version.bat" /Y');
|
||||||
fso.CreateFolder(PROJECT_PATH + '\\cordova');
|
|
||||||
createAppInfoJar();
|
// interpolate the activity name and package
|
||||||
WScript.Echo("Copying cordova command tools...");
|
Log("Updating AndroidManifest.xml and Main Activity...");
|
||||||
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\cordova\\appinfo.jar ' + PROJECT_PATH + '\\cordova\\appinfo.jar /Y');
|
replaceInFile(ACTIVITY_PATH, /__ACTIVITY__/, ACTIVITY);
|
||||||
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\cordova\\cordova.js ' + PROJECT_PATH + '\\cordova\\cordova.js /Y');
|
replaceInFile(ACTIVITY_PATH, /__ID__/, PACKAGE);
|
||||||
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\cordova\\cordova.bat ' + PROJECT_PATH + '\\cordova\\cordova.bat /Y');
|
|
||||||
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\cordova\\clean.bat ' + PROJECT_PATH + '\\cordova\\clean.bat /Y');
|
replaceInFile(MANIFEST_PATH, /__ACTIVITY__/, ACTIVITY);
|
||||||
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\cordova\\build.bat ' + PROJECT_PATH + '\\cordova\\build.bat /Y');
|
replaceInFile(MANIFEST_PATH, /__PACKAGE__/, PACKAGE);
|
||||||
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\cordova\\log.bat ' + PROJECT_PATH + '\\cordova\\log.bat /Y');
|
replaceInFile(MANIFEST_PATH, /__APILEVEL__/, API_LEVEL);
|
||||||
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\cordova\\run.bat ' + PROJECT_PATH + '\\cordova\\run.bat /Y');
|
|
||||||
|
replaceInFile(STRINGS_PATH, new RegExp('>' + SAFE_ACTIVITY + '<'), '>' + ACTIVITY + '<');
|
||||||
// interpolate the activity name and package
|
|
||||||
WScript.Echo("Updating AndroidManifest.xml and Main Activity...");
|
|
||||||
replaceInFile(ACTIVITY_PATH, /__ACTIVITY__/, ACTIVITY);
|
|
||||||
replaceInFile(ACTIVITY_PATH, /__ID__/, PACKAGE);
|
|
||||||
|
|
||||||
replaceInFile(MANIFEST_PATH, /__ACTIVITY__/, ACTIVITY);
|
|
||||||
replaceInFile(MANIFEST_PATH, /__PACKAGE__/, PACKAGE);
|
|
||||||
replaceInFile(MANIFEST_PATH, /__APILEVEL__/, API_LEVEL);
|
|
||||||
|
|
||||||
cleanup();
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
# or more contributor license agreements. See the NOTICE file
|
# or more contributor license agreements. See the NOTICE file
|
||||||
# distributed with this work for additional information
|
# distributed with this work for additional information
|
||||||
@@ -15,10 +16,24 @@
|
|||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
#!/bin/bash
|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
PROJECT_PATH=$( cd "$DIR/.." && pwd )
|
||||||
|
|
||||||
set -e
|
if [[ "$#" -eq 1 ]] ; then
|
||||||
|
if [[ $1 == "--debug" ]] ; then
|
||||||
CORDOVA_PATH=$( cd "$( dirname "$0" )" && pwd )
|
$DIR/clean
|
||||||
|
ant debug -f "$PROJECT_PATH"/build.xml
|
||||||
bash "$CORDOVA_PATH"/cordova build
|
elif [[ $1 == "--release" ]] ; then
|
||||||
|
$DIR/clean
|
||||||
|
ant release -f "$PROJECT_PATH"/build.xml
|
||||||
|
elif [[ $1 == "--nobuild" ]] ; then
|
||||||
|
echo "Skipping build..."
|
||||||
|
else
|
||||||
|
echo "Error : Build command '$1' not recognized."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Warning : [ --debug | --release | --nobuild ] not specified, defaulting to --debug"
|
||||||
|
$DIR/clean
|
||||||
|
ant debug -f "$PROJECT_PATH"/build.xml
|
||||||
|
fi
|
||||||
|
|||||||
@@ -14,5 +14,5 @@
|
|||||||
:: KIND, either express or implied. See the License for the
|
:: KIND, either express or implied. See the License for the
|
||||||
:: specific language governing permissions and limitations
|
:: specific language governing permissions and limitations
|
||||||
:: under the License.
|
:: under the License.
|
||||||
|
@ECHO OFF
|
||||||
%~dp0\cordova.bat build
|
%~dp0\cordova.bat build %*
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
# or more contributor license agreements. See the NOTICE file
|
# or more contributor license agreements. See the NOTICE file
|
||||||
# distributed with this work for additional information
|
# distributed with this work for additional information
|
||||||
@@ -15,10 +16,7 @@
|
|||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
#!/bin/bash
|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
PROJECT_PATH=$( cd "$DIR/.." && pwd )
|
||||||
set -e
|
echo "Cleaning project..."
|
||||||
|
ant -f "$PROJECT_PATH/build.xml" clean
|
||||||
CORDOVA_PATH=$( cd "$( dirname "$0" )" && pwd )
|
|
||||||
|
|
||||||
bash "$CORDOVA_PATH"/cordova clean
|
|
||||||
|
|||||||
@@ -14,5 +14,5 @@
|
|||||||
:: KIND, either express or implied. See the License for the
|
:: KIND, either express or implied. See the License for the
|
||||||
:: specific language governing permissions and limitations
|
:: specific language governing permissions and limitations
|
||||||
:: under the License.
|
:: under the License.
|
||||||
|
@ECHO OFF
|
||||||
%~dp0\cordova.bat clean
|
%~dp0\cordova.bat clean %*
|
||||||
|
|||||||
@@ -1,159 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
|
|
||||||
PROJECT_PATH=$( cd "$( dirname "$0" )/.." && pwd )
|
|
||||||
|
|
||||||
function check_devices {
|
|
||||||
# FIXME
|
|
||||||
local devices=`adb devices | awk '/List of devices attached/ { while(getline > 0) { print }}' | grep device`
|
|
||||||
if [ -z "$devices" ] ; then
|
|
||||||
echo "1"
|
|
||||||
else
|
|
||||||
echo "0"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function emulate {
|
|
||||||
declare -a avd_list=($(android list avd | grep "Name:" | cut -f 2 -d ":" | xargs))
|
|
||||||
# we need to start adb-server
|
|
||||||
adb start-server 1>/dev/null
|
|
||||||
|
|
||||||
# Do not launch an emulator if there is already one running or if a device is attached
|
|
||||||
if [ $(check_devices) == 0 ] ; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
local avd_id="1000" #FIXME: hopefully user does not have 1000 AVDs
|
|
||||||
# User has no AVDs
|
|
||||||
if [ ${#avd_list[@]} == 0 ]
|
|
||||||
then
|
|
||||||
echo "You don't have any Android Virtual Devices. Please create at least one AVD."
|
|
||||||
echo "android"
|
|
||||||
fi
|
|
||||||
# User has only one AVD
|
|
||||||
if [ ${#avd_list[@]} == 1 ]
|
|
||||||
then
|
|
||||||
emulator -cpu-delay 0 -no-boot-anim -cache /tmp/cache -avd ${avd_list[0]} 1> /dev/null 2>&1 &
|
|
||||||
# User has more than 1 AVD
|
|
||||||
elif [ ${#avd_list[@]} -gt 1 ]
|
|
||||||
then
|
|
||||||
while [ -z ${avd_list[$avd_id]} ]
|
|
||||||
do
|
|
||||||
echo "Choose from one of the following Android Virtual Devices [0 to $((${#avd_list[@]}-1))]:"
|
|
||||||
for(( i = 0 ; i < ${#avd_list[@]} ; i++ ))
|
|
||||||
do
|
|
||||||
echo "$i) ${avd_list[$i]}"
|
|
||||||
done
|
|
||||||
read -t 5 -p "> " avd_id
|
|
||||||
# default value if input timeout
|
|
||||||
if [ $avd_id -eq 1000 ] ; then avd_id=0 ; fi
|
|
||||||
done
|
|
||||||
emulator -cpu-delay 0 -no-boot-anim -cache /tmp/cache -avd ${avd_list[$avd_id]} 1> /dev/null 2>&1 &
|
|
||||||
fi
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function clean {
|
|
||||||
ant clean
|
|
||||||
}
|
|
||||||
# has to be used independently and not in conjunction with other commands
|
|
||||||
function log {
|
|
||||||
adb logcat
|
|
||||||
}
|
|
||||||
|
|
||||||
function run {
|
|
||||||
clean && emulate && wait_for_device && install && launch
|
|
||||||
}
|
|
||||||
|
|
||||||
function install {
|
|
||||||
|
|
||||||
declare -a devices=($(adb devices | awk '/List of devices attached/ { while(getline > 0) { print }}' | grep device | cut -f 1))
|
|
||||||
local device_id="1000" #FIXME: hopefully user does not have 1000 AVDs
|
|
||||||
|
|
||||||
if [ ${#devices[@]} == 0 ]
|
|
||||||
then
|
|
||||||
# should not reach here. Emulator should launch or device should be attached
|
|
||||||
echo "Emulator not running or device not attached. Could not install debug package"
|
|
||||||
exit 70
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ${#devices[@]} == 1 ]
|
|
||||||
then
|
|
||||||
export ANDROID_SERIAL=${devices[0]}
|
|
||||||
# User has more than 1 AVD
|
|
||||||
elif [ ${#devices[@]} -gt 1 ]
|
|
||||||
then
|
|
||||||
while [ -z ${devices[$device_id]} ]
|
|
||||||
do
|
|
||||||
echo "Choose from one of the following devices/emulators [0 to $((${#devices[@]}-1))]:"
|
|
||||||
for(( i = 0 ; i < ${#devices[@]} ; i++ ))
|
|
||||||
do
|
|
||||||
echo "$i) ${devices[$i]}"
|
|
||||||
done
|
|
||||||
read -t 5 -p "> " device_id
|
|
||||||
# default value if input timeout
|
|
||||||
if [ $device_id -eq 1000 ] ; then device_id=0 ; fi
|
|
||||||
done
|
|
||||||
export ANDROID_SERIAL=${devices[$device_id]}
|
|
||||||
fi
|
|
||||||
|
|
||||||
ant debug install
|
|
||||||
}
|
|
||||||
|
|
||||||
function build {
|
|
||||||
ant debug
|
|
||||||
}
|
|
||||||
|
|
||||||
function release {
|
|
||||||
ant release
|
|
||||||
}
|
|
||||||
|
|
||||||
function wait_for_device {
|
|
||||||
local i="0"
|
|
||||||
echo -n "Waiting for device..."
|
|
||||||
|
|
||||||
while [ $i -lt 300 ]
|
|
||||||
do
|
|
||||||
if [ $(check_devices) -eq 0 ]
|
|
||||||
then
|
|
||||||
break
|
|
||||||
else
|
|
||||||
sleep 1
|
|
||||||
i=$[i+1]
|
|
||||||
echo -n "."
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
# Device timeout: emulator has not started in time or device not attached
|
|
||||||
if [ $i -eq 300 ]
|
|
||||||
then
|
|
||||||
echo "device timeout!"
|
|
||||||
exit 69
|
|
||||||
else
|
|
||||||
echo "connected!"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function launch {
|
|
||||||
local launch_str=$(java -jar "$PROJECT_PATH"/cordova/appinfo.jar "$PROJECT_PATH"/AndroidManifest.xml)
|
|
||||||
adb shell am start -n $launch_str
|
|
||||||
}
|
|
||||||
|
|
||||||
# TODO parse arguments
|
|
||||||
(cd "$PROJECT_PATH" && $1)
|
|
||||||
@@ -14,14 +14,13 @@
|
|||||||
:: KIND, either express or implied. See the License for the
|
:: KIND, either express or implied. See the License for the
|
||||||
:: specific language governing permissions and limitations
|
:: specific language governing permissions and limitations
|
||||||
:: under the License.
|
:: under the License.
|
||||||
|
|
||||||
@ECHO OFF
|
@ECHO OFF
|
||||||
IF NOT DEFINED JAVA_HOME GOTO MISSING
|
IF NOT DEFINED JAVA_HOME GOTO MISSING
|
||||||
FOR %%X in (java.exe ant.bat android.bat) do (
|
FOR %%X in (java.exe ant.bat android.bat) do (
|
||||||
SET FOUND=%%~$PATH:X
|
SET FOUND=%%~$PATH:X
|
||||||
IF NOT DEFINED FOUND GOTO MISSING
|
IF NOT DEFINED FOUND GOTO MISSING
|
||||||
)
|
)
|
||||||
cscript %~dp0\cordova.js %*
|
cscript %~dp0\lib\cordova.js %* //nologo
|
||||||
GOTO END
|
GOTO END
|
||||||
:MISSING
|
:MISSING
|
||||||
ECHO Missing one of the following:
|
ECHO Missing one of the following:
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
// 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.
|
|
||||||
|
|
||||||
var ROOT = WScript.ScriptFullName.split('\\cordova\\cordova.js').join(''),
|
|
||||||
shell=WScript.CreateObject("WScript.Shell");
|
|
||||||
|
|
||||||
function exec(command) {
|
|
||||||
var oExec=shell.Exec(command);
|
|
||||||
var output = new String();
|
|
||||||
while(oExec.Status == 0) {
|
|
||||||
if(!oExec.StdOut.AtEndOfStream) {
|
|
||||||
var line = oExec.StdOut.ReadLine();
|
|
||||||
// XXX: Change to verbose mode
|
|
||||||
// WScript.StdOut.WriteLine(line);
|
|
||||||
output += line;
|
|
||||||
}
|
|
||||||
WScript.sleep(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
function device_running() {
|
|
||||||
var local_devices = shell.Exec("%comspec% /c adb devices").StdOut.ReadAll();
|
|
||||||
if(local_devices.match(/\w+\tdevice/)) {
|
|
||||||
WScript.Echo("Yes");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
WScript.Echo("No");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
function emulate() {
|
|
||||||
// don't run emulator if a device is plugged in or if emulator is already running
|
|
||||||
if(device_running()) {
|
|
||||||
//WScript.Echo("Device or Emulator already running!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var oExec = shell.Exec("%comspec% /c android.bat list avd");
|
|
||||||
var avd_list = [];
|
|
||||||
var avd_id = -10;
|
|
||||||
while(!oExec.StdOut.AtEndOfStream) {
|
|
||||||
var output = oExec.StdOut.ReadLine();
|
|
||||||
if(output.match(/Name: (.)*/)) {
|
|
||||||
avd_list.push(output.replace(/ *Name:\s/, ""));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// user has no AVDs
|
|
||||||
if(avd_list.length == 0) {
|
|
||||||
WScript.Echo("You don't have any Android Virtual Devices. Please create at least one AVD.");
|
|
||||||
WScript.Echo("android");
|
|
||||||
WScript.Quit(1);
|
|
||||||
}
|
|
||||||
// user has only one AVD so we launch that one
|
|
||||||
if(avd_list.length == 1) {
|
|
||||||
|
|
||||||
shell.Run("emulator -cpu-delay 0 -no-boot-anim -cache %Temp%\cache -avd "+avd_list[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// user has more than one avd so we ask them to choose
|
|
||||||
if(avd_list.length > 1) {
|
|
||||||
while(!avd_list[avd_id]) {
|
|
||||||
WScript.Echo("Choose from one of the following Android Virtual Devices [0 to "+(avd_list.length - 1)+"]:")
|
|
||||||
for(i = 0, j = avd_list.length ; i < j ; i++) {
|
|
||||||
WScript.Echo((i)+") "+avd_list[i]);
|
|
||||||
}
|
|
||||||
WScript.StdOut.Write("> ");
|
|
||||||
avd_id = new Number(WScript.StdIn.ReadLine());
|
|
||||||
}
|
|
||||||
|
|
||||||
shell.Run("emulator -cpu-delay 0 -no-boot-anim -cache %Temp%\\cache -avd "+avd_list[avd_id], 0, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clean() {
|
|
||||||
WScript.Echo("Cleaning project...");
|
|
||||||
exec("%comspec% /c ant.bat clean -f "+ROOT+"\\build.xml 2>&1");
|
|
||||||
}
|
|
||||||
|
|
||||||
function build() {
|
|
||||||
WScript.Echo("Building project...");
|
|
||||||
exec("%comspec% /c ant.bat debug -f "+ROOT+"\\build.xml 2>&1");
|
|
||||||
}
|
|
||||||
|
|
||||||
function install() {
|
|
||||||
WScript.Echo("Building/Installing project...");
|
|
||||||
exec("%comspec% /c ant.bat debug install -f "+ROOT+"\\build.xml 2>&1");
|
|
||||||
}
|
|
||||||
|
|
||||||
function log() {
|
|
||||||
shell.Run("%comspec% /c adb logcat");
|
|
||||||
}
|
|
||||||
|
|
||||||
function launch() {
|
|
||||||
WScript.Echo("Launching app...");
|
|
||||||
var launch_str=exec("%comspec% /c java -jar "+ROOT+"\\cordova\\appinfo.jar "+ROOT+"\\AndroidManifest.xml");
|
|
||||||
//WScript.Echo(launch_str);
|
|
||||||
exec("%comspec% /c adb shell am start -n "+launch_str+" 2>&1");
|
|
||||||
}
|
|
||||||
|
|
||||||
function run() {
|
|
||||||
var i=0;
|
|
||||||
clean();
|
|
||||||
emulate();
|
|
||||||
WScript.Stdout.Write('Waiting for device...');
|
|
||||||
while(!device_running() && i < 300) {
|
|
||||||
WScript.Stdout.Write('.');
|
|
||||||
WScript.sleep(1000);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
if(i == 300) {
|
|
||||||
WScript.Stderr.WriteLine("device/emulator timeout!");
|
|
||||||
} else {
|
|
||||||
WScript.Stdout.WriteLine("connected!");
|
|
||||||
}
|
|
||||||
install();
|
|
||||||
launch();
|
|
||||||
}
|
|
||||||
var args = WScript.Arguments;
|
|
||||||
if(args.count() != 1) {
|
|
||||||
WScript.StdErr.Write("An error has occured!\n");
|
|
||||||
WScript.Quit(1);
|
|
||||||
}
|
|
||||||
eval(args(0)+"()");
|
|
||||||
@@ -0,0 +1,609 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
var ROOT = WScript.ScriptFullName.split('\\cordova\\lib\\cordova.js').join(''),
|
||||||
|
shell = WScript.CreateObject("WScript.Shell"),
|
||||||
|
fso = WScript.CreateObject('Scripting.FileSystemObject');
|
||||||
|
//device_id for targeting specific device
|
||||||
|
var device_id;
|
||||||
|
//build types
|
||||||
|
var NONE = 0,
|
||||||
|
DEBUG = '--debug',
|
||||||
|
RELEASE = '--release',
|
||||||
|
NO_BUILD = '--nobuild';
|
||||||
|
var build_type = NONE;
|
||||||
|
|
||||||
|
//deploy tpyes
|
||||||
|
var NONE = 0,
|
||||||
|
EMULATOR = 1,
|
||||||
|
DEVICE = 2,
|
||||||
|
TARGET = 3;
|
||||||
|
var deploy_type = NONE;
|
||||||
|
|
||||||
|
|
||||||
|
// log to stdout or stderr
|
||||||
|
function Log(msg, error) {
|
||||||
|
if (error) {
|
||||||
|
WScript.StdErr.WriteLine(msg);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WScript.StdOut.WriteLine(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// executes a commmand in the shell, returning stdout
|
||||||
|
function exec(command) {
|
||||||
|
var oExec=shell.Exec(command);
|
||||||
|
var output = new String();
|
||||||
|
while (oExec.Status == 0) {
|
||||||
|
if (!oExec.StdOut.AtEndOfStream) {
|
||||||
|
var line = oExec.StdOut.ReadLine();
|
||||||
|
output += line;
|
||||||
|
}
|
||||||
|
WScript.sleep(100);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
// executes a command in the shell, returns stdout or stderr if error
|
||||||
|
function exec_out(command) {
|
||||||
|
var oExec=shell.Exec(command);
|
||||||
|
var output = new String();
|
||||||
|
while (oExec.Status == 0) {
|
||||||
|
if (!oExec.StdOut.AtEndOfStream) {
|
||||||
|
var line = oExec.StdOut.ReadLine();
|
||||||
|
// XXX: Change to verbose mode
|
||||||
|
// WScript.StdOut.WriteLine(line);
|
||||||
|
output += line;
|
||||||
|
}
|
||||||
|
WScript.sleep(100);
|
||||||
|
}
|
||||||
|
//Check to make sure our scripts did not encounter an error
|
||||||
|
if (!oExec.StdErr.AtEndOfStream) {
|
||||||
|
var line = oExec.StdErr.ReadAll();
|
||||||
|
return {'error' : true, 'output' : line};
|
||||||
|
}
|
||||||
|
return {'error' : false, 'output' : output};
|
||||||
|
}
|
||||||
|
|
||||||
|
// executes a commmand in the shell and outputs stdout and fails on stderr
|
||||||
|
function exec_verbose(command) {
|
||||||
|
//Log("Command: " + command);
|
||||||
|
var oShell=shell.Exec(command);
|
||||||
|
while (oShell.Status == 0) {
|
||||||
|
//Wait a little bit so we're not super looping
|
||||||
|
WScript.sleep(100);
|
||||||
|
//Print any stdout output from the script
|
||||||
|
if (!oShell.StdOut.AtEndOfStream) {
|
||||||
|
var line = oShell.StdOut.ReadLine();
|
||||||
|
Log(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Check to make sure our scripts did not encounter an error
|
||||||
|
if (!oShell.StdErr.AtEndOfStream) {
|
||||||
|
var line = oShell.StdErr.ReadAll();
|
||||||
|
Log(line, true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function version(path) {
|
||||||
|
var cordovajs_path = path + "\\assets\\www\\cordova.js";
|
||||||
|
if(fso.FileExists(cordovajs_path)) {
|
||||||
|
var f = fso.OpenTextFile(cordovajs_path, 1,2);
|
||||||
|
var cordovajs = f.ReadAll();
|
||||||
|
f.Close();
|
||||||
|
var version_regex = /^.*CORDOVA_JS_BUILD_LABEL.*$/m;
|
||||||
|
var version_line = cordovajs.match(version_regex) + "";
|
||||||
|
var version = version_line.match(/(\d+)\.(\d+)\.(\d+)(rc\d)?/) + "";
|
||||||
|
// TODO : figure out why this isn't matching properly so we can remove this substring workaround.
|
||||||
|
Log(version.substr(0, ((version.length/2) -1)));
|
||||||
|
} else {
|
||||||
|
Log("Error : Could not find cordova js.", true);
|
||||||
|
Log("Expected Location : " + cordovajs_path, true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_devices() {
|
||||||
|
var device_list = []
|
||||||
|
var local_devices = shell.Exec("%comspec% /c adb devices").StdOut.ReadAll();
|
||||||
|
if (local_devices.match(/\w+\tdevice/)) {
|
||||||
|
devices = local_devices.split('\r\n');
|
||||||
|
//format (ID DESCRIPTION)
|
||||||
|
for (i in devices) {
|
||||||
|
if (devices[i].match(/\w+\tdevice/) && !devices[i].match(/emulator/)) {
|
||||||
|
device_list.push(devices[i].replace(/\t/, ' '));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return device_list
|
||||||
|
}
|
||||||
|
|
||||||
|
function list_devices() {
|
||||||
|
var devices = get_devices();
|
||||||
|
if (devices.length > 0) {
|
||||||
|
for (i in devices) {
|
||||||
|
Log(devices[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log('No devices found, if your device is connected and not showing,');
|
||||||
|
Log(' then try and install the drivers for your device.');
|
||||||
|
Log(' http://developer.android.com/tools/extras/oem-usb.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_emulator_images() {
|
||||||
|
var avd_list = [];
|
||||||
|
var local_emulators = shell.Exec("%comspec% /c android list avds").StdOut.ReadAll();
|
||||||
|
if (local_emulators.match(/Name\:/)) {
|
||||||
|
emulators = local_emulators.split('\n');
|
||||||
|
var count = 0;
|
||||||
|
var output = '';
|
||||||
|
for (i in emulators) {
|
||||||
|
// Find the line with the emulator name.
|
||||||
|
if (emulators[i].match(/Name\:/)) {
|
||||||
|
// strip description
|
||||||
|
var emulator_name = emulators[i].replace(/\s*Name\:\s/, '') + ' ';
|
||||||
|
avd_list.push(emulator_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return avd_list;
|
||||||
|
}
|
||||||
|
|
||||||
|
function list_emulator_images() {
|
||||||
|
var images = get_emulator_images();
|
||||||
|
if (images.length > 0) {
|
||||||
|
for(i in images) {
|
||||||
|
Log(images[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log('No emulators found, if you would like to create an emulator follow the instructions');
|
||||||
|
Log(' provided here : http://developer.android.com/tools/devices/index.html');
|
||||||
|
Log(' Or run \'android create avd --name <name> --target <targetID>\' in on the command line.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_started_emulators() {
|
||||||
|
var started_emulators = [];
|
||||||
|
var local_devices = shell.Exec("%comspec% /c adb devices").StdOut.ReadAll();
|
||||||
|
if (local_devices.match(/emulator/)) {
|
||||||
|
devices = local_devices.split('\r\n');
|
||||||
|
//format (ID DESCRIPTION)
|
||||||
|
for (i in devices) {
|
||||||
|
if (devices[i].match(/\w+\tdevice/) && devices[i].match(/emulator/)) {
|
||||||
|
started_emulators.push(devices[i].replace(/\t/, ' '));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return started_emulators
|
||||||
|
}
|
||||||
|
|
||||||
|
function list_started_emulators() {
|
||||||
|
var images = get_started_emulators();
|
||||||
|
if (images.length > 0) {
|
||||||
|
for(i in images) {
|
||||||
|
Log(images[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log('No started emulators found, if you would like to start an emulator call ');
|
||||||
|
Log('\'list-emulator-images\'');
|
||||||
|
Log(' to get the name of an emulator and then start the emulator with');
|
||||||
|
Log('\'start-emulator <Name>\'');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function create_emulator() {
|
||||||
|
//get targets
|
||||||
|
var targets = shell.Exec('android.bat list targets').StdOut.ReadAll().match(/id:\s\d+/g);
|
||||||
|
if(targets) {
|
||||||
|
exec('%comspec% /c android create avd --name cordova_emulator --target ' + targets[targets.length - 1].replace(/id: /, ""));
|
||||||
|
} else {
|
||||||
|
Log("You do not have any android targets setup. Please create at least one target with the `android` command so that an emulator can be created.", true);
|
||||||
|
WScript.Quit(69);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function start_emulator(name) {
|
||||||
|
var emulators = get_emulator_images();
|
||||||
|
var started_emulators = get_started_emulators();
|
||||||
|
var num_started = started_emulators.length;
|
||||||
|
var emulator_name;
|
||||||
|
var started = false;
|
||||||
|
if (name) {
|
||||||
|
for (i in emulators) {
|
||||||
|
if (emulators[i].substr(0,name.length) == name) {
|
||||||
|
Log("Starting emulator : " + name);
|
||||||
|
shell.Exec("%comspec% /c emulator -avd " + name + " &");
|
||||||
|
//shell.Run("%comspec% /c start cmd /c emulator -cpu-delay 0 -no-boot-anim -cache %Temp%\cache -avd " + name);
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (emulators.length > 0 && started_emulators.length == 0) {
|
||||||
|
emulator_name = emulators[0].split(' ', 1)[0];
|
||||||
|
start_emulator(emulator_name);
|
||||||
|
return;
|
||||||
|
} else if (started_emulators.length > 0) {
|
||||||
|
Log("Emulator already started : " + started_emulators[0].split(' ', 1));
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
Log("Error : unable to start emulator, ensure you have emulators availible by checking \'list-emulator-images\'", true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!started) {
|
||||||
|
Log("Error : unable to start emulator, ensure you have emulators availible by checking \'list-emulator-images\'", true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// wait for emulator to get the ID
|
||||||
|
Log('Waiting for emulator...');
|
||||||
|
var boot_anim = null;
|
||||||
|
var emulator_ID = null;
|
||||||
|
var new_started = null;
|
||||||
|
var i = 0;
|
||||||
|
while(emulator_ID == null && i < 10) {
|
||||||
|
new_started = get_started_emulators();
|
||||||
|
if(new_started.length > started_emulators.length) {
|
||||||
|
// find new emulator that was just started to get it's ID
|
||||||
|
for(var i = 0; i < new_started.length; i++) {
|
||||||
|
if (new_started[i] != started_emulators[i]) {
|
||||||
|
emulator_ID = new_started[i].split(' ', 1)[0];
|
||||||
|
boot_anim = exec_out('%comspec% /c adb -s ' + emulator_ID + ' shell getprop init.svc.bootanim');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i == 10) {
|
||||||
|
Log('\nEmulator start timed out.');
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
i = 0;
|
||||||
|
WScript.Stdout.Write('Booting up emulator (this may take a while).');
|
||||||
|
// use boot animation property to tell when boot is complete.
|
||||||
|
while (!boot_anim.output.match(/stopped/) && i < 100) {
|
||||||
|
boot_anim = exec_out('%comspec% /c adb -s ' + emulator_ID + ' shell getprop init.svc.bootanim');
|
||||||
|
i++;
|
||||||
|
WScript.Stdout.Write('.');
|
||||||
|
WScript.Sleep(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < 100) {
|
||||||
|
Log('\nBoot Complete!');
|
||||||
|
// Unlock the device
|
||||||
|
shell.Exec("%comspec% /c adb -s " + emulator_ID + " shell input keyevent 82");
|
||||||
|
} else {
|
||||||
|
Log('\nEmulator boot timed out. Failed to load emulator');
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_apk(path) {
|
||||||
|
// check if file .apk has been created
|
||||||
|
if (fso.FolderExists(path + '\\bin')) {
|
||||||
|
var path_to_apk;
|
||||||
|
var out_folder = fso.GetFolder(path + '\\bin');
|
||||||
|
var out_files = new Enumerator(out_folder.Files);
|
||||||
|
for (;!out_files.atEnd(); out_files.moveNext()) {
|
||||||
|
var path = out_files.item() + '';
|
||||||
|
if (fso.GetExtensionName(path) == 'apk' && !path.match(/unaligned/)) {
|
||||||
|
path_to_apk = out_files.item();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (path_to_apk) {
|
||||||
|
return path_to_apk;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log('Failed to find apk, make sure you project is built and there is an ', true);
|
||||||
|
Log(' apk in <project>\\bin\\. To build your project use \'<project>\\cordova\\build\'', true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function install_device(path) {
|
||||||
|
var devices = get_devices();
|
||||||
|
var use_target = false;
|
||||||
|
if (devices.length < 1) {
|
||||||
|
Log("Error : No devices found to install to, make sure there are devices", true);
|
||||||
|
Log(" availible by checking \'<project_dir>\\cordova\\lib\\list-devices\'", true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
launch(path, devices[0].split(' ', 1)[0], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function install_emulator(path) {
|
||||||
|
var emulators = get_started_emulators();
|
||||||
|
var use_target = false;
|
||||||
|
if (emulators.length < 1) {
|
||||||
|
Log("Error : No emulators found to install to, make sure there are emulators", true);
|
||||||
|
Log(" availible by checking \'<project_dir>\\cordova\\lib\\list-started-emulators\'", true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
launch(path, emulators[0].split(' ', 1)[0], false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function install_target(path) {
|
||||||
|
if(device_id) {
|
||||||
|
var device = false;
|
||||||
|
var emulators = get_started_emulators();
|
||||||
|
var devices = get_devices();
|
||||||
|
var exists = false;
|
||||||
|
for (i in emulators) {
|
||||||
|
if (emulators[i].substr(0,device_id.length) == device_id) {
|
||||||
|
exists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i in devices) {
|
||||||
|
if (devices[i].substr(0,device_id.length) == device_id) {
|
||||||
|
exists = true;
|
||||||
|
device = true
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!exists) {
|
||||||
|
Log("Error : Unable to find target " + device_id, true);
|
||||||
|
Log("Please ensure the target exists by checking \'<project>\\cordova\\lib\\list-started-emulators'");
|
||||||
|
Log(" Or \'<project>\\cordova\\lib\\list-devices'");
|
||||||
|
}
|
||||||
|
launch(path, device_id, device);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log("You cannot install to a target without providing a valid target ID.", true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function launch(path, id, device) {
|
||||||
|
if(id) {
|
||||||
|
var path_to_apk = get_apk(path);
|
||||||
|
if (path_to_apk) {
|
||||||
|
var launch_name = exec_out("%comspec% /c java -jar "+path+"\\cordova\\appinfo.jar "+path+"\\AndroidManifest.xml");
|
||||||
|
if (launch_name.error) {
|
||||||
|
Log("Failed to get application name from appinfo.jar + AndroidManifest : ", true);
|
||||||
|
Log("Output : " + launch_name.output, true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
if (device) {
|
||||||
|
// install on device (-d)
|
||||||
|
Log("Installing app on device...");
|
||||||
|
} else {
|
||||||
|
// install on emulator (-e)
|
||||||
|
Log("Installing app on emulator...");
|
||||||
|
}
|
||||||
|
var cmd = '%comspec% /c adb -s ' + id + ' install -r ' + path_to_apk;
|
||||||
|
var install = exec_out(cmd);
|
||||||
|
if ( install.error && install.output.match(/Failure/)) {
|
||||||
|
Log("Error : Could not install apk to emulator : ", true);
|
||||||
|
Log(install.output, true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log(install.output);
|
||||||
|
}
|
||||||
|
// launch the application
|
||||||
|
Log("Launching application...");
|
||||||
|
cmd = '%comspec% /c adb -s ' + id + ' shell am start -W -a android.intent.action.MAIN -n ' + launch_name.output;
|
||||||
|
exec_verbose(cmd);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log('Failed to find apk, make sure you project is built and there is an ', true);
|
||||||
|
Log(' apk in <project>\\bin\\. To build your project use \'<project>\\cordova\\build\'', true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log("You cannot install to a target without providing a valid target ID.", true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clean(path) {
|
||||||
|
Log("Cleaning project...");
|
||||||
|
exec("%comspec% /c ant.bat clean -f "+path+"\\build.xml 2>&1");
|
||||||
|
}
|
||||||
|
|
||||||
|
function log() {
|
||||||
|
// filter out nativeGetEnabledTags spam from latest sdk bug.
|
||||||
|
shell.Run("%comspec% /c adb logcat | grep -v nativeGetEnabledTags");
|
||||||
|
}
|
||||||
|
|
||||||
|
function build(path) {
|
||||||
|
switch (build_type) {
|
||||||
|
case DEBUG :
|
||||||
|
clean(path);
|
||||||
|
Log("Building project...");
|
||||||
|
exec_verbose("%comspec% /c ant.bat debug -f "+path+"\\build.xml 2>&1");
|
||||||
|
break;
|
||||||
|
case RELEASE :
|
||||||
|
clean(path);
|
||||||
|
Log("Building project...");
|
||||||
|
exec_verbose("%comspec% /c ant.bat release -f "+path+"\\build.xml 2>&1");
|
||||||
|
break;
|
||||||
|
case NO_BUILD :
|
||||||
|
Log("Skipping build process.");
|
||||||
|
break;
|
||||||
|
case NONE :
|
||||||
|
clean(path);
|
||||||
|
Log("WARNING: [ --debug | --release | --nobuild ] not specified, defaulting to --debug.");
|
||||||
|
exec_verbose("%comspec% /c ant.bat debug -f "+path+"\\build.xml 2>&1");
|
||||||
|
break;
|
||||||
|
default :
|
||||||
|
Log("Build option not recognized: " + build_type, true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(path) {
|
||||||
|
switch(deploy_type) {
|
||||||
|
case EMULATOR :
|
||||||
|
build(path);
|
||||||
|
if(get_started_emulators().length == 0) {
|
||||||
|
start_emulator();
|
||||||
|
}
|
||||||
|
//TODO : Start emulator if one isn't started, and create one if none exists.
|
||||||
|
install_emulator(path);
|
||||||
|
break;
|
||||||
|
case DEVICE :
|
||||||
|
build(path);
|
||||||
|
install_device(path);
|
||||||
|
break;
|
||||||
|
case TARGET :
|
||||||
|
build(path);
|
||||||
|
install_target(path);
|
||||||
|
break;
|
||||||
|
case NONE :
|
||||||
|
if (get_devices().length > 0) {
|
||||||
|
Log("WARNING: [ --target=<ID> | --emulator | --device ] not specified, defaulting to --device");
|
||||||
|
deploy_type = DEVICE;
|
||||||
|
} else {
|
||||||
|
Log("WARNING: [ --target=<ID> | --emulator | --device ] not specified, defaulting to --emulator");
|
||||||
|
deploy_type = EMULATOR;
|
||||||
|
}
|
||||||
|
run(path);
|
||||||
|
break;
|
||||||
|
default :
|
||||||
|
Log("Deploy option not recognized: " + deploy_type, true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var args = WScript.Arguments;
|
||||||
|
if (args.count() == 0) {
|
||||||
|
Log("Error: no args provided.");
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// parse command
|
||||||
|
switch(args(0)) {
|
||||||
|
case "version" :
|
||||||
|
version(ROOT);
|
||||||
|
break;
|
||||||
|
case "build" :
|
||||||
|
if(args.Count() > 1) {
|
||||||
|
if (args(1) == "--release") {
|
||||||
|
build_type = RELEASE;
|
||||||
|
}
|
||||||
|
else if (args(1) == "--debug") {
|
||||||
|
build_type = DEBUG;
|
||||||
|
}
|
||||||
|
else if (args(1) == "--nobuild") {
|
||||||
|
build_type = NO_BUILD;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log('Error: \"' + args(i) + '\" is not recognized as a build option', true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
build(ROOT);
|
||||||
|
break;
|
||||||
|
case "clean" :
|
||||||
|
clean();
|
||||||
|
break;
|
||||||
|
case "list-devices" :
|
||||||
|
list_devices();
|
||||||
|
break;
|
||||||
|
case "list-emulator-images" :
|
||||||
|
list_emulator_images();
|
||||||
|
break;
|
||||||
|
case "list-started-emulators" :
|
||||||
|
list_started_emulators();
|
||||||
|
break;
|
||||||
|
case "start-emulator" :
|
||||||
|
if (args.Count() > 1) {
|
||||||
|
start_emulator(args(1))
|
||||||
|
} else {
|
||||||
|
start_emulator();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "install-emulator" :
|
||||||
|
if (args.Count() == 2) {
|
||||||
|
if (args(1).substr(0,9) == "--target=") {
|
||||||
|
device_id = args(1).split('--target=').join('');
|
||||||
|
install_emulator(ROOT);
|
||||||
|
} else {
|
||||||
|
Log('Error: \"' + args(1) + '\" is not recognized as an install option', true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
install_emulator(ROOT);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "install-device" :
|
||||||
|
if (args.Count() == 2) {
|
||||||
|
if (args(1).substr(0,9) == "--target=") {
|
||||||
|
device_id = args(1).split('--target=').join('');
|
||||||
|
install_target(ROOT);
|
||||||
|
} else {
|
||||||
|
Log('Error: \"' + args(1) + '\" is not recognized as an install option', true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
install_device(ROOT);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "run" :
|
||||||
|
//parse args
|
||||||
|
for(var i = 1; i < args.Count(); i++) {
|
||||||
|
if (args(i) == "--release") {
|
||||||
|
build_type = RELEASE;
|
||||||
|
}
|
||||||
|
else if (args(i) == "--debug") {
|
||||||
|
build_type = DEBUG;
|
||||||
|
}
|
||||||
|
else if (args(i) == "--nobuild") {
|
||||||
|
build_type = NO_BUILD;
|
||||||
|
}
|
||||||
|
else if (args(i) == "--emulator" || args(i) == "-e") {
|
||||||
|
deploy_type = EMULATOR;
|
||||||
|
}
|
||||||
|
else if (args(i) == "--device" || args(i) == "-d") {
|
||||||
|
deploy_type = DEVICE;
|
||||||
|
}
|
||||||
|
else if (args(i).substr(0,9) == "--target=") {
|
||||||
|
device_id = args(i).split("--target=").join("");
|
||||||
|
deploy_type = TARGET;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log('Error: \"' + args(i) + '\" is not recognized as a run option', true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run(ROOT);
|
||||||
|
break;
|
||||||
|
default :
|
||||||
|
Log("Cordova does not regognize the command " + args(0), true);
|
||||||
|
WScript.Quit(2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
PROJECT_PATH=$( cd "$DIR/../.." && pwd )
|
||||||
|
device_list=$("$DIR/list-devices")
|
||||||
|
if [ $? != 0 ]; then
|
||||||
|
echo "No devices found to deploy to. Please make sure your device is connected"
|
||||||
|
echo " and you can view it using the 'cordova/lib/list-devices' command."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
apks=`find $PROJECT_PATH/bin -type f -maxdepth 1 | egrep '\.apk$'`
|
||||||
|
apk_list=($apks)
|
||||||
|
if [[ ${#apk_list[@]} > 0 ]] ; then
|
||||||
|
# handle target
|
||||||
|
read -ra device_array <<< "$device_list"
|
||||||
|
if [[ "$#" -eq 1 ]] ; then
|
||||||
|
# deploy to given target
|
||||||
|
target=${1/--target=/}
|
||||||
|
else
|
||||||
|
# delete trailing space and 'device' after device ID
|
||||||
|
target=${device_array[0]}
|
||||||
|
fi
|
||||||
|
echo "Installing ${apk_list[0]} onto device $target..."
|
||||||
|
adb -s $target install -r ${apk_list[0]};
|
||||||
|
echo "Launching application..."
|
||||||
|
launch_str=$(java -jar "$PROJECT_PATH"/cordova/appinfo.jar "$PROJECT_PATH"/AndroidManifest.xml)
|
||||||
|
adb -s $target shell am start -W -a android.intent.action.MAIN -n $launch_str
|
||||||
|
else
|
||||||
|
echo "Application package not found, could not install to device"
|
||||||
|
echo " make sure your application is built before deploying."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
:: 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.
|
||||||
|
@ECHO OFF
|
||||||
|
SET full_path=%~dp0
|
||||||
|
IF EXIST %full_path%cordova.js (
|
||||||
|
cscript "%full_path%cordova.js" install-device %* //nologo
|
||||||
|
) ELSE (
|
||||||
|
ECHO.
|
||||||
|
ECHO ERROR: Could not find 'cordova.js' in cordova/lib, aborting...>&2
|
||||||
|
EXIT /B 1
|
||||||
|
)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
PROJECT_PATH=$( cd "$DIR/../.." && pwd )
|
||||||
|
emulator_list=$("$DIR/list-started-emulators")
|
||||||
|
if [ $? != 0 ]; then
|
||||||
|
echo "No emulators found to deploy to. Please make sure your emulator is started"
|
||||||
|
echo " You can view it using the 'cordova/lib/list-started-emulators' command."
|
||||||
|
echo " You can view created emulator images using the 'cordova/lib/list-emulator-images' command."
|
||||||
|
echo " You can start an emulator image using the 'cordova/lib/start-emulator' command."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
apks=`find $PROJECT_PATH/bin -type f -maxdepth 1 | egrep '\.apk$'`
|
||||||
|
apk_list=($apks)
|
||||||
|
if [[ ${#apk_list[@]} > 0 ]] ; then
|
||||||
|
# handle target emulator
|
||||||
|
if [[ "$#" -eq 1 ]] ; then
|
||||||
|
# deploy to given target
|
||||||
|
target=${1/--target=/}
|
||||||
|
else
|
||||||
|
# delete trailing space and 'device' after emulator ID
|
||||||
|
target=${emulator_list[0]}
|
||||||
|
fi
|
||||||
|
echo "Installing ${apk_list[0]} onto emulator $target..."
|
||||||
|
adb -s $target install -r ${apk_list[0]};
|
||||||
|
echo "Launching application..."
|
||||||
|
launch_str=$(java -jar "$PROJECT_PATH"/cordova/appinfo.jar "$PROJECT_PATH"/AndroidManifest.xml)
|
||||||
|
adb -s $target shell am start -W -a android.intent.action.MAIN -n $launch_str
|
||||||
|
else
|
||||||
|
echo "Application package not found, could not install to device"
|
||||||
|
echo " make sure your application is built before deploying."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
:: 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.
|
||||||
|
@ECHO OFF
|
||||||
|
SET full_path=%~dp0
|
||||||
|
IF EXIST %full_path%cordova.js (
|
||||||
|
cscript "%full_path%cordova.js" install-emulator %* //nologo
|
||||||
|
) ELSE (
|
||||||
|
ECHO.
|
||||||
|
ECHO ERROR: Could not find 'cordova.js' in cordova/lib, aborting...>&2
|
||||||
|
EXIT /B 1
|
||||||
|
)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
devices=`adb devices | awk '/List of devices attached/ { while(getline > 0) { print }}' | grep 'device' | grep -v 'emulator' | awk '{ print $1; }'`
|
||||||
|
device_list=($devices)
|
||||||
|
if [[ ${#device_list[@]} > 0 ]] ; then
|
||||||
|
for i in ${devices[@]}
|
||||||
|
do
|
||||||
|
echo $i
|
||||||
|
done
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "No devices found."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
:: 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.
|
||||||
|
@ECHO OFF
|
||||||
|
SET full_path=%~dp0
|
||||||
|
IF EXIST %full_path%cordova.js (
|
||||||
|
cscript "%full_path%cordova.js" list-devices //nologo
|
||||||
|
) ELSE (
|
||||||
|
ECHO.
|
||||||
|
ECHO ERROR: Could not find 'cordova.js' in cordova/lib, aborting...>&2
|
||||||
|
EXIT /B 1
|
||||||
|
)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
emulator_images=`android list avds | grep "Name:" | cut -f 2 -d ":"`
|
||||||
|
emulator_list=($emulator_images)
|
||||||
|
if [[ ${#emulator_list[@]} > 0 ]] ; then
|
||||||
|
for i in ${emulator_list[@]}
|
||||||
|
do
|
||||||
|
echo $i
|
||||||
|
done
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "No emulators found, if you would like to create an emulator follow the instructions"
|
||||||
|
echo " provided here : http://developer.android.com/tools/devices/index.html"
|
||||||
|
echo " Or run 'android create avd --name <name> --target <targetID>' in on the command line."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
:: 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.
|
||||||
|
@ECHO OFF
|
||||||
|
SET full_path=%~dp0
|
||||||
|
IF EXIST %full_path%cordova.js (
|
||||||
|
cscript "%full_path%cordova.js" list-emulator-images //nologo
|
||||||
|
) ELSE (
|
||||||
|
ECHO.
|
||||||
|
ECHO ERROR: Could not find 'cordova.js' in cordova/lib, aborting...>&2
|
||||||
|
EXIT /B 1
|
||||||
|
)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
devices=`adb devices | awk '/List of devices attached/ { while(getline > 0) { print $1;}}' | grep 'emulator' | grep -v 'offline'`
|
||||||
|
read -ra emulator_list <<< "$devices"
|
||||||
|
if [[ ${#emulator_list[@]} > 0 ]] ; then
|
||||||
|
for i in ${emulator_list[@]}
|
||||||
|
do
|
||||||
|
# remove space and 'device'
|
||||||
|
echo $i
|
||||||
|
done
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "No started emulators found (it may still be booting up), you can start an emulator by using the command"
|
||||||
|
echo " 'cordova/lib/start-emulator'"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
:: 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.
|
||||||
|
@ECHO OFF
|
||||||
|
SET full_path=%~dp0
|
||||||
|
IF EXIST %full_path%cordova.js (
|
||||||
|
cscript "%full_path%cordova.js" list-started-emulators //nologo
|
||||||
|
) ELSE (
|
||||||
|
ECHO.
|
||||||
|
ECHO ERROR: Could not find 'cordova.js' in cordova/lib, aborting...>&2
|
||||||
|
EXIT /B 1
|
||||||
|
)
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
PROJECT_PATH=$( cd "$( dirname "$0" )/../.." && pwd )
|
||||||
|
|
||||||
|
function dot {
|
||||||
|
sleep 1
|
||||||
|
echo -n "."
|
||||||
|
}
|
||||||
|
|
||||||
|
function wait_for_emulator() {
|
||||||
|
local emulator_log_path="$1"
|
||||||
|
local error_string
|
||||||
|
local status
|
||||||
|
|
||||||
|
# Try to detect fatal errors early
|
||||||
|
sleep 1.5
|
||||||
|
error_string=$(grep -F "ERROR: " ${emulator_log_path})
|
||||||
|
status=$?
|
||||||
|
|
||||||
|
if [ $status -eq 0 ]; then
|
||||||
|
echo "Emulator failed to start, fatal error detected"
|
||||||
|
echo "Error: ${error_string}"
|
||||||
|
echo "Full log available at: ${emulator_log_path}"
|
||||||
|
echo "Exiting..."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local i="0"
|
||||||
|
echo -n "Waiting for emulator"
|
||||||
|
emulator_string=$($DIR/list-started-emulators)
|
||||||
|
while [ $? != 0 ]
|
||||||
|
do
|
||||||
|
dot
|
||||||
|
i=$[i+1]
|
||||||
|
emulator_string=$($DIR/list-started-emulators)
|
||||||
|
done
|
||||||
|
read -ra target <<< "$emulator_string"
|
||||||
|
echo ""
|
||||||
|
echo -n "Waiting for it to boot up (this can take a while)"
|
||||||
|
while [ $i -lt 300 ]
|
||||||
|
do
|
||||||
|
boot_anim=$(adb -s $target shell getprop init.svc.bootanim 2>&1)
|
||||||
|
if [[ "$boot_anim" =~ "stopped" ]] ; then
|
||||||
|
break
|
||||||
|
else
|
||||||
|
i=$[i+1]
|
||||||
|
dot
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
# Device timeout: emulator has not started in time
|
||||||
|
if [ $i -eq 300 ]
|
||||||
|
then
|
||||||
|
echo ""
|
||||||
|
echo "Emulator timeout!"
|
||||||
|
exit 69
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "Connected!"
|
||||||
|
fi
|
||||||
|
# Unlock the device
|
||||||
|
adb -s $target shell input keyevent 82
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
emulator_images=$("$DIR/list-emulator-images")
|
||||||
|
if [ $? != 0 ]; then
|
||||||
|
echo "No emulators found, if you would like to create an emulator follow the instructions"
|
||||||
|
echo " provided here : http://developer.android.com/tools/devices/index.html"
|
||||||
|
echo " Or run 'android create avd --name <name> --target <targetID>' in on the command line."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# start first emulator
|
||||||
|
log_path=$(mktemp -t android_emulator)
|
||||||
|
|
||||||
|
# if target emulator is provided
|
||||||
|
if [[ "$#" -eq 1 ]] ; then
|
||||||
|
# check that it exists
|
||||||
|
if [[ $emulator_images =~ $1 ]] ; then
|
||||||
|
#xterm -e emulator -avd $1 &
|
||||||
|
emulator -avd $1 1> "${log_path}" 2>&1 &
|
||||||
|
else
|
||||||
|
echo "Could not find the provided emulator '$1', make sure the emulator exists"
|
||||||
|
echo " by checking 'cordova/lib/list-emulator-images'"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
read -ra emulator_list <<< "$emulator_images"
|
||||||
|
#xterm -e emulator -avd ${emulator_list[0]} &
|
||||||
|
emulator -avd ${emulator_list[0]} 1> "${log_path}" 2>&1 &
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Saving emulator log to: ${log_path}"
|
||||||
|
wait_for_emulator "$log_path"
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
:: 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.
|
||||||
|
@ECHO OFF
|
||||||
|
SET full_path=%~dp0
|
||||||
|
IF EXIST %full_path%cordova.js (
|
||||||
|
cscript "%full_path%cordova.js" start-emulator %* //nologo
|
||||||
|
) ELSE (
|
||||||
|
ECHO.
|
||||||
|
ECHO ERROR: Could not find 'cordova.js' in cordova/lib, aborting...>&2
|
||||||
|
EXIT /B 1
|
||||||
|
)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
# or more contributor license agreements. See the NOTICE file
|
# or more contributor license agreements. See the NOTICE file
|
||||||
# distributed with this work for additional information
|
# distributed with this work for additional information
|
||||||
@@ -15,10 +16,5 @@
|
|||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
#!/bin/bash
|
# filter out nativeGetEnabledTags spam from latest sdk bug.
|
||||||
|
adb logcat | grep -v nativeGetEnabledTags
|
||||||
set -e
|
|
||||||
|
|
||||||
CORDOVA_PATH=$( cd "$( dirname "$0" )/.." && pwd )
|
|
||||||
|
|
||||||
bash "$CORDOVA_PATH"/cordova/cordova log
|
|
||||||
|
|||||||
@@ -14,5 +14,5 @@
|
|||||||
:: KIND, either express or implied. See the License for the
|
:: KIND, either express or implied. See the License for the
|
||||||
:: specific language governing permissions and limitations
|
:: specific language governing permissions and limitations
|
||||||
:: under the License.
|
:: under the License.
|
||||||
|
@ECHO OFF
|
||||||
%~dp0\cordova.bat log
|
%~dp0\cordova.bat log %*
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
# or more contributor license agreements. See the NOTICE file
|
# or more contributor license agreements. See the NOTICE file
|
||||||
# distributed with this work for additional information
|
# distributed with this work for additional information
|
||||||
@@ -15,10 +16,66 @@
|
|||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
#!/bin/bash
|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
PROJECT_PATH=$( cd "$DIR/.." && pwd )
|
||||||
|
|
||||||
set -e
|
function run_on_device_or_emulator {
|
||||||
|
devices=`$DIR/lib/list-devices`
|
||||||
|
if [ $? = 0 ]; then
|
||||||
|
$DIR/lib/install-device
|
||||||
|
else
|
||||||
|
run_on_emulator
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
CORDOVA_PATH=$( cd "$( dirname "$0" )" && pwd )
|
function run_on_emulator {
|
||||||
|
emulators=`$DIR/lib/list-started-emulators`
|
||||||
|
if [ $? = 0 ] ; then
|
||||||
|
$DIR/lib/install-emulator
|
||||||
|
else
|
||||||
|
images=`$DIR/lib/list-emulator-images`
|
||||||
|
if [ $? = 0 ] ; then
|
||||||
|
$DIR/lib/start-emulator
|
||||||
|
$DIR/lib/install-emulator
|
||||||
|
else
|
||||||
|
echo "No devices/emulators started nor images available to start. How are we supposed to do this, then?"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
bash "$CORDOVA_PATH"/cordova run
|
|
||||||
|
|
||||||
|
if [[ "$#" -eq 2 ]] ; then
|
||||||
|
# TODO: the order of arguments here may be reversed from the assumption below
|
||||||
|
$DIR/build $2
|
||||||
|
if [[ $1 == "--device" ]] ; then
|
||||||
|
$DIR/lib/install-device
|
||||||
|
elif [[ $1 == "--emulator" ]] ; then
|
||||||
|
run_on_emulator
|
||||||
|
elif [[ $1 =~ "--target=" ]]; then
|
||||||
|
$DIR/lib/install-device $1
|
||||||
|
else
|
||||||
|
echo "Error : '$1' is not recognized as an install option"
|
||||||
|
fi
|
||||||
|
elif [[ "$#" -eq 1 ]] ; then
|
||||||
|
if [[ $1 == "--debug" || $1 == "--release" || $1 == "--nobuild" ]] ; then
|
||||||
|
$DIR/build $1
|
||||||
|
run_on_device_or_emulator
|
||||||
|
elif [[ $1 == "--device" ]] ; then
|
||||||
|
$DIR/build
|
||||||
|
$DIR/lib/install-device
|
||||||
|
elif [[ $1 == "--emulator" ]] ; then
|
||||||
|
$DIR/build
|
||||||
|
run_on_emulator
|
||||||
|
elif [[ $1 =~ "--target=" ]]; then
|
||||||
|
$DIR/build
|
||||||
|
$DIR/lib/install-device $1
|
||||||
|
else
|
||||||
|
echo "Error : '$1' is not recognized as an install option"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Warning : [ --device | --emulate | --target=<targetID> ] not specified, using defaults."
|
||||||
|
$DIR/build
|
||||||
|
run_on_device_or_emulator
|
||||||
|
fi
|
||||||
|
|||||||
@@ -1 +1,18 @@
|
|||||||
%~dp0\cordova.bat run
|
:: 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.
|
||||||
|
@ECHO OFF
|
||||||
|
%~dp0\cordova.bat run %*
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
# or more contributor license agreements. See the NOTICE file
|
# or more contributor license agreements. See the NOTICE file
|
||||||
# distributed with this work for additional information
|
# distributed with this work for additional information
|
||||||
@@ -15,10 +16,4 @@
|
|||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
#!/bin/bash
|
echo "2.9.0"
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
CORDOVA_PATH=$( cd "$( dirname "$0" )" && pwd )
|
|
||||||
|
|
||||||
bash "$CORDOVA_PATH"/cordova release
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
:: 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.
|
||||||
|
@ECHO OFF
|
||||||
|
%~dp0\cordova.bat version %*
|
||||||
@@ -59,5 +59,5 @@
|
|||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="7" android:targetSdkVersion="__APILEVEL__"/>
|
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="__APILEVEL__"/>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
-->
|
-->
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="format-detection" content="telephone=no" />
|
<meta name="format-detection" content="telephone=no" />
|
||||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi" />
|
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi" />
|
||||||
<link rel="stylesheet" type="text/css" href="css/index.css" />
|
<link rel="stylesheet" type="text/css" href="css/index.css" />
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<p class="event received">Device is Ready</p>
|
<p class="event received">Device is Ready</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript" src="cordova-2.5.0rc1.js"></script>
|
<script type="text/javascript" src="cordova.js"></script>
|
||||||
<script type="text/javascript" src="js/index.js"></script>
|
<script type="text/javascript" src="js/index.js"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
app.initialize();
|
app.initialize();
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 217 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 478 KiB |
|
Before Width: | Height: | Size: 493 KiB |
@@ -1,68 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<!--
|
|
||||||
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.
|
|
||||||
-->
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Jasmine Spec Runner</title>
|
|
||||||
|
|
||||||
<!-- jasmine source -->
|
|
||||||
<link rel="shortcut icon" type="image/png" href="spec/lib/jasmine-1.2.0/jasmine_favicon.png">
|
|
||||||
<link rel="stylesheet" type="text/css" href="spec/lib/jasmine-1.2.0/jasmine.css">
|
|
||||||
<script type="text/javascript" src="spec/lib/jasmine-1.2.0/jasmine.js"></script>
|
|
||||||
<script type="text/javascript" src="spec/lib/jasmine-1.2.0/jasmine-html.js"></script>
|
|
||||||
|
|
||||||
<!-- include source files here... -->
|
|
||||||
<script type="text/javascript" src="js/index.js"></script>
|
|
||||||
|
|
||||||
<!-- include spec files here... -->
|
|
||||||
<script type="text/javascript" src="spec/helper.js"></script>
|
|
||||||
<script type="text/javascript" src="spec/index.js"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
(function() {
|
|
||||||
var jasmineEnv = jasmine.getEnv();
|
|
||||||
jasmineEnv.updateInterval = 1000;
|
|
||||||
|
|
||||||
var htmlReporter = new jasmine.HtmlReporter();
|
|
||||||
|
|
||||||
jasmineEnv.addReporter(htmlReporter);
|
|
||||||
|
|
||||||
jasmineEnv.specFilter = function(spec) {
|
|
||||||
return htmlReporter.specFilter(spec);
|
|
||||||
};
|
|
||||||
|
|
||||||
var currentWindowOnload = window.onload;
|
|
||||||
|
|
||||||
window.onload = function() {
|
|
||||||
if (currentWindowOnload) {
|
|
||||||
currentWindowOnload();
|
|
||||||
}
|
|
||||||
execJasmine();
|
|
||||||
};
|
|
||||||
|
|
||||||
function execJasmine() {
|
|
||||||
jasmineEnv.execute();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="stage" style="display:none;"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
afterEach(function() {
|
|
||||||
document.getElementById('stage').innerHTML = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
var helper = {
|
|
||||||
trigger: function(obj, name) {
|
|
||||||
var e = document.createEvent('Event');
|
|
||||||
e.initEvent(name, true, true);
|
|
||||||
obj.dispatchEvent(e);
|
|
||||||
},
|
|
||||||
getComputedStyle: function(querySelector, property) {
|
|
||||||
var element = document.querySelector(querySelector);
|
|
||||||
return window.getComputedStyle(element).getPropertyValue(property);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
describe('app', function() {
|
|
||||||
describe('initialize', function() {
|
|
||||||
it('should bind deviceready', function() {
|
|
||||||
runs(function() {
|
|
||||||
spyOn(app, 'onDeviceReady');
|
|
||||||
app.initialize();
|
|
||||||
helper.trigger(window.document, 'deviceready');
|
|
||||||
});
|
|
||||||
|
|
||||||
waitsFor(function() {
|
|
||||||
return (app.onDeviceReady.calls.length > 0);
|
|
||||||
}, 'onDeviceReady should be called once', 500);
|
|
||||||
|
|
||||||
runs(function() {
|
|
||||||
expect(app.onDeviceReady).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('onDeviceReady', function() {
|
|
||||||
it('should report that it fired', function() {
|
|
||||||
spyOn(app, 'receivedEvent');
|
|
||||||
app.onDeviceReady();
|
|
||||||
expect(app.receivedEvent).toHaveBeenCalledWith('deviceready');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('receivedEvent', function() {
|
|
||||||
beforeEach(function() {
|
|
||||||
var el = document.getElementById('stage');
|
|
||||||
el.innerHTML = ['<div id="deviceready">',
|
|
||||||
' <p class="event listening">Listening</p>',
|
|
||||||
' <p class="event received">Received</p>',
|
|
||||||
'</div>'].join('\n');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should hide the listening element', function() {
|
|
||||||
app.receivedEvent('deviceready');
|
|
||||||
var displayStyle = helper.getComputedStyle('#deviceready .listening', 'display');
|
|
||||||
expect(displayStyle).toEqual('none');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show the received element', function() {
|
|
||||||
app.receivedEvent('deviceready');
|
|
||||||
var displayStyle = helper.getComputedStyle('#deviceready .received', 'display');
|
|
||||||
expect(displayStyle).toEqual('block');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
Copyright (c) 2008-2011 Pivotal Labs
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
a copy of this software and associated documentation files (the
|
|
||||||
"Software"), to deal in the Software without restriction, including
|
|
||||||
without limitation the rights to use, copy, modify, merge, publish,
|
|
||||||
distribute, sublicense, and/or sell copies of the Software, and to
|
|
||||||
permit persons to whom the Software is furnished to do so, subject to
|
|
||||||
the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be
|
|
||||||
included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
||||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
||||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
||||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
@@ -1,616 +0,0 @@
|
|||||||
jasmine.HtmlReporterHelpers = {};
|
|
||||||
|
|
||||||
jasmine.HtmlReporterHelpers.createDom = function(type, attrs, childrenVarArgs) {
|
|
||||||
var el = document.createElement(type);
|
|
||||||
|
|
||||||
for (var i = 2; i < arguments.length; i++) {
|
|
||||||
var child = arguments[i];
|
|
||||||
|
|
||||||
if (typeof child === 'string') {
|
|
||||||
el.appendChild(document.createTextNode(child));
|
|
||||||
} else {
|
|
||||||
if (child) {
|
|
||||||
el.appendChild(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var attr in attrs) {
|
|
||||||
if (attr == "className") {
|
|
||||||
el[attr] = attrs[attr];
|
|
||||||
} else {
|
|
||||||
el.setAttribute(attr, attrs[attr]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return el;
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.HtmlReporterHelpers.getSpecStatus = function(child) {
|
|
||||||
var results = child.results();
|
|
||||||
var status = results.passed() ? 'passed' : 'failed';
|
|
||||||
if (results.skipped) {
|
|
||||||
status = 'skipped';
|
|
||||||
}
|
|
||||||
|
|
||||||
return status;
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.HtmlReporterHelpers.appendToSummary = function(child, childElement) {
|
|
||||||
var parentDiv = this.dom.summary;
|
|
||||||
var parentSuite = (typeof child.parentSuite == 'undefined') ? 'suite' : 'parentSuite';
|
|
||||||
var parent = child[parentSuite];
|
|
||||||
|
|
||||||
if (parent) {
|
|
||||||
if (typeof this.views.suites[parent.id] == 'undefined') {
|
|
||||||
this.views.suites[parent.id] = new jasmine.HtmlReporter.SuiteView(parent, this.dom, this.views);
|
|
||||||
}
|
|
||||||
parentDiv = this.views.suites[parent.id].element;
|
|
||||||
}
|
|
||||||
|
|
||||||
parentDiv.appendChild(childElement);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
jasmine.HtmlReporterHelpers.addHelpers = function(ctor) {
|
|
||||||
for(var fn in jasmine.HtmlReporterHelpers) {
|
|
||||||
ctor.prototype[fn] = jasmine.HtmlReporterHelpers[fn];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.HtmlReporter = function(_doc) {
|
|
||||||
var self = this;
|
|
||||||
var doc = _doc || window.document;
|
|
||||||
|
|
||||||
var reporterView;
|
|
||||||
|
|
||||||
var dom = {};
|
|
||||||
|
|
||||||
// Jasmine Reporter Public Interface
|
|
||||||
self.logRunningSpecs = false;
|
|
||||||
|
|
||||||
self.reportRunnerStarting = function(runner) {
|
|
||||||
var specs = runner.specs() || [];
|
|
||||||
|
|
||||||
if (specs.length == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
createReporterDom(runner.env.versionString());
|
|
||||||
doc.body.appendChild(dom.reporter);
|
|
||||||
|
|
||||||
reporterView = new jasmine.HtmlReporter.ReporterView(dom);
|
|
||||||
reporterView.addSpecs(specs, self.specFilter);
|
|
||||||
};
|
|
||||||
|
|
||||||
self.reportRunnerResults = function(runner) {
|
|
||||||
reporterView && reporterView.complete();
|
|
||||||
};
|
|
||||||
|
|
||||||
self.reportSuiteResults = function(suite) {
|
|
||||||
reporterView.suiteComplete(suite);
|
|
||||||
};
|
|
||||||
|
|
||||||
self.reportSpecStarting = function(spec) {
|
|
||||||
if (self.logRunningSpecs) {
|
|
||||||
self.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.reportSpecResults = function(spec) {
|
|
||||||
reporterView.specComplete(spec);
|
|
||||||
};
|
|
||||||
|
|
||||||
self.log = function() {
|
|
||||||
var console = jasmine.getGlobal().console;
|
|
||||||
if (console && console.log) {
|
|
||||||
if (console.log.apply) {
|
|
||||||
console.log.apply(console, arguments);
|
|
||||||
} else {
|
|
||||||
console.log(arguments); // ie fix: console.log.apply doesn't exist on ie
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.specFilter = function(spec) {
|
|
||||||
if (!focusedSpecName()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return spec.getFullName().indexOf(focusedSpecName()) === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
return self;
|
|
||||||
|
|
||||||
function focusedSpecName() {
|
|
||||||
var specName;
|
|
||||||
|
|
||||||
(function memoizeFocusedSpec() {
|
|
||||||
if (specName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var paramMap = [];
|
|
||||||
var params = doc.location.search.substring(1).split('&');
|
|
||||||
|
|
||||||
for (var i = 0; i < params.length; i++) {
|
|
||||||
var p = params[i].split('=');
|
|
||||||
paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
specName = paramMap.spec;
|
|
||||||
})();
|
|
||||||
|
|
||||||
return specName;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createReporterDom(version) {
|
|
||||||
dom.reporter = self.createDom('div', { id: 'HTMLReporter', className: 'jasmine_reporter' },
|
|
||||||
dom.banner = self.createDom('div', { className: 'banner' },
|
|
||||||
self.createDom('span', { className: 'title' }, "Jasmine "),
|
|
||||||
self.createDom('span', { className: 'version' }, version)),
|
|
||||||
|
|
||||||
dom.symbolSummary = self.createDom('ul', {className: 'symbolSummary'}),
|
|
||||||
dom.alert = self.createDom('div', {className: 'alert'}),
|
|
||||||
dom.results = self.createDom('div', {className: 'results'},
|
|
||||||
dom.summary = self.createDom('div', { className: 'summary' }),
|
|
||||||
dom.details = self.createDom('div', { id: 'details' }))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter);jasmine.HtmlReporter.ReporterView = function(dom) {
|
|
||||||
this.startedAt = new Date();
|
|
||||||
this.runningSpecCount = 0;
|
|
||||||
this.completeSpecCount = 0;
|
|
||||||
this.passedCount = 0;
|
|
||||||
this.failedCount = 0;
|
|
||||||
this.skippedCount = 0;
|
|
||||||
|
|
||||||
this.createResultsMenu = function() {
|
|
||||||
this.resultsMenu = this.createDom('span', {className: 'resultsMenu bar'},
|
|
||||||
this.summaryMenuItem = this.createDom('a', {className: 'summaryMenuItem', href: "#"}, '0 specs'),
|
|
||||||
' | ',
|
|
||||||
this.detailsMenuItem = this.createDom('a', {className: 'detailsMenuItem', href: "#"}, '0 failing'));
|
|
||||||
|
|
||||||
this.summaryMenuItem.onclick = function() {
|
|
||||||
dom.reporter.className = dom.reporter.className.replace(/ showDetails/g, '');
|
|
||||||
};
|
|
||||||
|
|
||||||
this.detailsMenuItem.onclick = function() {
|
|
||||||
showDetails();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
this.addSpecs = function(specs, specFilter) {
|
|
||||||
this.totalSpecCount = specs.length;
|
|
||||||
|
|
||||||
this.views = {
|
|
||||||
specs: {},
|
|
||||||
suites: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (var i = 0; i < specs.length; i++) {
|
|
||||||
var spec = specs[i];
|
|
||||||
this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom, this.views);
|
|
||||||
if (specFilter(spec)) {
|
|
||||||
this.runningSpecCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.specComplete = function(spec) {
|
|
||||||
this.completeSpecCount++;
|
|
||||||
|
|
||||||
if (isUndefined(this.views.specs[spec.id])) {
|
|
||||||
this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom);
|
|
||||||
}
|
|
||||||
|
|
||||||
var specView = this.views.specs[spec.id];
|
|
||||||
|
|
||||||
switch (specView.status()) {
|
|
||||||
case 'passed':
|
|
||||||
this.passedCount++;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'failed':
|
|
||||||
this.failedCount++;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'skipped':
|
|
||||||
this.skippedCount++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
specView.refresh();
|
|
||||||
this.refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.suiteComplete = function(suite) {
|
|
||||||
var suiteView = this.views.suites[suite.id];
|
|
||||||
if (isUndefined(suiteView)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
suiteView.refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.refresh = function() {
|
|
||||||
|
|
||||||
if (isUndefined(this.resultsMenu)) {
|
|
||||||
this.createResultsMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
// currently running UI
|
|
||||||
if (isUndefined(this.runningAlert)) {
|
|
||||||
this.runningAlert = this.createDom('a', {href: "?", className: "runningAlert bar"});
|
|
||||||
dom.alert.appendChild(this.runningAlert);
|
|
||||||
}
|
|
||||||
this.runningAlert.innerHTML = "Running " + this.completeSpecCount + " of " + specPluralizedFor(this.totalSpecCount);
|
|
||||||
|
|
||||||
// skipped specs UI
|
|
||||||
if (isUndefined(this.skippedAlert)) {
|
|
||||||
this.skippedAlert = this.createDom('a', {href: "?", className: "skippedAlert bar"});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.skippedAlert.innerHTML = "Skipping " + this.skippedCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all";
|
|
||||||
|
|
||||||
if (this.skippedCount === 1 && isDefined(dom.alert)) {
|
|
||||||
dom.alert.appendChild(this.skippedAlert);
|
|
||||||
}
|
|
||||||
|
|
||||||
// passing specs UI
|
|
||||||
if (isUndefined(this.passedAlert)) {
|
|
||||||
this.passedAlert = this.createDom('span', {href: "?", className: "passingAlert bar"});
|
|
||||||
}
|
|
||||||
this.passedAlert.innerHTML = "Passing " + specPluralizedFor(this.passedCount);
|
|
||||||
|
|
||||||
// failing specs UI
|
|
||||||
if (isUndefined(this.failedAlert)) {
|
|
||||||
this.failedAlert = this.createDom('span', {href: "?", className: "failingAlert bar"});
|
|
||||||
}
|
|
||||||
this.failedAlert.innerHTML = "Failing " + specPluralizedFor(this.failedCount);
|
|
||||||
|
|
||||||
if (this.failedCount === 1 && isDefined(dom.alert)) {
|
|
||||||
dom.alert.appendChild(this.failedAlert);
|
|
||||||
dom.alert.appendChild(this.resultsMenu);
|
|
||||||
}
|
|
||||||
|
|
||||||
// summary info
|
|
||||||
this.summaryMenuItem.innerHTML = "" + specPluralizedFor(this.runningSpecCount);
|
|
||||||
this.detailsMenuItem.innerHTML = "" + this.failedCount + " failing";
|
|
||||||
};
|
|
||||||
|
|
||||||
this.complete = function() {
|
|
||||||
dom.alert.removeChild(this.runningAlert);
|
|
||||||
|
|
||||||
this.skippedAlert.innerHTML = "Ran " + this.runningSpecCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all";
|
|
||||||
|
|
||||||
if (this.failedCount === 0) {
|
|
||||||
dom.alert.appendChild(this.createDom('span', {className: 'passingAlert bar'}, "Passing " + specPluralizedFor(this.passedCount)));
|
|
||||||
} else {
|
|
||||||
showDetails();
|
|
||||||
}
|
|
||||||
|
|
||||||
dom.banner.appendChild(this.createDom('span', {className: 'duration'}, "finished in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"));
|
|
||||||
};
|
|
||||||
|
|
||||||
return this;
|
|
||||||
|
|
||||||
function showDetails() {
|
|
||||||
if (dom.reporter.className.search(/showDetails/) === -1) {
|
|
||||||
dom.reporter.className += " showDetails";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isUndefined(obj) {
|
|
||||||
return typeof obj === 'undefined';
|
|
||||||
}
|
|
||||||
|
|
||||||
function isDefined(obj) {
|
|
||||||
return !isUndefined(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
function specPluralizedFor(count) {
|
|
||||||
var str = count + " spec";
|
|
||||||
if (count > 1) {
|
|
||||||
str += "s"
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.ReporterView);
|
|
||||||
|
|
||||||
|
|
||||||
jasmine.HtmlReporter.SpecView = function(spec, dom, views) {
|
|
||||||
this.spec = spec;
|
|
||||||
this.dom = dom;
|
|
||||||
this.views = views;
|
|
||||||
|
|
||||||
this.symbol = this.createDom('li', { className: 'pending' });
|
|
||||||
this.dom.symbolSummary.appendChild(this.symbol);
|
|
||||||
|
|
||||||
this.summary = this.createDom('div', { className: 'specSummary' },
|
|
||||||
this.createDom('a', {
|
|
||||||
className: 'description',
|
|
||||||
href: '?spec=' + encodeURIComponent(this.spec.getFullName()),
|
|
||||||
title: this.spec.getFullName()
|
|
||||||
}, this.spec.description)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.detail = this.createDom('div', { className: 'specDetail' },
|
|
||||||
this.createDom('a', {
|
|
||||||
className: 'description',
|
|
||||||
href: '?spec=' + encodeURIComponent(this.spec.getFullName()),
|
|
||||||
title: this.spec.getFullName()
|
|
||||||
}, this.spec.getFullName())
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.HtmlReporter.SpecView.prototype.status = function() {
|
|
||||||
return this.getSpecStatus(this.spec);
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.HtmlReporter.SpecView.prototype.refresh = function() {
|
|
||||||
this.symbol.className = this.status();
|
|
||||||
|
|
||||||
switch (this.status()) {
|
|
||||||
case 'skipped':
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'passed':
|
|
||||||
this.appendSummaryToSuiteDiv();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'failed':
|
|
||||||
this.appendSummaryToSuiteDiv();
|
|
||||||
this.appendFailureDetail();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.HtmlReporter.SpecView.prototype.appendSummaryToSuiteDiv = function() {
|
|
||||||
this.summary.className += ' ' + this.status();
|
|
||||||
this.appendToSummary(this.spec, this.summary);
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.HtmlReporter.SpecView.prototype.appendFailureDetail = function() {
|
|
||||||
this.detail.className += ' ' + this.status();
|
|
||||||
|
|
||||||
var resultItems = this.spec.results().getItems();
|
|
||||||
var messagesDiv = this.createDom('div', { className: 'messages' });
|
|
||||||
|
|
||||||
for (var i = 0; i < resultItems.length; i++) {
|
|
||||||
var result = resultItems[i];
|
|
||||||
|
|
||||||
if (result.type == 'log') {
|
|
||||||
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
|
|
||||||
} else if (result.type == 'expect' && result.passed && !result.passed()) {
|
|
||||||
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));
|
|
||||||
|
|
||||||
if (result.trace.stack) {
|
|
||||||
messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messagesDiv.childNodes.length > 0) {
|
|
||||||
this.detail.appendChild(messagesDiv);
|
|
||||||
this.dom.details.appendChild(this.detail);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SpecView);jasmine.HtmlReporter.SuiteView = function(suite, dom, views) {
|
|
||||||
this.suite = suite;
|
|
||||||
this.dom = dom;
|
|
||||||
this.views = views;
|
|
||||||
|
|
||||||
this.element = this.createDom('div', { className: 'suite' },
|
|
||||||
this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(this.suite.getFullName()) }, this.suite.description)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.appendToSummary(this.suite, this.element);
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.HtmlReporter.SuiteView.prototype.status = function() {
|
|
||||||
return this.getSpecStatus(this.suite);
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.HtmlReporter.SuiteView.prototype.refresh = function() {
|
|
||||||
this.element.className += " " + this.status();
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SuiteView);
|
|
||||||
|
|
||||||
/* @deprecated Use jasmine.HtmlReporter instead
|
|
||||||
*/
|
|
||||||
jasmine.TrivialReporter = function(doc) {
|
|
||||||
this.document = doc || document;
|
|
||||||
this.suiteDivs = {};
|
|
||||||
this.logRunningSpecs = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) {
|
|
||||||
var el = document.createElement(type);
|
|
||||||
|
|
||||||
for (var i = 2; i < arguments.length; i++) {
|
|
||||||
var child = arguments[i];
|
|
||||||
|
|
||||||
if (typeof child === 'string') {
|
|
||||||
el.appendChild(document.createTextNode(child));
|
|
||||||
} else {
|
|
||||||
if (child) { el.appendChild(child); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var attr in attrs) {
|
|
||||||
if (attr == "className") {
|
|
||||||
el[attr] = attrs[attr];
|
|
||||||
} else {
|
|
||||||
el.setAttribute(attr, attrs[attr]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return el;
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) {
|
|
||||||
var showPassed, showSkipped;
|
|
||||||
|
|
||||||
this.outerDiv = this.createDom('div', { id: 'TrivialReporter', className: 'jasmine_reporter' },
|
|
||||||
this.createDom('div', { className: 'banner' },
|
|
||||||
this.createDom('div', { className: 'logo' },
|
|
||||||
this.createDom('span', { className: 'title' }, "Jasmine"),
|
|
||||||
this.createDom('span', { className: 'version' }, runner.env.versionString())),
|
|
||||||
this.createDom('div', { className: 'options' },
|
|
||||||
"Show ",
|
|
||||||
showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }),
|
|
||||||
this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "),
|
|
||||||
showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }),
|
|
||||||
this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
this.runnerDiv = this.createDom('div', { className: 'runner running' },
|
|
||||||
this.createDom('a', { className: 'run_spec', href: '?' }, "run all"),
|
|
||||||
this.runnerMessageSpan = this.createDom('span', {}, "Running..."),
|
|
||||||
this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, ""))
|
|
||||||
);
|
|
||||||
|
|
||||||
this.document.body.appendChild(this.outerDiv);
|
|
||||||
|
|
||||||
var suites = runner.suites();
|
|
||||||
for (var i = 0; i < suites.length; i++) {
|
|
||||||
var suite = suites[i];
|
|
||||||
var suiteDiv = this.createDom('div', { className: 'suite' },
|
|
||||||
this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"),
|
|
||||||
this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description));
|
|
||||||
this.suiteDivs[suite.id] = suiteDiv;
|
|
||||||
var parentDiv = this.outerDiv;
|
|
||||||
if (suite.parentSuite) {
|
|
||||||
parentDiv = this.suiteDivs[suite.parentSuite.id];
|
|
||||||
}
|
|
||||||
parentDiv.appendChild(suiteDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.startedAt = new Date();
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
showPassed.onclick = function(evt) {
|
|
||||||
if (showPassed.checked) {
|
|
||||||
self.outerDiv.className += ' show-passed';
|
|
||||||
} else {
|
|
||||||
self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, '');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
showSkipped.onclick = function(evt) {
|
|
||||||
if (showSkipped.checked) {
|
|
||||||
self.outerDiv.className += ' show-skipped';
|
|
||||||
} else {
|
|
||||||
self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, '');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) {
|
|
||||||
var results = runner.results();
|
|
||||||
var className = (results.failedCount > 0) ? "runner failed" : "runner passed";
|
|
||||||
this.runnerDiv.setAttribute("class", className);
|
|
||||||
//do it twice for IE
|
|
||||||
this.runnerDiv.setAttribute("className", className);
|
|
||||||
var specs = runner.specs();
|
|
||||||
var specCount = 0;
|
|
||||||
for (var i = 0; i < specs.length; i++) {
|
|
||||||
if (this.specFilter(specs[i])) {
|
|
||||||
specCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s");
|
|
||||||
message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s";
|
|
||||||
this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild);
|
|
||||||
|
|
||||||
this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString()));
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) {
|
|
||||||
var results = suite.results();
|
|
||||||
var status = results.passed() ? 'passed' : 'failed';
|
|
||||||
if (results.totalCount === 0) { // todo: change this to check results.skipped
|
|
||||||
status = 'skipped';
|
|
||||||
}
|
|
||||||
this.suiteDivs[suite.id].className += " " + status;
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) {
|
|
||||||
if (this.logRunningSpecs) {
|
|
||||||
this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) {
|
|
||||||
var results = spec.results();
|
|
||||||
var status = results.passed() ? 'passed' : 'failed';
|
|
||||||
if (results.skipped) {
|
|
||||||
status = 'skipped';
|
|
||||||
}
|
|
||||||
var specDiv = this.createDom('div', { className: 'spec ' + status },
|
|
||||||
this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"),
|
|
||||||
this.createDom('a', {
|
|
||||||
className: 'description',
|
|
||||||
href: '?spec=' + encodeURIComponent(spec.getFullName()),
|
|
||||||
title: spec.getFullName()
|
|
||||||
}, spec.description));
|
|
||||||
|
|
||||||
|
|
||||||
var resultItems = results.getItems();
|
|
||||||
var messagesDiv = this.createDom('div', { className: 'messages' });
|
|
||||||
for (var i = 0; i < resultItems.length; i++) {
|
|
||||||
var result = resultItems[i];
|
|
||||||
|
|
||||||
if (result.type == 'log') {
|
|
||||||
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
|
|
||||||
} else if (result.type == 'expect' && result.passed && !result.passed()) {
|
|
||||||
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));
|
|
||||||
|
|
||||||
if (result.trace.stack) {
|
|
||||||
messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messagesDiv.childNodes.length > 0) {
|
|
||||||
specDiv.appendChild(messagesDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.suiteDivs[spec.suite.id].appendChild(specDiv);
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.TrivialReporter.prototype.log = function() {
|
|
||||||
var console = jasmine.getGlobal().console;
|
|
||||||
if (console && console.log) {
|
|
||||||
if (console.log.apply) {
|
|
||||||
console.log.apply(console, arguments);
|
|
||||||
} else {
|
|
||||||
console.log(arguments); // ie fix: console.log.apply doesn't exist on ie
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.TrivialReporter.prototype.getLocation = function() {
|
|
||||||
return this.document.location;
|
|
||||||
};
|
|
||||||
|
|
||||||
jasmine.TrivialReporter.prototype.specFilter = function(spec) {
|
|
||||||
var paramMap = {};
|
|
||||||
var params = this.getLocation().search.substring(1).split('&');
|
|
||||||
for (var i = 0; i < params.length; i++) {
|
|
||||||
var p = params[i].split('=');
|
|
||||||
paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!paramMap.spec) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return spec.getFullName().indexOf(paramMap.spec) === 0;
|
|
||||||
};
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; }
|
|
||||||
|
|
||||||
#HTMLReporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; }
|
|
||||||
#HTMLReporter a { text-decoration: none; }
|
|
||||||
#HTMLReporter a:hover { text-decoration: underline; }
|
|
||||||
#HTMLReporter p, #HTMLReporter h1, #HTMLReporter h2, #HTMLReporter h3, #HTMLReporter h4, #HTMLReporter h5, #HTMLReporter h6 { margin: 0; line-height: 14px; }
|
|
||||||
#HTMLReporter .banner, #HTMLReporter .symbolSummary, #HTMLReporter .summary, #HTMLReporter .resultMessage, #HTMLReporter .specDetail .description, #HTMLReporter .alert .bar, #HTMLReporter .stackTrace { padding-left: 9px; padding-right: 9px; }
|
|
||||||
#HTMLReporter #jasmine_content { position: fixed; right: 100%; }
|
|
||||||
#HTMLReporter .version { color: #aaaaaa; }
|
|
||||||
#HTMLReporter .banner { margin-top: 14px; }
|
|
||||||
#HTMLReporter .duration { color: #aaaaaa; float: right; }
|
|
||||||
#HTMLReporter .symbolSummary { overflow: hidden; *zoom: 1; margin: 14px 0; }
|
|
||||||
#HTMLReporter .symbolSummary li { display: block; float: left; height: 7px; width: 14px; margin-bottom: 7px; font-size: 16px; }
|
|
||||||
#HTMLReporter .symbolSummary li.passed { font-size: 14px; }
|
|
||||||
#HTMLReporter .symbolSummary li.passed:before { color: #5e7d00; content: "\02022"; }
|
|
||||||
#HTMLReporter .symbolSummary li.failed { line-height: 9px; }
|
|
||||||
#HTMLReporter .symbolSummary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; }
|
|
||||||
#HTMLReporter .symbolSummary li.skipped { font-size: 14px; }
|
|
||||||
#HTMLReporter .symbolSummary li.skipped:before { color: #bababa; content: "\02022"; }
|
|
||||||
#HTMLReporter .symbolSummary li.pending { line-height: 11px; }
|
|
||||||
#HTMLReporter .symbolSummary li.pending:before { color: #aaaaaa; content: "-"; }
|
|
||||||
#HTMLReporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; }
|
|
||||||
#HTMLReporter .runningAlert { background-color: #666666; }
|
|
||||||
#HTMLReporter .skippedAlert { background-color: #aaaaaa; }
|
|
||||||
#HTMLReporter .skippedAlert:first-child { background-color: #333333; }
|
|
||||||
#HTMLReporter .skippedAlert:hover { text-decoration: none; color: white; text-decoration: underline; }
|
|
||||||
#HTMLReporter .passingAlert { background-color: #a6b779; }
|
|
||||||
#HTMLReporter .passingAlert:first-child { background-color: #5e7d00; }
|
|
||||||
#HTMLReporter .failingAlert { background-color: #cf867e; }
|
|
||||||
#HTMLReporter .failingAlert:first-child { background-color: #b03911; }
|
|
||||||
#HTMLReporter .results { margin-top: 14px; }
|
|
||||||
#HTMLReporter #details { display: none; }
|
|
||||||
#HTMLReporter .resultsMenu, #HTMLReporter .resultsMenu a { background-color: #fff; color: #333333; }
|
|
||||||
#HTMLReporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; }
|
|
||||||
#HTMLReporter.showDetails .summaryMenuItem:hover { text-decoration: underline; }
|
|
||||||
#HTMLReporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; }
|
|
||||||
#HTMLReporter.showDetails .summary { display: none; }
|
|
||||||
#HTMLReporter.showDetails #details { display: block; }
|
|
||||||
#HTMLReporter .summaryMenuItem { font-weight: bold; text-decoration: underline; }
|
|
||||||
#HTMLReporter .summary { margin-top: 14px; }
|
|
||||||
#HTMLReporter .summary .suite .suite, #HTMLReporter .summary .specSummary { margin-left: 14px; }
|
|
||||||
#HTMLReporter .summary .specSummary.passed a { color: #5e7d00; }
|
|
||||||
#HTMLReporter .summary .specSummary.failed a { color: #b03911; }
|
|
||||||
#HTMLReporter .description + .suite { margin-top: 0; }
|
|
||||||
#HTMLReporter .suite { margin-top: 14px; }
|
|
||||||
#HTMLReporter .suite a { color: #333333; }
|
|
||||||
#HTMLReporter #details .specDetail { margin-bottom: 28px; }
|
|
||||||
#HTMLReporter #details .specDetail .description { display: block; color: white; background-color: #b03911; }
|
|
||||||
#HTMLReporter .resultMessage { padding-top: 14px; color: #333333; }
|
|
||||||
#HTMLReporter .resultMessage span.result { display: block; }
|
|
||||||
#HTMLReporter .stackTrace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; }
|
|
||||||
|
|
||||||
#TrivialReporter { padding: 8px 13px; position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow-y: scroll; background-color: white; font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; /*.resultMessage {*/ /*white-space: pre;*/ /*}*/ }
|
|
||||||
#TrivialReporter a:visited, #TrivialReporter a { color: #303; }
|
|
||||||
#TrivialReporter a:hover, #TrivialReporter a:active { color: blue; }
|
|
||||||
#TrivialReporter .run_spec { float: right; padding-right: 5px; font-size: .8em; text-decoration: none; }
|
|
||||||
#TrivialReporter .banner { color: #303; background-color: #fef; padding: 5px; }
|
|
||||||
#TrivialReporter .logo { float: left; font-size: 1.1em; padding-left: 5px; }
|
|
||||||
#TrivialReporter .logo .version { font-size: .6em; padding-left: 1em; }
|
|
||||||
#TrivialReporter .runner.running { background-color: yellow; }
|
|
||||||
#TrivialReporter .options { text-align: right; font-size: .8em; }
|
|
||||||
#TrivialReporter .suite { border: 1px outset gray; margin: 5px 0; padding-left: 1em; }
|
|
||||||
#TrivialReporter .suite .suite { margin: 5px; }
|
|
||||||
#TrivialReporter .suite.passed { background-color: #dfd; }
|
|
||||||
#TrivialReporter .suite.failed { background-color: #fdd; }
|
|
||||||
#TrivialReporter .spec { margin: 5px; padding-left: 1em; clear: both; }
|
|
||||||
#TrivialReporter .spec.failed, #TrivialReporter .spec.passed, #TrivialReporter .spec.skipped { padding-bottom: 5px; border: 1px solid gray; }
|
|
||||||
#TrivialReporter .spec.failed { background-color: #fbb; border-color: red; }
|
|
||||||
#TrivialReporter .spec.passed { background-color: #bfb; border-color: green; }
|
|
||||||
#TrivialReporter .spec.skipped { background-color: #bbb; }
|
|
||||||
#TrivialReporter .messages { border-left: 1px dashed gray; padding-left: 1em; padding-right: 1em; }
|
|
||||||
#TrivialReporter .passed { background-color: #cfc; display: none; }
|
|
||||||
#TrivialReporter .failed { background-color: #fbb; }
|
|
||||||
#TrivialReporter .skipped { color: #777; background-color: #eee; display: none; }
|
|
||||||
#TrivialReporter .resultMessage span.result { display: block; line-height: 2em; color: black; }
|
|
||||||
#TrivialReporter .resultMessage .mismatch { color: black; }
|
|
||||||
#TrivialReporter .stackTrace { white-space: pre; font-size: .8em; margin-left: 10px; max-height: 5em; overflow: auto; border: 1px inset red; padding: 1em; background: #eef; }
|
|
||||||
#TrivialReporter .finished-at { padding-left: 1em; font-size: .6em; }
|
|
||||||
#TrivialReporter.show-passed .passed, #TrivialReporter.show-skipped .skipped { display: block; }
|
|
||||||
#TrivialReporter #jasmine_content { position: fixed; right: 100%; }
|
|
||||||
#TrivialReporter .runner { border: 1px solid gray; display: block; margin: 5px 0; padding: 2px 0 2px 10px; }
|
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# update a cordova/android project's command line tools
|
||||||
|
#
|
||||||
|
# USAGE
|
||||||
|
# ./update [path]
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -z "$1" ] || [ "$1" == "-h" ]
|
||||||
|
then
|
||||||
|
echo 'usage: update path'
|
||||||
|
echo "Make sure the Android SDK tools folder is in your PATH!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
BUILD_PATH="$( cd "$( dirname "$0" )/.." && pwd )"
|
||||||
|
VERSION=$(cat "$BUILD_PATH"/VERSION)
|
||||||
|
|
||||||
|
PROJECT_PATH="${1:-'./example'}"
|
||||||
|
|
||||||
|
if [ ! -d "$PROJECT_PATH" ]
|
||||||
|
then
|
||||||
|
echo "The project path has to exist for it to be updated"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# cleanup after exit and/or on error
|
||||||
|
function on_exit {
|
||||||
|
if [ -f "$BUILD_PATH"/framework/cordova-$VERSION.jar ]
|
||||||
|
then
|
||||||
|
rm "$BUILD_PATH"/framework/cordova-$VERSION.jar
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAppInfoJar {
|
||||||
|
(cd "$BUILD_PATH"/bin/templates/cordova/ApplicationInfo &&
|
||||||
|
javac ApplicationInfo.java &&
|
||||||
|
jar -cfe ../appinfo.jar ApplicationInfo ApplicationInfo.class
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function on_error {
|
||||||
|
echo "An unexpected error occurred: $previous_command exited with $?"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function replace {
|
||||||
|
local pattern=$1
|
||||||
|
local filename=$2
|
||||||
|
# Mac OS X requires -i argument
|
||||||
|
if [[ "$OSTYPE" =~ "darwin" ]]
|
||||||
|
then
|
||||||
|
/usr/bin/sed -i '' -e $pattern "$filename"
|
||||||
|
elif [[ "$OSTYPE" =~ "linux" ]]
|
||||||
|
then
|
||||||
|
/bin/sed -i -e $pattern "$filename"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# we do not want the script to silently fail
|
||||||
|
trap 'previous_command=$this_command; this_command=$BASH_COMMAND' DEBUG
|
||||||
|
trap on_error ERR
|
||||||
|
trap on_exit EXIT
|
||||||
|
|
||||||
|
ANDROID_BIN="${ANDROID_BIN:=$( which android )}"
|
||||||
|
|
||||||
|
TARGET=$("$ANDROID_BIN" list targets | grep id: | tail -1 | cut -f 2 -d ' ' )
|
||||||
|
API_LEVEL=$("$ANDROID_BIN" list target | grep "API level:" | tail -n 1 | cut -f 2 -d ':' | tr -d ' ')
|
||||||
|
|
||||||
|
# check that build targets exist
|
||||||
|
if [ -z "$TARGET" ] || [ -z "$API_LEVEL" ]
|
||||||
|
then
|
||||||
|
echo "No Android Targets are installed. Please install at least one via the android SDK"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# if this a distribution release no need to build a jar
|
||||||
|
if [ ! -e "$BUILD_PATH"/cordova-$VERSION.jar ] && [ -d "$BUILD_PATH"/framework ]
|
||||||
|
then
|
||||||
|
# update the cordova-android framework for the desired target
|
||||||
|
"$ANDROID_BIN" update project --target $TARGET --path "$BUILD_PATH"/framework &> /dev/null
|
||||||
|
|
||||||
|
# compile cordova.js and cordova.jar
|
||||||
|
(cd "$BUILD_PATH"/framework && ant jar &> /dev/null )
|
||||||
|
fi
|
||||||
|
|
||||||
|
# copy cordova.js, cordova.jar and res/xml
|
||||||
|
if [ -d "$BUILD_PATH"/framework ]
|
||||||
|
then
|
||||||
|
cp "$BUILD_PATH"/framework/assets/www/cordova.js "$PROJECT_PATH"/assets/www/cordova.js
|
||||||
|
cp "$BUILD_PATH"/framework/cordova-$VERSION.jar "$PROJECT_PATH"/libs/cordova-$VERSION.jar
|
||||||
|
else
|
||||||
|
cp "$BUILD_PATH"/cordova.js "$PROJECT_PATH"/assets/www/cordova.js
|
||||||
|
cp "$BUILD_PATH"/cordova-$VERSION.jar "$PROJECT_PATH"/libs/cordova-$VERSION.jar
|
||||||
|
fi
|
||||||
|
|
||||||
|
# creating cordova folder and copying run/build/log/launch scripts
|
||||||
|
if [ ! -e "$PROJECT_PATH/cordova" ]
|
||||||
|
then
|
||||||
|
mkdir "$PROJECT_PATH"/cordova
|
||||||
|
mkdir "$PROJECT_PATH"/cordova/lib
|
||||||
|
fi
|
||||||
|
cp "$BUILD_PATH"/bin/templates/cordova/appinfo.jar "$PROJECT_PATH"/cordova/appinfo.jar
|
||||||
|
cp "$BUILD_PATH"/bin/templates/cordova/build "$PROJECT_PATH"/cordova/build
|
||||||
|
cp "$BUILD_PATH"/bin/templates/cordova/clean "$PROJECT_PATH"/cordova/clean
|
||||||
|
cp "$BUILD_PATH"/bin/templates/cordova/log "$PROJECT_PATH"/cordova/log
|
||||||
|
cp "$BUILD_PATH"/bin/templates/cordova/run "$PROJECT_PATH"/cordova/run
|
||||||
|
cp "$BUILD_PATH"/bin/templates/cordova/lib/cordova.js "$PROJECT_PATH"/cordova/lib/cordova.js
|
||||||
|
cp "$BUILD_PATH"/bin/templates/cordova/lib/install-device "$PROJECT_PATH"/cordova/lib/install-device
|
||||||
|
cp "$BUILD_PATH"/bin/templates/cordova/lib/install-emulator "$PROJECT_PATH"/cordova/lib/install-emulator
|
||||||
|
cp "$BUILD_PATH"/bin/templates/cordova/lib/list-devices "$PROJECT_PATH"/cordova/lib/list-devices
|
||||||
|
cp "$BUILD_PATH"/bin/templates/cordova/lib/list-emulator-images "$PROJECT_PATH"/cordova/lib/list-emulator-images
|
||||||
|
cp "$BUILD_PATH"/bin/templates/cordova/lib/list-started-emulators "$PROJECT_PATH"/cordova/lib/list-started-emulators
|
||||||
|
cp "$BUILD_PATH"/bin/templates/cordova/lib/start-emulator "$PROJECT_PATH"/cordova/lib/start-emulator
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
:: 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.
|
||||||
|
|
||||||
|
@ECHO OFF
|
||||||
|
IF NOT DEFINED JAVA_HOME GOTO MISSING
|
||||||
|
FOR %%X in (java.exe javac.exe ant.bat android.bat) do (
|
||||||
|
SET FOUND=%%~$PATH:X
|
||||||
|
IF NOT DEFINED FOUND GOTO MISSING
|
||||||
|
)
|
||||||
|
cscript "%~dp0\update.js" %*
|
||||||
|
GOTO END
|
||||||
|
:MISSING
|
||||||
|
ECHO Missing one of the following:
|
||||||
|
ECHO JDK: http://java.oracle.com
|
||||||
|
ECHO Android SDK: http://developer.android.com
|
||||||
|
ECHO Apache ant: http://ant.apache.org
|
||||||
|
EXIT /B 1
|
||||||
|
:END
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* create a cordova/android project
|
||||||
|
*
|
||||||
|
* USAGE
|
||||||
|
* ./update [path]
|
||||||
|
*/
|
||||||
|
|
||||||
|
var fso = WScript.CreateObject('Scripting.FileSystemObject');
|
||||||
|
|
||||||
|
function read(filename) {
|
||||||
|
var fso=WScript.CreateObject("Scripting.FileSystemObject");
|
||||||
|
var f=fso.OpenTextFile(filename, 1);
|
||||||
|
var s=f.ReadAll();
|
||||||
|
f.Close();
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkTargets(targets) {
|
||||||
|
if(!targets) {
|
||||||
|
WScript.Echo("You do not have any android targets setup. Please create at least one target with the `android` command");
|
||||||
|
WScript.Quit(69);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTarget() {
|
||||||
|
var targets = shell.Exec('android.bat list targets').StdOut.ReadAll().match(/id:\s\d+/g);
|
||||||
|
checkTargets(targets);
|
||||||
|
return targets[targets.length - 1].replace(/id: /, ""); // TODO: give users the option to set their target
|
||||||
|
}
|
||||||
|
|
||||||
|
function setApiLevel() {
|
||||||
|
var targets = shell.Exec('android.bat list targets').StdOut.ReadAll().match(/API level:\s\d+/g);
|
||||||
|
checkTargets(targets);
|
||||||
|
return targets[targets.length - 1].replace(/API level: /, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function write(filename, contents) {
|
||||||
|
var fso=WScript.CreateObject("Scripting.FileSystemObject");
|
||||||
|
var f=fso.OpenTextFile(filename, 2, true);
|
||||||
|
f.Write(contents);
|
||||||
|
f.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceInFile(filename, regexp, replacement) {
|
||||||
|
write(filename, read(filename).replace(regexp, replacement));
|
||||||
|
}
|
||||||
|
|
||||||
|
function exec(command) {
|
||||||
|
var oShell=shell.Exec(command);
|
||||||
|
while (oShell.Status == 0) {
|
||||||
|
if(!oShell.StdOut.AtEndOfStream) {
|
||||||
|
var line = oShell.StdOut.ReadLine();
|
||||||
|
// XXX: Change to verbose mode
|
||||||
|
// WScript.StdOut.WriteLine(line);
|
||||||
|
}
|
||||||
|
WScript.sleep(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAppInfoJar() {
|
||||||
|
if(!fso.FileExists(ROOT+"\\bin\\templates\\cordova\\appinfo.jar")) {
|
||||||
|
WScript.Echo("Creating appinfo.jar...");
|
||||||
|
var cur = shell.CurrentDirectory;
|
||||||
|
shell.CurrentDirectory = ROOT+"\\bin\\templates\\cordova\\ApplicationInfo";
|
||||||
|
exec("javac ApplicationInfo.java");
|
||||||
|
exec("jar -cfe ..\\appinfo.jar ApplicationInfo ApplicationInfo.class");
|
||||||
|
shell.CurrentDirectory = cur;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
if(fso.FileExists(ROOT + '\\framework\\cordova-'+VERSION+'.jar')) {
|
||||||
|
fso.DeleteFile(ROOT + '\\framework\\cordova-'+VERSION+'.jar');
|
||||||
|
}
|
||||||
|
if(fso.FileExists(ROOT + '\\framework\\assets\\www\\cordova-'+VERSION+'.js')) {
|
||||||
|
fso.DeleteFile(ROOT + '\\framework\\assets\\www\\cordova-'+VERSION+'.js');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var args = WScript.Arguments, PROJECT_PATH="example",
|
||||||
|
shell=WScript.CreateObject("WScript.Shell");
|
||||||
|
|
||||||
|
// working dir
|
||||||
|
var ROOT = WScript.ScriptFullName.split('\\bin\\update.js').join('');
|
||||||
|
|
||||||
|
if (args.Count() == 1) {
|
||||||
|
PROJECT_PATH=args(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!fso.FolderExists(PROJECT_PATH)) {
|
||||||
|
WScript.Echo("Project doesn't exist!");
|
||||||
|
WScript.Quit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var TARGET=setTarget();
|
||||||
|
var API_LEVEL=setApiLevel();
|
||||||
|
var VERSION=read(ROOT+'\\VERSION').replace(/\r\n/,'').replace(/\n/,'');
|
||||||
|
|
||||||
|
// build from source. distro should have these files
|
||||||
|
if (!fso.FileExists(ROOT+'\\cordova-'+VERSION+'.jar') &&
|
||||||
|
!fso.FileExists(ROOT+'\\cordova-'+VERSION+'.js')) {
|
||||||
|
WScript.Echo("Building jar and js files...");
|
||||||
|
// update the cordova framework project to a target that exists on this machine
|
||||||
|
exec('android.bat update project --target '+TARGET+' --path '+ROOT+'\\framework');
|
||||||
|
exec('ant.bat -f \"'+ ROOT +'\\framework\\build.xml\" jar');
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we have the source or the distro files
|
||||||
|
WScript.Echo("Copying js, jar & config.xml files...");
|
||||||
|
if(fso.FolderExists(ROOT + '\\framework')) {
|
||||||
|
exec('%comspec% /c copy "'+ROOT+'"\\framework\\assets\\www\\cordova-'+VERSION+'.js '+PROJECT_PATH+'\\assets\\www\\cordova-'+VERSION+'.js /Y');
|
||||||
|
exec('%comspec% /c copy "'+ROOT+'"\\framework\\cordova-'+VERSION+'.jar '+PROJECT_PATH+'\\libs\\cordova-'+VERSION+'.jar /Y');
|
||||||
|
} else {
|
||||||
|
// copy in cordova.js
|
||||||
|
exec('%comspec% /c copy "'+ROOT+'"\\cordova-'+VERSION+'.js '+PROJECT_PATH+'\\assets\\www\\cordova-'+VERSION+'.js /Y');
|
||||||
|
// copy in cordova.jar
|
||||||
|
exec('%comspec% /c copy "'+ROOT+'"\\cordova-'+VERSION+'.jar '+PROJECT_PATH+'\\libs\\cordova-'+VERSION+'.jar /Y');
|
||||||
|
// copy in xml
|
||||||
|
}
|
||||||
|
|
||||||
|
// update cordova scripts
|
||||||
|
createAppInfoJar();
|
||||||
|
WScript.Echo("Copying cordova command tools...");
|
||||||
|
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\cordova\\appinfo.jar ' + PROJECT_PATH + '\\cordova\\appinfo.jar /Y');
|
||||||
|
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\cordova\\cordova.bat ' + PROJECT_PATH + '\\cordova\\cordova.bat /Y');
|
||||||
|
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\cordova\\clean.bat ' + PROJECT_PATH + '\\cordova\\clean.bat /Y');
|
||||||
|
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\cordova\\build.bat ' + PROJECT_PATH + '\\cordova\\build.bat /Y');
|
||||||
|
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\cordova\\log.bat ' + PROJECT_PATH + '\\cordova\\log.bat /Y');
|
||||||
|
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\cordova\\run.bat ' + PROJECT_PATH + '\\cordova\\run.bat /Y');
|
||||||
|
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\cordova\\lib\\cordova.js ' + PROJECT_PATH + '\\cordova\\lib\\cordova.js /Y');
|
||||||
|
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\cordova\\lib\\install-device.bat ' + PROJECT_PATH + '\\cordova\\lib\\install-device.bat /Y');
|
||||||
|
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\cordova\\lib\\install-emulator.bat ' + PROJECT_PATH + '\\cordova\\lib\\install-emulator.bat /Y');
|
||||||
|
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\cordova\\lib\\list-emulator-images.bat ' + PROJECT_PATH + '\\cordova\\lib\\list-emulator-images.bat /Y');
|
||||||
|
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\cordova\\lib\\list-devices.bat ' + PROJECT_PATH + '\\cordova\\lib\\list-devices.bat /Y');
|
||||||
|
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\cordova\\lib\\list-started-emulators.bat ' + PROJECT_PATH + '\\cordova\\lib\\list-started-emulators.bat /Y');
|
||||||
|
exec('%comspec% /c copy "'+ROOT+'"\\bin\\templates\\cordova\\lib\\start-emulator.bat ' + PROJECT_PATH + '\\cordova\\lib\\start-emulator.bat /Y');
|
||||||
|
|
||||||
|
cleanup();
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
|
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
|
||||||
<classpathentry kind="src" path="src"/>
|
<classpathentry kind="src" path="src"/>
|
||||||
<classpathentry kind="src" path="gen"/>
|
<classpathentry kind="src" path="gen"/>
|
||||||
<classpathentry kind="lib" path="libs/commons-codec-1.7.jar"/>
|
|
||||||
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
|
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
|
||||||
<classpathentry kind="output" path="bin/classes"/>
|
<classpathentry kind="output" path="bin/classes"/>
|
||||||
</classpath>
|
</classpath>
|
||||||
|
|||||||
@@ -50,16 +50,11 @@
|
|||||||
|
|
||||||
<application android:icon="@drawable/icon" android:label="@string/app_name"
|
<application android:icon="@drawable/icon" android:label="@string/app_name"
|
||||||
android:debuggable="true">
|
android:debuggable="true">
|
||||||
<activity android:name=".StandAlone" android:windowSoftInputMode="adjustPan"
|
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity android:name="org.apache.cordova.DroidGap" android:label="@string/app_name"
|
<activity android:name="org.apache.cordova.DroidGap" android:label="@string/app_name"
|
||||||
android:configChanges="orientation|keyboardHidden">
|
android:configChanges="orientation|keyboardHidden">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title></title>
|
<title></title>
|
||||||
<script src="cordova-2.5.0rc1.js"></script>
|
<script src="cordova.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|||||||
@@ -26,28 +26,11 @@
|
|||||||
</filterchain>
|
</filterchain>
|
||||||
</loadfile>
|
</loadfile>
|
||||||
|
|
||||||
<!-- check that the version of ant is at least 1.8.0, as is needed
|
<!-- check that the version of ant is at least 1.8.0 -->
|
||||||
for the dblQuote property -->
|
|
||||||
<antversion property="thisantversion" atleast="1.8.0" />
|
<antversion property="thisantversion" atleast="1.8.0" />
|
||||||
<fail message="The required minimum version of ant is 1.8.0, you have ${ant.version}"
|
<fail message="The required minimum version of ant is 1.8.0, you have ${ant.version}"
|
||||||
unless="thisantversion" />
|
unless="thisantversion" />
|
||||||
|
|
||||||
<!-- check that commons codec is available. You should copy the codec jar to
|
|
||||||
framework/libs, as it is not included in the Cordova distribution.
|
|
||||||
The name of the jar file in framework/libs does not matter. -->
|
|
||||||
<available classname="org.apache.commons.codec.binary.Base64"
|
|
||||||
property="exists.base64"
|
|
||||||
ignoresystemclasses="true">
|
|
||||||
<classpath>
|
|
||||||
<pathelement path="${classpath}" />
|
|
||||||
<fileset dir="libs">
|
|
||||||
<include name="*.jar" />
|
|
||||||
</fileset>
|
|
||||||
</classpath>
|
|
||||||
</available>
|
|
||||||
<fail message="You need to put a copy of Apache Commons Codec jar in the framework/libs directory"
|
|
||||||
unless="exists.base64" />
|
|
||||||
|
|
||||||
<!-- The local.properties file is created and updated by the 'android'
|
<!-- The local.properties file is created and updated by the 'android'
|
||||||
tool. (For example "sdkdir/tools/android update project -p ." inside
|
tool. (For example "sdkdir/tools/android update project -p ." inside
|
||||||
of this directory where the AndroidManifest.xml file exists. This
|
of this directory where the AndroidManifest.xml file exists. This
|
||||||
@@ -80,9 +63,6 @@
|
|||||||
-->
|
-->
|
||||||
<property file="ant.properties" />
|
<property file="ant.properties" />
|
||||||
|
|
||||||
<!-- We need to setup the double quote. -->
|
|
||||||
<property name="dblQuote">"</property>
|
|
||||||
|
|
||||||
<!-- The project.properties file is created and updated by the 'android'
|
<!-- The project.properties file is created and updated by the 'android'
|
||||||
tool, as well as ADT.
|
tool, as well as ADT.
|
||||||
|
|
||||||
@@ -136,33 +116,10 @@
|
|||||||
-->
|
-->
|
||||||
<import file="${sdk.dir}/tools/ant/build.xml" />
|
<import file="${sdk.dir}/tools/ant/build.xml" />
|
||||||
|
|
||||||
<!-- Combine JavaScript files into one cordova-uncompressed.js file. -->
|
|
||||||
<target name="build-javascript" depends="clean">
|
|
||||||
|
|
||||||
<!-- Clean up existing files -->
|
|
||||||
<!--<delete file="assets/www/cordova_${version}.js"/>-->
|
|
||||||
|
|
||||||
<!-- Create uncompressed JS file -->
|
|
||||||
<concat destfile="assets/www/cordova-${version}.js">
|
|
||||||
<filelist dir="assets/js" files="cordova.android.js"/>
|
|
||||||
</concat>
|
|
||||||
|
|
||||||
<!-- update project files to reference cordova-x.x.x.min.js -->
|
|
||||||
<replaceregexp match="cordova(.*)\.js" replace="cordova-${version}.js" byline="true">
|
|
||||||
<fileset file="assets/www/index.html" />
|
|
||||||
<fileset file="../bin/templates/project/assets/www/index.html" />
|
|
||||||
</replaceregexp>
|
|
||||||
|
|
||||||
<!-- This is sketchy, but it works, ${dblQuote} does not -->
|
|
||||||
<replaceregexp match="cordovaVersion = [\u0022].*[\u0022];" replace='cordovaVersion = ${dblQuote}${version}${dblQuote};' byline="true">
|
|
||||||
<fileset file="src/org/apache/cordova/Device.java" />
|
|
||||||
</replaceregexp>
|
|
||||||
</target>
|
|
||||||
|
|
||||||
<!-- Build Cordova jar file that includes all native code, and Cordova JS file
|
<!-- Build Cordova jar file that includes all native code, and Cordova JS file
|
||||||
that includes all JavaScript code.
|
that includes all JavaScript code.
|
||||||
-->
|
-->
|
||||||
<target name="jar" depends="build-javascript, -compile">
|
<target name="jar" depends="-compile">
|
||||||
<jar jarfile="cordova-${version}.jar" basedir="bin/classes" excludes="org/apache/cordova/R.class,org/apache/cordova/R$*.class"/>
|
<jar jarfile="cordova-${version}.jar" basedir="bin/classes" excludes="org/apache/cordova/R.class,org/apache/cordova/R$*.class"/>
|
||||||
</target>
|
</target>
|
||||||
|
|
||||||
@@ -195,10 +152,10 @@
|
|||||||
</junit>
|
</junit>
|
||||||
</target>
|
</target>
|
||||||
|
|
||||||
<target name="cordova_debug" depends="build-javascript, debug">
|
<target name="cordova_debug" depends="debug">
|
||||||
</target>
|
</target>
|
||||||
|
|
||||||
<target name="cordova_release" depends="build-javascript, release">
|
<target name="cordova_release" depends="release">
|
||||||
</target>
|
</target>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -1,60 +1,107 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!--
|
<!--
|
||||||
Licensed to the Apache Software Foundation (ASF) under one
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
or more contributor license agreements. See the NOTICE file
|
or more contributor license agreements. See the NOTICE file
|
||||||
distributed with this work for additional information
|
distributed with this work for additional information
|
||||||
regarding copyright ownership. The ASF licenses this file
|
regarding copyright ownership. The ASF licenses this file
|
||||||
to you under the Apache License, Version 2.0 (the
|
to you under the Apache License, Version 2.0 (the
|
||||||
"License"); you may not use this file except in compliance
|
"License"); you may not use this file except in compliance
|
||||||
with the License. You may obtain a copy of the License at
|
with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing,
|
Unless required by applicable law or agreed to in writing,
|
||||||
software distributed under the License is distributed on an
|
software distributed under the License is distributed on an
|
||||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
KIND, either express or implied. See the License for the
|
KIND, either express or implied. See the License for the
|
||||||
specific language governing permissions and limitations
|
specific language governing permissions and limitations
|
||||||
under the License.
|
under the License.
|
||||||
-->
|
-->
|
||||||
<cordova>
|
<widget xmlns = "http://www.w3.org/ns/widgets"
|
||||||
<!--
|
id = "io.cordova.helloCordova"
|
||||||
access elements control the Android whitelist.
|
version = "2.0.0">
|
||||||
Domains are assumed blocked unless set otherwise
|
<name>Hello Cordova</name>
|
||||||
-->
|
|
||||||
|
|
||||||
<access origin="http://127.0.0.1*"/> <!-- allow local pages -->
|
<description>
|
||||||
|
A sample Apache Cordova application that responds to the deviceready event.
|
||||||
|
</description>
|
||||||
|
|
||||||
<!-- <access origin="https://example.com" /> allow any secure requests to example.com -->
|
<author href="http://cordova.io" email="dev@cordova.apache.org">
|
||||||
<!-- <access origin="https://example.com" subdomains="true" /> such as above, but including subdomains, such as www -->
|
Apache Cordova Team
|
||||||
<access origin=".*"/>
|
</author>
|
||||||
|
|
||||||
|
<access origin="*"/>
|
||||||
|
|
||||||
<!-- <content src="http://mysite.com/myapp.html" /> for external pages -->
|
<!-- <content src="http://mysite.com/myapp.html" /> for external pages -->
|
||||||
<content src="index.html" />
|
<content src="index.html" />
|
||||||
|
|
||||||
<log level="DEBUG"/>
|
<preference name="loglevel" value="DEBUG" />
|
||||||
<preference name="useBrowserHistory" value="true" />
|
<!--
|
||||||
<preference name="exit-on-suspend" value="false" />
|
<preference name="splashscreen" value="resourceName" />
|
||||||
<plugins>
|
<preference name="backgroundColor" value="0xFFF" />
|
||||||
<plugin name="App" value="org.apache.cordova.App"/>
|
<preference name="loadUrlTimeoutValue" value="20000" />
|
||||||
<plugin name="Geolocation" value="org.apache.cordova.GeoBroker"/>
|
<preference name="InAppBrowserStorageEnabled" value="true" />
|
||||||
<plugin name="Device" value="org.apache.cordova.Device"/>
|
<preference name="disallowOverscroll" value="true" />
|
||||||
<plugin name="Accelerometer" value="org.apache.cordova.AccelListener"/>
|
-->
|
||||||
<plugin name="Compass" value="org.apache.cordova.CompassListener"/>
|
|
||||||
<plugin name="Media" value="org.apache.cordova.AudioHandler"/>
|
|
||||||
<plugin name="Camera" value="org.apache.cordova.CameraLauncher"/>
|
|
||||||
<plugin name="Contacts" value="org.apache.cordova.ContactManager"/>
|
|
||||||
<plugin name="File" value="org.apache.cordova.FileUtils"/>
|
|
||||||
<plugin name="NetworkStatus" value="org.apache.cordova.NetworkManager"/>
|
|
||||||
<plugin name="Notification" value="org.apache.cordova.Notification"/>
|
|
||||||
<plugin name="Storage" value="org.apache.cordova.Storage"/>
|
|
||||||
<plugin name="FileTransfer" value="org.apache.cordova.FileTransfer"/>
|
|
||||||
<plugin name="Capture" value="org.apache.cordova.Capture"/>
|
|
||||||
<plugin name="Battery" value="org.apache.cordova.BatteryListener"/>
|
|
||||||
<plugin name="SplashScreen" value="org.apache.cordova.SplashScreen"/>
|
|
||||||
<plugin name="Echo" value="org.apache.cordova.Echo" />
|
|
||||||
<plugin name="Globalization" value="org.apache.cordova.Globalization"/>
|
|
||||||
<plugin name="InAppBrowser" value="org.apache.cordova.InAppBrowser"/>
|
|
||||||
</plugins>
|
|
||||||
</cordova>
|
|
||||||
|
|
||||||
|
<feature name="App">
|
||||||
|
<param name="android-package" value="org.apache.cordova.App"/>
|
||||||
|
</feature>
|
||||||
|
<feature name="Geolocation">
|
||||||
|
<param name="android-package" value="org.apache.cordova.GeoBroker"/>
|
||||||
|
</feature>
|
||||||
|
<feature name="Device">
|
||||||
|
<param name="android-package" value="org.apache.cordova.Device"/>
|
||||||
|
</feature>
|
||||||
|
<feature name="Accelerometer">
|
||||||
|
<param name="android-package" value="org.apache.cordova.AccelListener"/>
|
||||||
|
</feature>
|
||||||
|
<feature name="Compass">
|
||||||
|
<param name="android-package" value="org.apache.cordova.CompassListener"/>
|
||||||
|
</feature>
|
||||||
|
<feature name="Media">
|
||||||
|
<param name="android-package" value="org.apache.cordova.AudioHandler"/>
|
||||||
|
</feature>
|
||||||
|
<feature name="Camera">
|
||||||
|
<param name="android-package" value="org.apache.cordova.CameraLauncher"/>
|
||||||
|
</feature>
|
||||||
|
<feature name="Contacts">
|
||||||
|
<param name="android-package" value="org.apache.cordova.ContactManager"/>
|
||||||
|
</feature>
|
||||||
|
<feature name="File">
|
||||||
|
<param name="android-package" value="org.apache.cordova.FileUtils"/>
|
||||||
|
</feature>
|
||||||
|
<feature name="NetworkStatus">
|
||||||
|
<param name="android-package" value="org.apache.cordova.NetworkManager"/>
|
||||||
|
</feature>
|
||||||
|
<feature name="Notification">
|
||||||
|
<param name="android-package" value="org.apache.cordova.Notification"/>
|
||||||
|
</feature>
|
||||||
|
<feature name="Storage">
|
||||||
|
<param name="android-package" value="org.apache.cordova.Storage"/>
|
||||||
|
</feature>
|
||||||
|
<feature name="FileTransfer">
|
||||||
|
<param name="android-package" value="org.apache.cordova.FileTransfer"/>
|
||||||
|
</feature>
|
||||||
|
<feature name="Capture">
|
||||||
|
<param name="android-package" value="org.apache.cordova.Capture"/>
|
||||||
|
</feature>
|
||||||
|
<feature name="Battery">
|
||||||
|
<param name="android-package" value="org.apache.cordova.BatteryListener"/>
|
||||||
|
</feature>
|
||||||
|
<feature name="SplashScreen">
|
||||||
|
<param name="android-package" value="org.apache.cordova.SplashScreen"/>
|
||||||
|
</feature>
|
||||||
|
<feature name="Echo">
|
||||||
|
<param name="android-package" value="org.apache.cordova.Echo"/>
|
||||||
|
</feature>
|
||||||
|
<feature name="Globalization">
|
||||||
|
<param name="android-package" value="org.apache.cordova.Globalization"/>
|
||||||
|
</feature>
|
||||||
|
<feature name="InAppBrowser">
|
||||||
|
<param name="android-package" value="org.apache.cordova.InAppBrowser"/>
|
||||||
|
</feature>
|
||||||
|
<!-- Deprecated plugins element. Remove in 3.0 -->
|
||||||
|
<plugins>
|
||||||
|
</plugins>
|
||||||
|
</widget>
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2012 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp;
|
||||||
|
|
||||||
|
import java.net.Proxy;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import javax.net.ssl.HostnameVerifier;
|
||||||
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
|
|
||||||
|
import static com.squareup.okhttp.internal.Util.equal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A specification for a connection to an origin server. For simple connections,
|
||||||
|
* this is the server's hostname and port. If an explicit proxy is requested (or
|
||||||
|
* {@link Proxy#NO_PROXY no proxy} is explicitly requested), this also includes
|
||||||
|
* that proxy information. For secure connections the address also includes the
|
||||||
|
* SSL socket factory and hostname verifier.
|
||||||
|
*
|
||||||
|
* <p>HTTP requests that share the same {@code Address} may also share the same
|
||||||
|
* {@link Connection}.
|
||||||
|
*/
|
||||||
|
public final class Address {
|
||||||
|
final Proxy proxy;
|
||||||
|
final String uriHost;
|
||||||
|
final int uriPort;
|
||||||
|
final SSLSocketFactory sslSocketFactory;
|
||||||
|
final HostnameVerifier hostnameVerifier;
|
||||||
|
|
||||||
|
public Address(String uriHost, int uriPort, SSLSocketFactory sslSocketFactory,
|
||||||
|
HostnameVerifier hostnameVerifier, Proxy proxy) throws UnknownHostException {
|
||||||
|
if (uriHost == null) throw new NullPointerException("uriHost == null");
|
||||||
|
if (uriPort <= 0) throw new IllegalArgumentException("uriPort <= 0: " + uriPort);
|
||||||
|
this.proxy = proxy;
|
||||||
|
this.uriHost = uriHost;
|
||||||
|
this.uriPort = uriPort;
|
||||||
|
this.sslSocketFactory = sslSocketFactory;
|
||||||
|
this.hostnameVerifier = hostnameVerifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the hostname of the origin server. */
|
||||||
|
public String getUriHost() {
|
||||||
|
return uriHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the port of the origin server; typically 80 or 443. Unlike
|
||||||
|
* may {@code getPort()} accessors, this method never returns -1.
|
||||||
|
*/
|
||||||
|
public int getUriPort() {
|
||||||
|
return uriPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the SSL socket factory, or null if this is not an HTTPS
|
||||||
|
* address.
|
||||||
|
*/
|
||||||
|
public SSLSocketFactory getSslSocketFactory() {
|
||||||
|
return sslSocketFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the hostname verifier, or null if this is not an HTTPS
|
||||||
|
* address.
|
||||||
|
*/
|
||||||
|
public HostnameVerifier getHostnameVerifier() {
|
||||||
|
return hostnameVerifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns this address's explicitly-specified HTTP proxy, or null to
|
||||||
|
* delegate to the HTTP client's proxy selector.
|
||||||
|
*/
|
||||||
|
public Proxy getProxy() {
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public boolean equals(Object other) {
|
||||||
|
if (other instanceof Address) {
|
||||||
|
Address that = (Address) other;
|
||||||
|
return equal(this.proxy, that.proxy)
|
||||||
|
&& this.uriHost.equals(that.uriHost)
|
||||||
|
&& this.uriPort == that.uriPort
|
||||||
|
&& equal(this.sslSocketFactory, that.sslSocketFactory)
|
||||||
|
&& equal(this.hostnameVerifier, that.hostnameVerifier);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public int hashCode() {
|
||||||
|
int result = 17;
|
||||||
|
result = 31 * result + uriHost.hashCode();
|
||||||
|
result = 31 * result + uriPort;
|
||||||
|
result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0);
|
||||||
|
result = 31 * result + (hostnameVerifier != null ? hostnameVerifier.hashCode() : 0);
|
||||||
|
result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
/*
|
||||||
|
* 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.squareup.okhttp;
|
||||||
|
|
||||||
|
import com.squareup.okhttp.internal.Platform;
|
||||||
|
import com.squareup.okhttp.internal.http.HttpAuthenticator;
|
||||||
|
import com.squareup.okhttp.internal.http.HttpEngine;
|
||||||
|
import com.squareup.okhttp.internal.http.HttpTransport;
|
||||||
|
import com.squareup.okhttp.internal.http.RawHeaders;
|
||||||
|
import com.squareup.okhttp.internal.http.SpdyTransport;
|
||||||
|
import com.squareup.okhttp.internal.spdy.SpdyConnection;
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.BufferedOutputStream;
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.Proxy;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
|
||||||
|
import static java.net.HttpURLConnection.HTTP_OK;
|
||||||
|
import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection,
|
||||||
|
* which may be used for multiple HTTP request/response exchanges. Connections
|
||||||
|
* may be direct to the origin server or via a proxy.
|
||||||
|
*
|
||||||
|
* <p>Typically instances of this class are created, connected and exercised
|
||||||
|
* automatically by the HTTP client. Applications may use this class to monitor
|
||||||
|
* HTTP connections as members of a {@link ConnectionPool connection pool}.
|
||||||
|
*
|
||||||
|
* <p>Do not confuse this class with the misnamed {@code HttpURLConnection},
|
||||||
|
* which isn't so much a connection as a single request/response exchange.
|
||||||
|
*
|
||||||
|
* <h3>Modern TLS</h3>
|
||||||
|
* There are tradeoffs when selecting which options to include when negotiating
|
||||||
|
* a secure connection to a remote host. Newer TLS options are quite useful:
|
||||||
|
* <ul>
|
||||||
|
* <li>Server Name Indication (SNI) enables one IP address to negotiate secure
|
||||||
|
* connections for multiple domain names.
|
||||||
|
* <li>Next Protocol Negotiation (NPN) enables the HTTPS port (443) to be used
|
||||||
|
* for both HTTP and SPDY transports.
|
||||||
|
* </ul>
|
||||||
|
* Unfortunately, older HTTPS servers refuse to connect when such options are
|
||||||
|
* presented. Rather than avoiding these options entirely, this class allows a
|
||||||
|
* connection to be attempted with modern options and then retried without them
|
||||||
|
* should the attempt fail.
|
||||||
|
*/
|
||||||
|
public final class Connection implements Closeable {
|
||||||
|
private static final byte[] NPN_PROTOCOLS = new byte[] {
|
||||||
|
6, 's', 'p', 'd', 'y', '/', '3',
|
||||||
|
8, 'h', 't', 't', 'p', '/', '1', '.', '1'
|
||||||
|
};
|
||||||
|
private static final byte[] SPDY3 = new byte[] {
|
||||||
|
's', 'p', 'd', 'y', '/', '3'
|
||||||
|
};
|
||||||
|
private static final byte[] HTTP_11 = new byte[] {
|
||||||
|
'h', 't', 't', 'p', '/', '1', '.', '1'
|
||||||
|
};
|
||||||
|
|
||||||
|
private final Route route;
|
||||||
|
|
||||||
|
private Socket socket;
|
||||||
|
private InputStream in;
|
||||||
|
private OutputStream out;
|
||||||
|
private boolean connected = false;
|
||||||
|
private SpdyConnection spdyConnection;
|
||||||
|
private int httpMinorVersion = 1; // Assume HTTP/1.1
|
||||||
|
private long idleStartTimeNs;
|
||||||
|
|
||||||
|
public Connection(Route route) {
|
||||||
|
this.route = route;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest)
|
||||||
|
throws IOException {
|
||||||
|
if (connected) {
|
||||||
|
throw new IllegalStateException("already connected");
|
||||||
|
}
|
||||||
|
connected = true;
|
||||||
|
socket = (route.proxy.type() != Proxy.Type.HTTP) ? new Socket(route.proxy) : new Socket();
|
||||||
|
socket.connect(route.inetSocketAddress, connectTimeout);
|
||||||
|
socket.setSoTimeout(readTimeout);
|
||||||
|
in = socket.getInputStream();
|
||||||
|
out = socket.getOutputStream();
|
||||||
|
|
||||||
|
if (route.address.sslSocketFactory != null) {
|
||||||
|
upgradeToTls(tunnelRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use MTU-sized buffers to send fewer packets.
|
||||||
|
int mtu = Platform.get().getMtu(socket);
|
||||||
|
in = new BufferedInputStream(in, mtu);
|
||||||
|
out = new BufferedOutputStream(out, mtu);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an {@code SSLSocket} and perform the TLS handshake and certificate
|
||||||
|
* validation.
|
||||||
|
*/
|
||||||
|
private void upgradeToTls(TunnelRequest tunnelRequest) throws IOException {
|
||||||
|
Platform platform = Platform.get();
|
||||||
|
|
||||||
|
// Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
|
||||||
|
if (requiresTunnel()) {
|
||||||
|
makeTunnel(tunnelRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the wrapper over connected socket.
|
||||||
|
socket = route.address.sslSocketFactory
|
||||||
|
.createSocket(socket, route.address.uriHost, route.address.uriPort, true /* autoClose */);
|
||||||
|
SSLSocket sslSocket = (SSLSocket) socket;
|
||||||
|
if (route.modernTls) {
|
||||||
|
platform.enableTlsExtensions(sslSocket, route.address.uriHost);
|
||||||
|
} else {
|
||||||
|
platform.supportTlsIntolerantServer(sslSocket);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.modernTls) {
|
||||||
|
platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force handshake. This can throw!
|
||||||
|
sslSocket.startHandshake();
|
||||||
|
|
||||||
|
// Verify that the socket's certificates are acceptable for the target host.
|
||||||
|
if (!route.address.hostnameVerifier.verify(route.address.uriHost, sslSocket.getSession())) {
|
||||||
|
throw new IOException("Hostname '" + route.address.uriHost + "' was not verified");
|
||||||
|
}
|
||||||
|
|
||||||
|
out = sslSocket.getOutputStream();
|
||||||
|
in = sslSocket.getInputStream();
|
||||||
|
|
||||||
|
byte[] selectedProtocol;
|
||||||
|
if (route.modernTls
|
||||||
|
&& (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
|
||||||
|
if (Arrays.equals(selectedProtocol, SPDY3)) {
|
||||||
|
sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream.
|
||||||
|
spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, in, out)
|
||||||
|
.build();
|
||||||
|
} else if (!Arrays.equals(selectedProtocol, HTTP_11)) {
|
||||||
|
throw new IOException(
|
||||||
|
"Unexpected NPN transport " + new String(selectedProtocol, "ISO-8859-1"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if {@link #connect} has been attempted on this connection. */
|
||||||
|
public boolean isConnected() {
|
||||||
|
return connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void close() throws IOException {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the route used by this connection. */
|
||||||
|
public Route getRoute() {
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the socket that this connection uses, or null if the connection
|
||||||
|
* is not currently connected.
|
||||||
|
*/
|
||||||
|
public Socket getSocket() {
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if this connection is alive. */
|
||||||
|
public boolean isAlive() {
|
||||||
|
return !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetIdleStartTime() {
|
||||||
|
if (spdyConnection != null) {
|
||||||
|
throw new IllegalStateException("spdyConnection != null");
|
||||||
|
}
|
||||||
|
this.idleStartTimeNs = System.nanoTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if this connection is idle. */
|
||||||
|
public boolean isIdle() {
|
||||||
|
return spdyConnection == null || spdyConnection.isIdle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this connection has been idle for longer than
|
||||||
|
* {@code keepAliveDurationNs}.
|
||||||
|
*/
|
||||||
|
public boolean isExpired(long keepAliveDurationNs) {
|
||||||
|
return isIdle() && System.nanoTime() - getIdleStartTimeNs() > keepAliveDurationNs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the time in ns when this connection became idle. Undefined if
|
||||||
|
* this connection is not idle.
|
||||||
|
*/
|
||||||
|
public long getIdleStartTimeNs() {
|
||||||
|
return spdyConnection == null ? idleStartTimeNs : spdyConnection.getIdleStartTimeNs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the transport appropriate for this connection. */
|
||||||
|
public Object newTransport(HttpEngine httpEngine) throws IOException {
|
||||||
|
return (spdyConnection != null) ? new SpdyTransport(httpEngine, spdyConnection)
|
||||||
|
: new HttpTransport(httpEngine, out, in);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this is a SPDY connection. Such connections can be used
|
||||||
|
* in multiple HTTP requests simultaneously.
|
||||||
|
*/
|
||||||
|
public boolean isSpdy() {
|
||||||
|
return spdyConnection != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpdyConnection getSpdyConnection() {
|
||||||
|
return spdyConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the minor HTTP version that should be used for future requests on
|
||||||
|
* this connection. Either 0 for HTTP/1.0, or 1 for HTTP/1.1. The default
|
||||||
|
* value is 1 for new connections.
|
||||||
|
*/
|
||||||
|
public int getHttpMinorVersion() {
|
||||||
|
return httpMinorVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHttpMinorVersion(int httpMinorVersion) {
|
||||||
|
this.httpMinorVersion = httpMinorVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the HTTP connection needs to tunnel one protocol over
|
||||||
|
* another, such as when using HTTPS through an HTTP proxy. When doing so,
|
||||||
|
* we must avoid buffering bytes intended for the higher-level protocol.
|
||||||
|
*/
|
||||||
|
public boolean requiresTunnel() {
|
||||||
|
return route.address.sslSocketFactory != null && route.proxy.type() == Proxy.Type.HTTP;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To make an HTTPS connection over an HTTP proxy, send an unencrypted
|
||||||
|
* CONNECT request to create the proxy connection. This may need to be
|
||||||
|
* retried if the proxy requires authorization.
|
||||||
|
*/
|
||||||
|
private void makeTunnel(TunnelRequest tunnelRequest) throws IOException {
|
||||||
|
RawHeaders requestHeaders = tunnelRequest.getRequestHeaders();
|
||||||
|
while (true) {
|
||||||
|
out.write(requestHeaders.toBytes());
|
||||||
|
RawHeaders responseHeaders = RawHeaders.fromBytes(in);
|
||||||
|
|
||||||
|
switch (responseHeaders.getResponseCode()) {
|
||||||
|
case HTTP_OK:
|
||||||
|
return;
|
||||||
|
case HTTP_PROXY_AUTH:
|
||||||
|
requestHeaders = new RawHeaders(requestHeaders);
|
||||||
|
URL url = new URL("https", tunnelRequest.host, tunnelRequest.port, "/");
|
||||||
|
boolean credentialsFound = HttpAuthenticator.processAuthHeader(HTTP_PROXY_AUTH,
|
||||||
|
responseHeaders, requestHeaders, route.proxy, url);
|
||||||
|
if (credentialsFound) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
throw new IOException("Failed to authenticate with proxy");
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new IOException(
|
||||||
|
"Unexpected response code for CONNECT: " + responseHeaders.getResponseCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
/*
|
||||||
|
* 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.squareup.okhttp;
|
||||||
|
|
||||||
|
import com.squareup.okhttp.internal.Platform;
|
||||||
|
import com.squareup.okhttp.internal.Util;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ListIterator;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages reuse of HTTP and SPDY connections for reduced network latency. HTTP
|
||||||
|
* requests that share the same {@link com.squareup.okhttp.Address} may share a
|
||||||
|
* {@link com.squareup.okhttp.Connection}. This class implements the policy of
|
||||||
|
* which connections to keep open for future use.
|
||||||
|
*
|
||||||
|
* <p>The {@link #getDefault() system-wide default} uses system properties for
|
||||||
|
* tuning parameters:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code http.keepAlive} true if HTTP and SPDY connections should be
|
||||||
|
* pooled at all. Default is true.
|
||||||
|
* <li>{@code http.maxConnections} maximum number of idle connections to
|
||||||
|
* each to keep in the pool. Default is 5.
|
||||||
|
* <li>{@code http.keepAliveDuration} Time in milliseconds to keep the
|
||||||
|
* connection alive in the pool before closing it. Default is 5 minutes.
|
||||||
|
* This property isn't used by {@code HttpURLConnection}.
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>The default instance <i>doesn't</i> adjust its configuration as system
|
||||||
|
* properties are changed. This assumes that the applications that set these
|
||||||
|
* parameters do so before making HTTP connections, and that this class is
|
||||||
|
* initialized lazily.
|
||||||
|
*/
|
||||||
|
public class ConnectionPool {
|
||||||
|
private static final int MAX_CONNECTIONS_TO_CLEANUP = 2;
|
||||||
|
private static final long DEFAULT_KEEP_ALIVE_DURATION_MS = 5 * 60 * 1000; // 5 min
|
||||||
|
|
||||||
|
private static final ConnectionPool systemDefault;
|
||||||
|
|
||||||
|
static {
|
||||||
|
String keepAlive = System.getProperty("http.keepAlive");
|
||||||
|
String keepAliveDuration = System.getProperty("http.keepAliveDuration");
|
||||||
|
String maxIdleConnections = System.getProperty("http.maxConnections");
|
||||||
|
long keepAliveDurationMs = keepAliveDuration != null ? Long.parseLong(keepAliveDuration)
|
||||||
|
: DEFAULT_KEEP_ALIVE_DURATION_MS;
|
||||||
|
if (keepAlive != null && !Boolean.parseBoolean(keepAlive)) {
|
||||||
|
systemDefault = new ConnectionPool(0, keepAliveDurationMs);
|
||||||
|
} else if (maxIdleConnections != null) {
|
||||||
|
systemDefault = new ConnectionPool(Integer.parseInt(maxIdleConnections), keepAliveDurationMs);
|
||||||
|
} else {
|
||||||
|
systemDefault = new ConnectionPool(5, keepAliveDurationMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The maximum number of idle connections for each address. */
|
||||||
|
private final int maxIdleConnections;
|
||||||
|
private final long keepAliveDurationNs;
|
||||||
|
|
||||||
|
private final LinkedList<Connection> connections = new LinkedList<Connection>();
|
||||||
|
|
||||||
|
/** We use a single background thread to cleanup expired connections. */
|
||||||
|
private final ExecutorService executorService =
|
||||||
|
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
|
||||||
|
private final Callable<Void> connectionsCleanupCallable = new Callable<Void>() {
|
||||||
|
@Override public Void call() throws Exception {
|
||||||
|
List<Connection> expiredConnections = new ArrayList<Connection>(MAX_CONNECTIONS_TO_CLEANUP);
|
||||||
|
int idleConnectionCount = 0;
|
||||||
|
synchronized (ConnectionPool.this) {
|
||||||
|
for (ListIterator<Connection> i = connections.listIterator(connections.size());
|
||||||
|
i.hasPrevious(); ) {
|
||||||
|
Connection connection = i.previous();
|
||||||
|
if (!connection.isAlive() || connection.isExpired(keepAliveDurationNs)) {
|
||||||
|
i.remove();
|
||||||
|
expiredConnections.add(connection);
|
||||||
|
if (expiredConnections.size() == MAX_CONNECTIONS_TO_CLEANUP) break;
|
||||||
|
} else if (connection.isIdle()) {
|
||||||
|
idleConnectionCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ListIterator<Connection> i = connections.listIterator(connections.size());
|
||||||
|
i.hasPrevious() && idleConnectionCount > maxIdleConnections; ) {
|
||||||
|
Connection connection = i.previous();
|
||||||
|
if (connection.isIdle()) {
|
||||||
|
expiredConnections.add(connection);
|
||||||
|
i.remove();
|
||||||
|
--idleConnectionCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Connection expiredConnection : expiredConnections) {
|
||||||
|
Util.closeQuietly(expiredConnection);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public ConnectionPool(int maxIdleConnections, long keepAliveDurationMs) {
|
||||||
|
this.maxIdleConnections = maxIdleConnections;
|
||||||
|
this.keepAliveDurationNs = keepAliveDurationMs * 1000 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a snapshot of the connections in this pool, ordered from newest to
|
||||||
|
* oldest. Waits for the cleanup callable to run if it is currently scheduled.
|
||||||
|
*/
|
||||||
|
List<Connection> getConnections() {
|
||||||
|
waitForCleanupCallableToRun();
|
||||||
|
synchronized (this) {
|
||||||
|
return new ArrayList<Connection>(connections);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocks until the executor service has processed all currently enqueued
|
||||||
|
* jobs.
|
||||||
|
*/
|
||||||
|
private void waitForCleanupCallableToRun() {
|
||||||
|
try {
|
||||||
|
executorService.submit(new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
}
|
||||||
|
}).get();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ConnectionPool getDefault() {
|
||||||
|
return systemDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns total number of connections in the pool. */
|
||||||
|
public synchronized int getConnectionCount() {
|
||||||
|
return connections.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns total number of spdy connections in the pool. */
|
||||||
|
public synchronized int getSpdyConnectionCount() {
|
||||||
|
int total = 0;
|
||||||
|
for (Connection connection : connections) {
|
||||||
|
if (connection.isSpdy()) total++;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns total number of http connections in the pool. */
|
||||||
|
public synchronized int getHttpConnectionCount() {
|
||||||
|
int total = 0;
|
||||||
|
for (Connection connection : connections) {
|
||||||
|
if (!connection.isSpdy()) total++;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a recycled connection to {@code address}, or null if no such connection exists. */
|
||||||
|
public synchronized Connection get(Address address) {
|
||||||
|
Connection foundConnection = null;
|
||||||
|
for (ListIterator<Connection> i = connections.listIterator(connections.size());
|
||||||
|
i.hasPrevious(); ) {
|
||||||
|
Connection connection = i.previous();
|
||||||
|
if (!connection.getRoute().getAddress().equals(address)
|
||||||
|
|| !connection.isAlive()
|
||||||
|
|| System.nanoTime() - connection.getIdleStartTimeNs() >= keepAliveDurationNs) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
i.remove();
|
||||||
|
if (!connection.isSpdy()) {
|
||||||
|
try {
|
||||||
|
Platform.get().tagSocket(connection.getSocket());
|
||||||
|
} catch (SocketException e) {
|
||||||
|
Util.closeQuietly(connection);
|
||||||
|
// When unable to tag, skip recycling and close
|
||||||
|
Platform.get().logW("Unable to tagSocket(): " + e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foundConnection = connection;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundConnection != null && foundConnection.isSpdy()) {
|
||||||
|
connections.addFirst(foundConnection); // Add it back after iteration.
|
||||||
|
}
|
||||||
|
|
||||||
|
executorService.submit(connectionsCleanupCallable);
|
||||||
|
return foundConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gives {@code connection} to the pool. The pool may store the connection,
|
||||||
|
* or close it, as its policy describes.
|
||||||
|
*
|
||||||
|
* <p>It is an error to use {@code connection} after calling this method.
|
||||||
|
*/
|
||||||
|
public void recycle(Connection connection) {
|
||||||
|
executorService.submit(connectionsCleanupCallable);
|
||||||
|
|
||||||
|
if (connection.isSpdy()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connection.isAlive()) {
|
||||||
|
Util.closeQuietly(connection);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Platform.get().untagSocket(connection.getSocket());
|
||||||
|
} catch (SocketException e) {
|
||||||
|
// When unable to remove tagging, skip recycling and close.
|
||||||
|
Platform.get().logW("Unable to untagSocket(): " + e);
|
||||||
|
Util.closeQuietly(connection);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (this) {
|
||||||
|
connections.addFirst(connection);
|
||||||
|
connection.resetIdleStartTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shares the SPDY connection with the pool. Callers to this method may
|
||||||
|
* continue to use {@code connection}.
|
||||||
|
*/
|
||||||
|
public void maybeShare(Connection connection) {
|
||||||
|
executorService.submit(connectionsCleanupCallable);
|
||||||
|
if (!connection.isSpdy()) {
|
||||||
|
// Only SPDY connections are sharable.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (connection.isAlive()) {
|
||||||
|
synchronized (this) {
|
||||||
|
connections.addFirst(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Close and remove all connections in the pool. */
|
||||||
|
public void evictAll() {
|
||||||
|
List<Connection> connections;
|
||||||
|
synchronized (this) {
|
||||||
|
connections = new ArrayList<Connection>(this.connections);
|
||||||
|
this.connections.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Connection connection : connections) {
|
||||||
|
Util.closeQuietly(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,693 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2010 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp;
|
||||||
|
|
||||||
|
import com.squareup.okhttp.internal.Base64;
|
||||||
|
import com.squareup.okhttp.internal.DiskLruCache;
|
||||||
|
import com.squareup.okhttp.internal.StrictLineReader;
|
||||||
|
import com.squareup.okhttp.internal.Util;
|
||||||
|
import com.squareup.okhttp.internal.http.HttpEngine;
|
||||||
|
import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
|
||||||
|
import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
|
||||||
|
import com.squareup.okhttp.internal.http.OkResponseCache;
|
||||||
|
import com.squareup.okhttp.internal.http.RawHeaders;
|
||||||
|
import com.squareup.okhttp.internal.http.ResponseHeaders;
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FilterInputStream;
|
||||||
|
import java.io.FilterOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.net.CacheRequest;
|
||||||
|
import java.net.CacheResponse;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.ResponseCache;
|
||||||
|
import java.net.SecureCacheResponse;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
|
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||||
|
|
||||||
|
import static com.squareup.okhttp.internal.Util.US_ASCII;
|
||||||
|
import static com.squareup.okhttp.internal.Util.UTF_8;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caches HTTP and HTTPS responses to the filesystem so they may be reused,
|
||||||
|
* saving time and bandwidth.
|
||||||
|
*
|
||||||
|
* <h3>Cache Optimization</h3>
|
||||||
|
* To measure cache effectiveness, this class tracks three statistics:
|
||||||
|
* <ul>
|
||||||
|
* <li><strong>{@link #getRequestCount() Request Count:}</strong> the number
|
||||||
|
* of HTTP requests issued since this cache was created.
|
||||||
|
* <li><strong>{@link #getNetworkCount() Network Count:}</strong> the
|
||||||
|
* number of those requests that required network use.
|
||||||
|
* <li><strong>{@link #getHitCount() Hit Count:}</strong> the number of
|
||||||
|
* those requests whose responses were served by the cache.
|
||||||
|
* </ul>
|
||||||
|
* Sometimes a request will result in a conditional cache hit. If the cache
|
||||||
|
* contains a stale copy of the response, the client will issue a conditional
|
||||||
|
* {@code GET}. The server will then send either the updated response if it has
|
||||||
|
* changed, or a short 'not modified' response if the client's copy is still
|
||||||
|
* valid. Such responses increment both the network count and hit count.
|
||||||
|
*
|
||||||
|
* <p>The best way to improve the cache hit rate is by configuring the web
|
||||||
|
* server to return cacheable responses. Although this client honors all <a
|
||||||
|
* href="http://www.ietf.org/rfc/rfc2616.txt">HTTP/1.1 (RFC 2068)</a> cache
|
||||||
|
* headers, it doesn't cache partial responses.
|
||||||
|
*
|
||||||
|
* <h3>Force a Network Response</h3>
|
||||||
|
* In some situations, such as after a user clicks a 'refresh' button, it may be
|
||||||
|
* necessary to skip the cache, and fetch data directly from the server. To force
|
||||||
|
* a full refresh, add the {@code no-cache} directive: <pre> {@code
|
||||||
|
* connection.addRequestProperty("Cache-Control", "no-cache");
|
||||||
|
* }</pre>
|
||||||
|
* If it is only necessary to force a cached response to be validated by the
|
||||||
|
* server, use the more efficient {@code max-age=0} instead: <pre> {@code
|
||||||
|
* connection.addRequestProperty("Cache-Control", "max-age=0");
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <h3>Force a Cache Response</h3>
|
||||||
|
* Sometimes you'll want to show resources if they are available immediately,
|
||||||
|
* but not otherwise. This can be used so your application can show
|
||||||
|
* <i>something</i> while waiting for the latest data to be downloaded. To
|
||||||
|
* restrict a request to locally-cached resources, add the {@code
|
||||||
|
* only-if-cached} directive: <pre> {@code
|
||||||
|
* try {
|
||||||
|
* connection.addRequestProperty("Cache-Control", "only-if-cached");
|
||||||
|
* InputStream cached = connection.getInputStream();
|
||||||
|
* // the resource was cached! show it
|
||||||
|
* } catch (FileNotFoundException e) {
|
||||||
|
* // the resource was not cached
|
||||||
|
* }
|
||||||
|
* }</pre>
|
||||||
|
* This technique works even better in situations where a stale response is
|
||||||
|
* better than no response. To permit stale cached responses, use the {@code
|
||||||
|
* max-stale} directive with the maximum staleness in seconds: <pre> {@code
|
||||||
|
* int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
|
||||||
|
* connection.addRequestProperty("Cache-Control", "max-stale=" + maxStale);
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
public final class HttpResponseCache extends ResponseCache {
|
||||||
|
private static final char[] DIGITS =
|
||||||
|
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
|
||||||
|
|
||||||
|
// TODO: add APIs to iterate the cache?
|
||||||
|
private static final int VERSION = 201105;
|
||||||
|
private static final int ENTRY_METADATA = 0;
|
||||||
|
private static final int ENTRY_BODY = 1;
|
||||||
|
private static final int ENTRY_COUNT = 2;
|
||||||
|
|
||||||
|
private final DiskLruCache cache;
|
||||||
|
|
||||||
|
/* read and write statistics, all guarded by 'this' */
|
||||||
|
private int writeSuccessCount;
|
||||||
|
private int writeAbortCount;
|
||||||
|
private int networkCount;
|
||||||
|
private int hitCount;
|
||||||
|
private int requestCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Although this class only exposes the limited ResponseCache API, it
|
||||||
|
* implements the full OkResponseCache interface. This field is used as a
|
||||||
|
* package private handle to the complete implementation. It delegates to
|
||||||
|
* public and private members of this type.
|
||||||
|
*/
|
||||||
|
final OkResponseCache okResponseCache = new OkResponseCache() {
|
||||||
|
@Override public CacheResponse get(URI uri, String requestMethod,
|
||||||
|
Map<String, List<String>> requestHeaders) throws IOException {
|
||||||
|
return HttpResponseCache.this.get(uri, requestMethod, requestHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
|
||||||
|
return HttpResponseCache.this.put(uri, connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void update(
|
||||||
|
CacheResponse conditionalCacheHit, HttpURLConnection connection) throws IOException {
|
||||||
|
HttpResponseCache.this.update(conditionalCacheHit, connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void trackConditionalCacheHit() {
|
||||||
|
HttpResponseCache.this.trackConditionalCacheHit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void trackResponse(ResponseSource source) {
|
||||||
|
HttpResponseCache.this.trackResponse(source);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public HttpResponseCache(File directory, long maxSize) throws IOException {
|
||||||
|
cache = DiskLruCache.open(directory, VERSION, ENTRY_COUNT, maxSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String uriToKey(URI uri) {
|
||||||
|
try {
|
||||||
|
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
|
||||||
|
byte[] md5bytes = messageDigest.digest(uri.toString().getBytes("UTF-8"));
|
||||||
|
return bytesToHexString(md5bytes);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String bytesToHexString(byte[] bytes) {
|
||||||
|
char[] digits = DIGITS;
|
||||||
|
char[] buf = new char[bytes.length * 2];
|
||||||
|
int c = 0;
|
||||||
|
for (byte b : bytes) {
|
||||||
|
buf[c++] = digits[(b >> 4) & 0xf];
|
||||||
|
buf[c++] = digits[b & 0xf];
|
||||||
|
}
|
||||||
|
return new String(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public CacheResponse get(URI uri, String requestMethod,
|
||||||
|
Map<String, List<String>> requestHeaders) {
|
||||||
|
String key = uriToKey(uri);
|
||||||
|
DiskLruCache.Snapshot snapshot;
|
||||||
|
Entry entry;
|
||||||
|
try {
|
||||||
|
snapshot = cache.get(key);
|
||||||
|
if (snapshot == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
entry = new Entry(snapshot.getInputStream(ENTRY_METADATA));
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Give up because the cache cannot be read.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entry.matches(uri, requestMethod, requestHeaders)) {
|
||||||
|
snapshot.close();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry.isHttps() ? new EntrySecureCacheResponse(entry, snapshot)
|
||||||
|
: new EntryCacheResponse(entry, snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
|
||||||
|
if (!(urlConnection instanceof HttpURLConnection)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpURLConnection httpConnection = (HttpURLConnection) urlConnection;
|
||||||
|
String requestMethod = httpConnection.getRequestMethod();
|
||||||
|
String key = uriToKey(uri);
|
||||||
|
|
||||||
|
if (requestMethod.equals("POST") || requestMethod.equals("PUT") || requestMethod.equals(
|
||||||
|
"DELETE")) {
|
||||||
|
try {
|
||||||
|
cache.remove(key);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
// The cache cannot be written.
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} else if (!requestMethod.equals("GET")) {
|
||||||
|
// Don't cache non-GET responses. We're technically allowed to cache
|
||||||
|
// HEAD requests and some POST requests, but the complexity of doing
|
||||||
|
// so is high and the benefit is low.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpEngine httpEngine = getHttpEngine(httpConnection);
|
||||||
|
if (httpEngine == null) {
|
||||||
|
// Don't cache unless the HTTP implementation is ours.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResponseHeaders response = httpEngine.getResponseHeaders();
|
||||||
|
if (response.hasVaryAll()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
RawHeaders varyHeaders =
|
||||||
|
httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields());
|
||||||
|
Entry entry = new Entry(uri, varyHeaders, httpConnection);
|
||||||
|
DiskLruCache.Editor editor = null;
|
||||||
|
try {
|
||||||
|
editor = cache.edit(key);
|
||||||
|
if (editor == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
entry.writeTo(editor);
|
||||||
|
return new CacheRequestImpl(editor);
|
||||||
|
} catch (IOException e) {
|
||||||
|
abortQuietly(editor);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
|
||||||
|
throws IOException {
|
||||||
|
HttpEngine httpEngine = getHttpEngine(httpConnection);
|
||||||
|
URI uri = httpEngine.getUri();
|
||||||
|
ResponseHeaders response = httpEngine.getResponseHeaders();
|
||||||
|
RawHeaders varyHeaders =
|
||||||
|
httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields());
|
||||||
|
Entry entry = new Entry(uri, varyHeaders, httpConnection);
|
||||||
|
DiskLruCache.Snapshot snapshot = (conditionalCacheHit instanceof EntryCacheResponse)
|
||||||
|
? ((EntryCacheResponse) conditionalCacheHit).snapshot
|
||||||
|
: ((EntrySecureCacheResponse) conditionalCacheHit).snapshot;
|
||||||
|
DiskLruCache.Editor editor = null;
|
||||||
|
try {
|
||||||
|
editor = snapshot.edit(); // returns null if snapshot is not current
|
||||||
|
if (editor != null) {
|
||||||
|
entry.writeTo(editor);
|
||||||
|
editor.commit();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
abortQuietly(editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void abortQuietly(DiskLruCache.Editor editor) {
|
||||||
|
// Give up because the cache cannot be written.
|
||||||
|
try {
|
||||||
|
if (editor != null) {
|
||||||
|
editor.abort();
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpEngine getHttpEngine(URLConnection httpConnection) {
|
||||||
|
if (httpConnection instanceof HttpURLConnectionImpl) {
|
||||||
|
return ((HttpURLConnectionImpl) httpConnection).getHttpEngine();
|
||||||
|
} else if (httpConnection instanceof HttpsURLConnectionImpl) {
|
||||||
|
return ((HttpsURLConnectionImpl) httpConnection).getHttpEngine();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the cache and deletes all of its stored values. This will delete
|
||||||
|
* all files in the cache directory including files that weren't created by
|
||||||
|
* the cache.
|
||||||
|
*/
|
||||||
|
public void delete() throws IOException {
|
||||||
|
cache.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int getWriteAbortCount() {
|
||||||
|
return writeAbortCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int getWriteSuccessCount() {
|
||||||
|
return writeSuccessCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void trackResponse(ResponseSource source) {
|
||||||
|
requestCount++;
|
||||||
|
|
||||||
|
switch (source) {
|
||||||
|
case CACHE:
|
||||||
|
hitCount++;
|
||||||
|
break;
|
||||||
|
case CONDITIONAL_CACHE:
|
||||||
|
case NETWORK:
|
||||||
|
networkCount++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void trackConditionalCacheHit() {
|
||||||
|
hitCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int getNetworkCount() {
|
||||||
|
return networkCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int getHitCount() {
|
||||||
|
return hitCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int getRequestCount() {
|
||||||
|
return requestCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class CacheRequestImpl extends CacheRequest {
|
||||||
|
private final DiskLruCache.Editor editor;
|
||||||
|
private OutputStream cacheOut;
|
||||||
|
private boolean done;
|
||||||
|
private OutputStream body;
|
||||||
|
|
||||||
|
public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException {
|
||||||
|
this.editor = editor;
|
||||||
|
this.cacheOut = editor.newOutputStream(ENTRY_BODY);
|
||||||
|
this.body = new FilterOutputStream(cacheOut) {
|
||||||
|
@Override public void close() throws IOException {
|
||||||
|
synchronized (HttpResponseCache.this) {
|
||||||
|
if (done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
done = true;
|
||||||
|
writeSuccessCount++;
|
||||||
|
}
|
||||||
|
super.close();
|
||||||
|
editor.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte[] buffer, int offset, int length) throws IOException {
|
||||||
|
// Since we don't override "write(int oneByte)", we can write directly to "out"
|
||||||
|
// and avoid the inefficient implementation from the FilterOutputStream.
|
||||||
|
out.write(buffer, offset, length);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void abort() {
|
||||||
|
synchronized (HttpResponseCache.this) {
|
||||||
|
if (done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
done = true;
|
||||||
|
writeAbortCount++;
|
||||||
|
}
|
||||||
|
Util.closeQuietly(cacheOut);
|
||||||
|
try {
|
||||||
|
editor.abort();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public OutputStream getBody() throws IOException {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Entry {
|
||||||
|
private final String uri;
|
||||||
|
private final RawHeaders varyHeaders;
|
||||||
|
private final String requestMethod;
|
||||||
|
private final RawHeaders responseHeaders;
|
||||||
|
private final String cipherSuite;
|
||||||
|
private final Certificate[] peerCertificates;
|
||||||
|
private final Certificate[] localCertificates;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an entry from an input stream. A typical entry looks like this:
|
||||||
|
* <pre>{@code
|
||||||
|
* http://google.com/foo
|
||||||
|
* GET
|
||||||
|
* 2
|
||||||
|
* Accept-Language: fr-CA
|
||||||
|
* Accept-Charset: UTF-8
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* 3
|
||||||
|
* Content-Type: image/png
|
||||||
|
* Content-Length: 100
|
||||||
|
* Cache-Control: max-age=600
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>A typical HTTPS file looks like this:
|
||||||
|
* <pre>{@code
|
||||||
|
* https://google.com/foo
|
||||||
|
* GET
|
||||||
|
* 2
|
||||||
|
* Accept-Language: fr-CA
|
||||||
|
* Accept-Charset: UTF-8
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* 3
|
||||||
|
* Content-Type: image/png
|
||||||
|
* Content-Length: 100
|
||||||
|
* Cache-Control: max-age=600
|
||||||
|
*
|
||||||
|
* AES_256_WITH_MD5
|
||||||
|
* 2
|
||||||
|
* base64-encoded peerCertificate[0]
|
||||||
|
* base64-encoded peerCertificate[1]
|
||||||
|
* -1
|
||||||
|
* }</pre>
|
||||||
|
* The file is newline separated. The first two lines are the URL and
|
||||||
|
* the request method. Next is the number of HTTP Vary request header
|
||||||
|
* lines, followed by those lines.
|
||||||
|
*
|
||||||
|
* <p>Next is the response status line, followed by the number of HTTP
|
||||||
|
* response header lines, followed by those lines.
|
||||||
|
*
|
||||||
|
* <p>HTTPS responses also contain SSL session information. This begins
|
||||||
|
* with a blank line, and then a line containing the cipher suite. Next
|
||||||
|
* is the length of the peer certificate chain. These certificates are
|
||||||
|
* base64-encoded and appear each on their own line. The next line
|
||||||
|
* contains the length of the local certificate chain. These
|
||||||
|
* certificates are also base64-encoded and appear each on their own
|
||||||
|
* line. A length of -1 is used to encode a null array.
|
||||||
|
*/
|
||||||
|
public Entry(InputStream in) throws IOException {
|
||||||
|
try {
|
||||||
|
StrictLineReader reader = new StrictLineReader(in, US_ASCII);
|
||||||
|
uri = reader.readLine();
|
||||||
|
requestMethod = reader.readLine();
|
||||||
|
varyHeaders = new RawHeaders();
|
||||||
|
int varyRequestHeaderLineCount = reader.readInt();
|
||||||
|
for (int i = 0; i < varyRequestHeaderLineCount; i++) {
|
||||||
|
varyHeaders.addLine(reader.readLine());
|
||||||
|
}
|
||||||
|
|
||||||
|
responseHeaders = new RawHeaders();
|
||||||
|
responseHeaders.setStatusLine(reader.readLine());
|
||||||
|
int responseHeaderLineCount = reader.readInt();
|
||||||
|
for (int i = 0; i < responseHeaderLineCount; i++) {
|
||||||
|
responseHeaders.addLine(reader.readLine());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHttps()) {
|
||||||
|
String blank = reader.readLine();
|
||||||
|
if (blank.length() > 0) {
|
||||||
|
throw new IOException("expected \"\" but was \"" + blank + "\"");
|
||||||
|
}
|
||||||
|
cipherSuite = reader.readLine();
|
||||||
|
peerCertificates = readCertArray(reader);
|
||||||
|
localCertificates = readCertArray(reader);
|
||||||
|
} else {
|
||||||
|
cipherSuite = null;
|
||||||
|
peerCertificates = null;
|
||||||
|
localCertificates = null;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
in.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Entry(URI uri, RawHeaders varyHeaders, HttpURLConnection httpConnection)
|
||||||
|
throws IOException {
|
||||||
|
this.uri = uri.toString();
|
||||||
|
this.varyHeaders = varyHeaders;
|
||||||
|
this.requestMethod = httpConnection.getRequestMethod();
|
||||||
|
this.responseHeaders = RawHeaders.fromMultimap(httpConnection.getHeaderFields(), true);
|
||||||
|
|
||||||
|
if (isHttps()) {
|
||||||
|
HttpsURLConnection httpsConnection = (HttpsURLConnection) httpConnection;
|
||||||
|
cipherSuite = httpsConnection.getCipherSuite();
|
||||||
|
Certificate[] peerCertificatesNonFinal = null;
|
||||||
|
try {
|
||||||
|
peerCertificatesNonFinal = httpsConnection.getServerCertificates();
|
||||||
|
} catch (SSLPeerUnverifiedException ignored) {
|
||||||
|
}
|
||||||
|
peerCertificates = peerCertificatesNonFinal;
|
||||||
|
localCertificates = httpsConnection.getLocalCertificates();
|
||||||
|
} else {
|
||||||
|
cipherSuite = null;
|
||||||
|
peerCertificates = null;
|
||||||
|
localCertificates = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeTo(DiskLruCache.Editor editor) throws IOException {
|
||||||
|
OutputStream out = editor.newOutputStream(ENTRY_METADATA);
|
||||||
|
Writer writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
|
||||||
|
|
||||||
|
writer.write(uri + '\n');
|
||||||
|
writer.write(requestMethod + '\n');
|
||||||
|
writer.write(Integer.toString(varyHeaders.length()) + '\n');
|
||||||
|
for (int i = 0; i < varyHeaders.length(); i++) {
|
||||||
|
writer.write(varyHeaders.getFieldName(i) + ": " + varyHeaders.getValue(i) + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.write(responseHeaders.getStatusLine() + '\n');
|
||||||
|
writer.write(Integer.toString(responseHeaders.length()) + '\n');
|
||||||
|
for (int i = 0; i < responseHeaders.length(); i++) {
|
||||||
|
writer.write(responseHeaders.getFieldName(i) + ": " + responseHeaders.getValue(i) + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHttps()) {
|
||||||
|
writer.write('\n');
|
||||||
|
writer.write(cipherSuite + '\n');
|
||||||
|
writeCertArray(writer, peerCertificates);
|
||||||
|
writeCertArray(writer, localCertificates);
|
||||||
|
}
|
||||||
|
writer.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isHttps() {
|
||||||
|
return uri.startsWith("https://");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Certificate[] readCertArray(StrictLineReader reader) throws IOException {
|
||||||
|
int length = reader.readInt();
|
||||||
|
if (length == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
|
||||||
|
Certificate[] result = new Certificate[length];
|
||||||
|
for (int i = 0; i < result.length; i++) {
|
||||||
|
String line = reader.readLine();
|
||||||
|
byte[] bytes = Base64.decode(line.getBytes("US-ASCII"));
|
||||||
|
result[i] = certificateFactory.generateCertificate(new ByteArrayInputStream(bytes));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
throw new IOException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeCertArray(Writer writer, Certificate[] certificates) throws IOException {
|
||||||
|
if (certificates == null) {
|
||||||
|
writer.write("-1\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
writer.write(Integer.toString(certificates.length) + '\n');
|
||||||
|
for (Certificate certificate : certificates) {
|
||||||
|
byte[] bytes = certificate.getEncoded();
|
||||||
|
String line = Base64.encode(bytes);
|
||||||
|
writer.write(line + '\n');
|
||||||
|
}
|
||||||
|
} catch (CertificateEncodingException e) {
|
||||||
|
throw new IOException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean matches(URI uri, String requestMethod,
|
||||||
|
Map<String, List<String>> requestHeaders) {
|
||||||
|
return this.uri.equals(uri.toString())
|
||||||
|
&& this.requestMethod.equals(requestMethod)
|
||||||
|
&& new ResponseHeaders(uri, responseHeaders).varyMatches(varyHeaders.toMultimap(false),
|
||||||
|
requestHeaders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an input stream that reads the body of a snapshot, closing the
|
||||||
|
* snapshot when the stream is closed.
|
||||||
|
*/
|
||||||
|
private static InputStream newBodyInputStream(final DiskLruCache.Snapshot snapshot) {
|
||||||
|
return new FilterInputStream(snapshot.getInputStream(ENTRY_BODY)) {
|
||||||
|
@Override public void close() throws IOException {
|
||||||
|
snapshot.close();
|
||||||
|
super.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static class EntryCacheResponse extends CacheResponse {
|
||||||
|
private final Entry entry;
|
||||||
|
private final DiskLruCache.Snapshot snapshot;
|
||||||
|
private final InputStream in;
|
||||||
|
|
||||||
|
public EntryCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
|
||||||
|
this.entry = entry;
|
||||||
|
this.snapshot = snapshot;
|
||||||
|
this.in = newBodyInputStream(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public Map<String, List<String>> getHeaders() {
|
||||||
|
return entry.responseHeaders.toMultimap(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public InputStream getBody() {
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class EntrySecureCacheResponse extends SecureCacheResponse {
|
||||||
|
private final Entry entry;
|
||||||
|
private final DiskLruCache.Snapshot snapshot;
|
||||||
|
private final InputStream in;
|
||||||
|
|
||||||
|
public EntrySecureCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
|
||||||
|
this.entry = entry;
|
||||||
|
this.snapshot = snapshot;
|
||||||
|
this.in = newBodyInputStream(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public Map<String, List<String>> getHeaders() {
|
||||||
|
return entry.responseHeaders.toMultimap(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public InputStream getBody() {
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public String getCipherSuite() {
|
||||||
|
return entry.cipherSuite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public List<Certificate> getServerCertificateChain()
|
||||||
|
throws SSLPeerUnverifiedException {
|
||||||
|
if (entry.peerCertificates == null || entry.peerCertificates.length == 0) {
|
||||||
|
throw new SSLPeerUnverifiedException(null);
|
||||||
|
}
|
||||||
|
return Arrays.asList(entry.peerCertificates.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
|
||||||
|
if (entry.peerCertificates == null || entry.peerCertificates.length == 0) {
|
||||||
|
throw new SSLPeerUnverifiedException(null);
|
||||||
|
}
|
||||||
|
return ((X509Certificate) entry.peerCertificates[0]).getSubjectX500Principal();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public List<Certificate> getLocalCertificateChain() {
|
||||||
|
if (entry.localCertificates == null || entry.localCertificates.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Arrays.asList(entry.localCertificates.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public Principal getLocalPrincipal() {
|
||||||
|
if (entry.localCertificates == null || entry.localCertificates.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ((X509Certificate) entry.localCertificates[0]).getSubjectX500Principal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2012 Square, Inc.
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp;
|
||||||
|
|
||||||
|
import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
|
||||||
|
import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
|
||||||
|
import com.squareup.okhttp.internal.http.OkResponseCache;
|
||||||
|
import com.squareup.okhttp.internal.http.OkResponseCacheAdapter;
|
||||||
|
import java.net.CookieHandler;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.Proxy;
|
||||||
|
import java.net.ProxySelector;
|
||||||
|
import java.net.ResponseCache;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import javax.net.ssl.HostnameVerifier;
|
||||||
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
|
|
||||||
|
/** Configures and creates HTTP connections. */
|
||||||
|
public final class OkHttpClient {
|
||||||
|
private Proxy proxy;
|
||||||
|
private Set<Route> failedRoutes = Collections.synchronizedSet(new LinkedHashSet<Route>());
|
||||||
|
private ProxySelector proxySelector;
|
||||||
|
private CookieHandler cookieHandler;
|
||||||
|
private ResponseCache responseCache;
|
||||||
|
private SSLSocketFactory sslSocketFactory;
|
||||||
|
private HostnameVerifier hostnameVerifier;
|
||||||
|
private ConnectionPool connectionPool;
|
||||||
|
private boolean followProtocolRedirects = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the HTTP proxy that will be used by connections created by this
|
||||||
|
* client. This takes precedence over {@link #setProxySelector}, which is
|
||||||
|
* only honored when this proxy is null (which it is by default). To disable
|
||||||
|
* proxy use completely, call {@code setProxy(Proxy.NO_PROXY)}.
|
||||||
|
*/
|
||||||
|
public OkHttpClient setProxy(Proxy proxy) {
|
||||||
|
this.proxy = proxy;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Proxy getProxy() {
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the proxy selection policy to be used if no {@link #setProxy proxy}
|
||||||
|
* is specified explicitly. The proxy selector may return multiple proxies;
|
||||||
|
* in that case they will be tried in sequence until a successful connection
|
||||||
|
* is established.
|
||||||
|
*
|
||||||
|
* <p>If unset, the {@link ProxySelector#getDefault() system-wide default}
|
||||||
|
* proxy selector will be used.
|
||||||
|
*/
|
||||||
|
public OkHttpClient setProxySelector(ProxySelector proxySelector) {
|
||||||
|
this.proxySelector = proxySelector;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProxySelector getProxySelector() {
|
||||||
|
return proxySelector;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the cookie handler to be used to read outgoing cookies and write
|
||||||
|
* incoming cookies.
|
||||||
|
*
|
||||||
|
* <p>If unset, the {@link CookieHandler#getDefault() system-wide default}
|
||||||
|
* cookie handler will be used.
|
||||||
|
*/
|
||||||
|
public OkHttpClient setCookieHandler(CookieHandler cookieHandler) {
|
||||||
|
this.cookieHandler = cookieHandler;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CookieHandler getCookieHandler() {
|
||||||
|
return cookieHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the response cache to be used to read and write cached responses.
|
||||||
|
*
|
||||||
|
* <p>If unset, the {@link ResponseCache#getDefault() system-wide default}
|
||||||
|
* response cache will be used.
|
||||||
|
*/
|
||||||
|
public OkHttpClient setResponseCache(ResponseCache responseCache) {
|
||||||
|
this.responseCache = responseCache;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseCache getResponseCache() {
|
||||||
|
return responseCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OkResponseCache okResponseCache() {
|
||||||
|
if (responseCache instanceof HttpResponseCache) {
|
||||||
|
return ((HttpResponseCache) responseCache).okResponseCache;
|
||||||
|
} else if (responseCache != null) {
|
||||||
|
return new OkResponseCacheAdapter(responseCache);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the socket factory used to secure HTTPS connections.
|
||||||
|
*
|
||||||
|
* <p>If unset, the {@link HttpsURLConnection#getDefaultSSLSocketFactory()
|
||||||
|
* system-wide default} SSL socket factory will be used.
|
||||||
|
*/
|
||||||
|
public OkHttpClient setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
|
||||||
|
this.sslSocketFactory = sslSocketFactory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SSLSocketFactory getSslSocketFactory() {
|
||||||
|
return sslSocketFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the verifier used to confirm that response certificates apply to
|
||||||
|
* requested hostnames for HTTPS connections.
|
||||||
|
*
|
||||||
|
* <p>If unset, the {@link HttpsURLConnection#getDefaultHostnameVerifier()
|
||||||
|
* system-wide default} hostname verifier will be used.
|
||||||
|
*/
|
||||||
|
public OkHttpClient setHostnameVerifier(HostnameVerifier hostnameVerifier) {
|
||||||
|
this.hostnameVerifier = hostnameVerifier;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HostnameVerifier getHostnameVerifier() {
|
||||||
|
return hostnameVerifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the connection pool used to recycle HTTP and HTTPS connections.
|
||||||
|
*
|
||||||
|
* <p>If unset, the {@link ConnectionPool#getDefault() system-wide
|
||||||
|
* default} connection pool will be used.
|
||||||
|
*/
|
||||||
|
public OkHttpClient setConnectionPool(ConnectionPool connectionPool) {
|
||||||
|
this.connectionPool = connectionPool;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConnectionPool getConnectionPool() {
|
||||||
|
return connectionPool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure this client to follow redirects from HTTPS to HTTP and from HTTP
|
||||||
|
* to HTTPS.
|
||||||
|
*
|
||||||
|
* <p>If unset, protocol redirects will be followed. This is different than
|
||||||
|
* the built-in {@code HttpURLConnection}'s default.
|
||||||
|
*/
|
||||||
|
public OkHttpClient setFollowProtocolRedirects(boolean followProtocolRedirects) {
|
||||||
|
this.followProtocolRedirects = followProtocolRedirects;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getFollowProtocolRedirects() {
|
||||||
|
return followProtocolRedirects;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpURLConnection open(URL url) {
|
||||||
|
String protocol = url.getProtocol();
|
||||||
|
OkHttpClient copy = copyWithDefaults();
|
||||||
|
if (protocol.equals("http")) {
|
||||||
|
return new HttpURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes);
|
||||||
|
} else if (protocol.equals("https")) {
|
||||||
|
return new HttpsURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Unexpected protocol: " + protocol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a shallow copy of this OkHttpClient that uses the system-wide default for
|
||||||
|
* each field that hasn't been explicitly configured.
|
||||||
|
*/
|
||||||
|
private OkHttpClient copyWithDefaults() {
|
||||||
|
OkHttpClient result = new OkHttpClient();
|
||||||
|
result.proxy = proxy;
|
||||||
|
result.failedRoutes = failedRoutes;
|
||||||
|
result.proxySelector = proxySelector != null ? proxySelector : ProxySelector.getDefault();
|
||||||
|
result.cookieHandler = cookieHandler != null ? cookieHandler : CookieHandler.getDefault();
|
||||||
|
result.responseCache = responseCache != null ? responseCache : ResponseCache.getDefault();
|
||||||
|
result.sslSocketFactory = sslSocketFactory != null
|
||||||
|
? sslSocketFactory
|
||||||
|
: HttpsURLConnection.getDefaultSSLSocketFactory();
|
||||||
|
result.hostnameVerifier = hostnameVerifier != null
|
||||||
|
? hostnameVerifier
|
||||||
|
: HttpsURLConnection.getDefaultHostnameVerifier();
|
||||||
|
result.connectionPool = connectionPool != null ? connectionPool : ConnectionPool.getDefault();
|
||||||
|
result.followProtocolRedirects = followProtocolRedirects;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2012 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.CacheResponse;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A response cache that supports statistics tracking and updating stored
|
||||||
|
* responses. Implementations of {@link java.net.ResponseCache} should implement
|
||||||
|
* this interface to receive additional support from the HTTP engine.
|
||||||
|
*/
|
||||||
|
public interface OkResponseCache {
|
||||||
|
|
||||||
|
/** Track an HTTP response being satisfied by {@code source}. */
|
||||||
|
void trackResponse(ResponseSource source);
|
||||||
|
|
||||||
|
/** Track an conditional GET that was satisfied by this cache. */
|
||||||
|
void trackConditionalCacheHit();
|
||||||
|
|
||||||
|
/** Updates stored HTTP headers using a hit on a conditional GET. */
|
||||||
|
void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
|
||||||
|
throws IOException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp;
|
||||||
|
|
||||||
|
/** The source of an HTTP response. */
|
||||||
|
public enum ResponseSource {
|
||||||
|
|
||||||
|
/** The response was returned from the local cache. */
|
||||||
|
CACHE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The response is available in the cache but must be validated with the
|
||||||
|
* network. The cache result will be used if it is still valid; otherwise
|
||||||
|
* the network's response will be used.
|
||||||
|
*/
|
||||||
|
CONDITIONAL_CACHE,
|
||||||
|
|
||||||
|
/** The response was returned from the network. */
|
||||||
|
NETWORK;
|
||||||
|
|
||||||
|
public boolean requiresConnection() {
|
||||||
|
return this == CONDITIONAL_CACHE || this == NETWORK;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2013 Square, Inc.
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp;
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Proxy;
|
||||||
|
|
||||||
|
/** Represents the route used by a connection to reach an endpoint. */
|
||||||
|
public class Route {
|
||||||
|
final Address address;
|
||||||
|
final Proxy proxy;
|
||||||
|
final InetSocketAddress inetSocketAddress;
|
||||||
|
final boolean modernTls;
|
||||||
|
|
||||||
|
public Route(Address address, Proxy proxy, InetSocketAddress inetSocketAddress,
|
||||||
|
boolean modernTls) {
|
||||||
|
if (address == null) throw new NullPointerException("address == null");
|
||||||
|
if (proxy == null) throw new NullPointerException("proxy == null");
|
||||||
|
if (inetSocketAddress == null) throw new NullPointerException("inetSocketAddress == null");
|
||||||
|
this.address = address;
|
||||||
|
this.proxy = proxy;
|
||||||
|
this.inetSocketAddress = inetSocketAddress;
|
||||||
|
this.modernTls = modernTls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the {@link Address} of this route. */
|
||||||
|
public Address getAddress() {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link Proxy} of this route.
|
||||||
|
*
|
||||||
|
* <strong>Warning:</strong> This may be different than the proxy returned
|
||||||
|
* by {@link #getAddress}! That is the proxy that the user asked to be
|
||||||
|
* connected to; this returns the proxy that they were actually connected
|
||||||
|
* to. The two may disagree when a proxy selector selects a different proxy
|
||||||
|
* for a connection.
|
||||||
|
*/
|
||||||
|
public Proxy getProxy() {
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the {@link InetSocketAddress} of this route. */
|
||||||
|
public InetSocketAddress getSocketAddress() {
|
||||||
|
return inetSocketAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if this route uses modern tls. */
|
||||||
|
public boolean isModernTls() {
|
||||||
|
return modernTls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a copy of this route with flipped tls mode. */
|
||||||
|
public Route flipTlsMode() {
|
||||||
|
return new Route(address, proxy, inetSocketAddress, !modernTls);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public boolean equals(Object obj) {
|
||||||
|
if (obj instanceof Route) {
|
||||||
|
Route other = (Route) obj;
|
||||||
|
return (address.equals(other.address)
|
||||||
|
&& proxy.equals(other.proxy)
|
||||||
|
&& inetSocketAddress.equals(other.inetSocketAddress)
|
||||||
|
&& modernTls == other.modernTls);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public int hashCode() {
|
||||||
|
int result = 17;
|
||||||
|
result = 31 * result + address.hashCode();
|
||||||
|
result = 31 * result + proxy.hashCode();
|
||||||
|
result = 31 * result + inetSocketAddress.hashCode();
|
||||||
|
result = result + (modernTls ? (31 * result) : 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2012 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp;
|
||||||
|
|
||||||
|
import com.squareup.okhttp.internal.http.RawHeaders;
|
||||||
|
|
||||||
|
import static com.squareup.okhttp.internal.Util.getDefaultPort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Routing and authentication information sent to an HTTP proxy to create a
|
||||||
|
* HTTPS to an origin server. Everything in the tunnel request is sent
|
||||||
|
* unencrypted to the proxy server.
|
||||||
|
*
|
||||||
|
* <p>See <a href="http://www.ietf.org/rfc/rfc2817.txt">RFC 2817, Section
|
||||||
|
* 5.2</a>.
|
||||||
|
*/
|
||||||
|
public final class TunnelRequest {
|
||||||
|
final String host;
|
||||||
|
final int port;
|
||||||
|
final String userAgent;
|
||||||
|
final String proxyAuthorization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param host the origin server's hostname. Not null.
|
||||||
|
* @param port the origin server's port, like 80 or 443.
|
||||||
|
* @param userAgent the client's user-agent. Not null.
|
||||||
|
* @param proxyAuthorization proxy authorization, or null if the proxy is
|
||||||
|
* used without an authorization header.
|
||||||
|
*/
|
||||||
|
public TunnelRequest(String host, int port, String userAgent, String proxyAuthorization) {
|
||||||
|
if (host == null) throw new NullPointerException("host == null");
|
||||||
|
if (userAgent == null) throw new NullPointerException("userAgent == null");
|
||||||
|
this.host = host;
|
||||||
|
this.port = port;
|
||||||
|
this.userAgent = userAgent;
|
||||||
|
this.proxyAuthorization = proxyAuthorization;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we're creating a TLS tunnel, send only the minimum set of headers.
|
||||||
|
* This avoids sending potentially sensitive data like HTTP cookies to
|
||||||
|
* the proxy unencrypted.
|
||||||
|
*/
|
||||||
|
RawHeaders getRequestHeaders() {
|
||||||
|
RawHeaders result = new RawHeaders();
|
||||||
|
result.setRequestLine("CONNECT " + host + ":" + port + " HTTP/1.1");
|
||||||
|
|
||||||
|
// Always set Host and User-Agent.
|
||||||
|
result.set("Host", port == getDefaultPort("https") ? host : (host + ":" + port));
|
||||||
|
result.set("User-Agent", userAgent);
|
||||||
|
|
||||||
|
// Copy over the Proxy-Authorization header if it exists.
|
||||||
|
if (proxyAuthorization != null) {
|
||||||
|
result.set("Proxy-Authorization", proxyAuthorization);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always set the Proxy-Connection to Keep-Alive for the benefit of
|
||||||
|
// HTTP/1.0 proxies like Squid.
|
||||||
|
result.set("Proxy-Connection", "Keep-Alive");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2010 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp.internal;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An output stream for an HTTP request body.
|
||||||
|
*
|
||||||
|
* <p>Since a single socket's output stream may be used to write multiple HTTP
|
||||||
|
* requests to the same server, subclasses should not close the socket stream.
|
||||||
|
*/
|
||||||
|
public abstract class AbstractOutputStream extends OutputStream {
|
||||||
|
protected boolean closed;
|
||||||
|
|
||||||
|
@Override public final void write(int data) throws IOException {
|
||||||
|
write(new byte[] { (byte) data });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final void checkNotClosed() throws IOException {
|
||||||
|
if (closed) {
|
||||||
|
throw new IOException("stream closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if this stream was closed locally. */
|
||||||
|
public boolean isClosed() {
|
||||||
|
return closed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Alexander Y. Kleymenov
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.squareup.okhttp.internal;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
|
||||||
|
import static com.squareup.okhttp.internal.Util.EMPTY_BYTE_ARRAY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="http://www.ietf.org/rfc/rfc2045.txt">Base64</a> encoder/decoder.
|
||||||
|
* In violation of the RFC, this encoder doesn't wrap lines at 76 columns.
|
||||||
|
*/
|
||||||
|
public final class Base64 {
|
||||||
|
private Base64() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] decode(byte[] in) {
|
||||||
|
return decode(in, in.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] decode(byte[] in, int len) {
|
||||||
|
// approximate output length
|
||||||
|
int length = len / 4 * 3;
|
||||||
|
// return an empty array on empty or short input without padding
|
||||||
|
if (length == 0) {
|
||||||
|
return EMPTY_BYTE_ARRAY;
|
||||||
|
}
|
||||||
|
// temporary array
|
||||||
|
byte[] out = new byte[length];
|
||||||
|
// number of padding characters ('=')
|
||||||
|
int pad = 0;
|
||||||
|
byte chr;
|
||||||
|
// compute the number of the padding characters
|
||||||
|
// and adjust the length of the input
|
||||||
|
for (; ; len--) {
|
||||||
|
chr = in[len - 1];
|
||||||
|
// skip the neutral characters
|
||||||
|
if ((chr == '\n') || (chr == '\r') || (chr == ' ') || (chr == '\t')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (chr == '=') {
|
||||||
|
pad++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// index in the output array
|
||||||
|
int outIndex = 0;
|
||||||
|
// index in the input array
|
||||||
|
int inIndex = 0;
|
||||||
|
// holds the value of the input character
|
||||||
|
int bits = 0;
|
||||||
|
// holds the value of the input quantum
|
||||||
|
int quantum = 0;
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
chr = in[i];
|
||||||
|
// skip the neutral characters
|
||||||
|
if ((chr == '\n') || (chr == '\r') || (chr == ' ') || (chr == '\t')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((chr >= 'A') && (chr <= 'Z')) {
|
||||||
|
// char ASCII value
|
||||||
|
// A 65 0
|
||||||
|
// Z 90 25 (ASCII - 65)
|
||||||
|
bits = chr - 65;
|
||||||
|
} else if ((chr >= 'a') && (chr <= 'z')) {
|
||||||
|
// char ASCII value
|
||||||
|
// a 97 26
|
||||||
|
// z 122 51 (ASCII - 71)
|
||||||
|
bits = chr - 71;
|
||||||
|
} else if ((chr >= '0') && (chr <= '9')) {
|
||||||
|
// char ASCII value
|
||||||
|
// 0 48 52
|
||||||
|
// 9 57 61 (ASCII + 4)
|
||||||
|
bits = chr + 4;
|
||||||
|
} else if (chr == '+') {
|
||||||
|
bits = 62;
|
||||||
|
} else if (chr == '/') {
|
||||||
|
bits = 63;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// append the value to the quantum
|
||||||
|
quantum = (quantum << 6) | (byte) bits;
|
||||||
|
if (inIndex % 4 == 3) {
|
||||||
|
// 4 characters were read, so make the output:
|
||||||
|
out[outIndex++] = (byte) (quantum >> 16);
|
||||||
|
out[outIndex++] = (byte) (quantum >> 8);
|
||||||
|
out[outIndex++] = (byte) quantum;
|
||||||
|
}
|
||||||
|
inIndex++;
|
||||||
|
}
|
||||||
|
if (pad > 0) {
|
||||||
|
// adjust the quantum value according to the padding
|
||||||
|
quantum = quantum << (6 * pad);
|
||||||
|
// make output
|
||||||
|
out[outIndex++] = (byte) (quantum >> 16);
|
||||||
|
if (pad == 1) {
|
||||||
|
out[outIndex++] = (byte) (quantum >> 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// create the resulting array
|
||||||
|
byte[] result = new byte[outIndex];
|
||||||
|
System.arraycopy(out, 0, result, 0, outIndex);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final byte[] MAP = new byte[] {
|
||||||
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
|
||||||
|
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
|
||||||
|
'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4',
|
||||||
|
'5', '6', '7', '8', '9', '+', '/'
|
||||||
|
};
|
||||||
|
|
||||||
|
public static String encode(byte[] in) {
|
||||||
|
int length = (in.length + 2) * 4 / 3;
|
||||||
|
byte[] out = new byte[length];
|
||||||
|
int index = 0, end = in.length - in.length % 3;
|
||||||
|
for (int i = 0; i < end; i += 3) {
|
||||||
|
out[index++] = MAP[(in[i] & 0xff) >> 2];
|
||||||
|
out[index++] = MAP[((in[i] & 0x03) << 4) | ((in[i + 1] & 0xff) >> 4)];
|
||||||
|
out[index++] = MAP[((in[i + 1] & 0x0f) << 2) | ((in[i + 2] & 0xff) >> 6)];
|
||||||
|
out[index++] = MAP[(in[i + 2] & 0x3f)];
|
||||||
|
}
|
||||||
|
switch (in.length % 3) {
|
||||||
|
case 1:
|
||||||
|
out[index++] = MAP[(in[end] & 0xff) >> 2];
|
||||||
|
out[index++] = MAP[(in[end] & 0x03) << 4];
|
||||||
|
out[index++] = '=';
|
||||||
|
out[index++] = '=';
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
out[index++] = MAP[(in[end] & 0xff) >> 2];
|
||||||
|
out[index++] = MAP[((in[end] & 0x03) << 4) | ((in[end + 1] & 0xff) >> 4)];
|
||||||
|
out[index++] = MAP[((in[end + 1] & 0x0f) << 2)];
|
||||||
|
out[index++] = '=';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new String(out, 0, index, "US-ASCII");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,926 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp.internal;
|
||||||
|
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.FilterOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cache that uses a bounded amount of space on a filesystem. Each cache
|
||||||
|
* entry has a string key and a fixed number of values. Each key must match
|
||||||
|
* the regex <strong>[a-z0-9_-]{1,64}</strong>. Values are byte sequences,
|
||||||
|
* accessible as streams or files. Each value must be between {@code 0} and
|
||||||
|
* {@code Integer.MAX_VALUE} bytes in length.
|
||||||
|
*
|
||||||
|
* <p>The cache stores its data in a directory on the filesystem. This
|
||||||
|
* directory must be exclusive to the cache; the cache may delete or overwrite
|
||||||
|
* files from its directory. It is an error for multiple processes to use the
|
||||||
|
* same cache directory at the same time.
|
||||||
|
*
|
||||||
|
* <p>This cache limits the number of bytes that it will store on the
|
||||||
|
* filesystem. When the number of stored bytes exceeds the limit, the cache will
|
||||||
|
* remove entries in the background until the limit is satisfied. The limit is
|
||||||
|
* not strict: the cache may temporarily exceed it while waiting for files to be
|
||||||
|
* deleted. The limit does not include filesystem overhead or the cache
|
||||||
|
* journal so space-sensitive applications should set a conservative limit.
|
||||||
|
*
|
||||||
|
* <p>Clients call {@link #edit} to create or update the values of an entry. An
|
||||||
|
* entry may have only one editor at one time; if a value is not available to be
|
||||||
|
* edited then {@link #edit} will return null.
|
||||||
|
* <ul>
|
||||||
|
* <li>When an entry is being <strong>created</strong> it is necessary to
|
||||||
|
* supply a full set of values; the empty value should be used as a
|
||||||
|
* placeholder if necessary.
|
||||||
|
* <li>When an entry is being <strong>edited</strong>, it is not necessary
|
||||||
|
* to supply data for every value; values default to their previous
|
||||||
|
* value.
|
||||||
|
* </ul>
|
||||||
|
* Every {@link #edit} call must be matched by a call to {@link Editor#commit}
|
||||||
|
* or {@link Editor#abort}. Committing is atomic: a read observes the full set
|
||||||
|
* of values as they were before or after the commit, but never a mix of values.
|
||||||
|
*
|
||||||
|
* <p>Clients call {@link #get} to read a snapshot of an entry. The read will
|
||||||
|
* observe the value at the time that {@link #get} was called. Updates and
|
||||||
|
* removals after the call do not impact ongoing reads.
|
||||||
|
*
|
||||||
|
* <p>This class is tolerant of some I/O errors. If files are missing from the
|
||||||
|
* filesystem, the corresponding entries will be dropped from the cache. If
|
||||||
|
* an error occurs while writing a cache value, the edit will fail silently.
|
||||||
|
* Callers should handle other problems by catching {@code IOException} and
|
||||||
|
* responding appropriately.
|
||||||
|
*/
|
||||||
|
public final class DiskLruCache implements Closeable {
|
||||||
|
static final String JOURNAL_FILE = "journal";
|
||||||
|
static final String JOURNAL_FILE_TEMP = "journal.tmp";
|
||||||
|
static final String JOURNAL_FILE_BACKUP = "journal.bkp";
|
||||||
|
static final String MAGIC = "libcore.io.DiskLruCache";
|
||||||
|
static final String VERSION_1 = "1";
|
||||||
|
static final long ANY_SEQUENCE_NUMBER = -1;
|
||||||
|
static final Pattern LEGAL_KEY_PATTERN = Pattern.compile("[a-z0-9_-]{1,64}");
|
||||||
|
private static final String CLEAN = "CLEAN";
|
||||||
|
private static final String DIRTY = "DIRTY";
|
||||||
|
private static final String REMOVE = "REMOVE";
|
||||||
|
private static final String READ = "READ";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This cache uses a journal file named "journal". A typical journal file
|
||||||
|
* looks like this:
|
||||||
|
* libcore.io.DiskLruCache
|
||||||
|
* 1
|
||||||
|
* 100
|
||||||
|
* 2
|
||||||
|
*
|
||||||
|
* CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
|
||||||
|
* DIRTY 335c4c6028171cfddfbaae1a9c313c52
|
||||||
|
* CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
|
||||||
|
* REMOVE 335c4c6028171cfddfbaae1a9c313c52
|
||||||
|
* DIRTY 1ab96a171faeeee38496d8b330771a7a
|
||||||
|
* CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
|
||||||
|
* READ 335c4c6028171cfddfbaae1a9c313c52
|
||||||
|
* READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
|
||||||
|
*
|
||||||
|
* The first five lines of the journal form its header. They are the
|
||||||
|
* constant string "libcore.io.DiskLruCache", the disk cache's version,
|
||||||
|
* the application's version, the value count, and a blank line.
|
||||||
|
*
|
||||||
|
* Each of the subsequent lines in the file is a record of the state of a
|
||||||
|
* cache entry. Each line contains space-separated values: a state, a key,
|
||||||
|
* and optional state-specific values.
|
||||||
|
* o DIRTY lines track that an entry is actively being created or updated.
|
||||||
|
* Every successful DIRTY action should be followed by a CLEAN or REMOVE
|
||||||
|
* action. DIRTY lines without a matching CLEAN or REMOVE indicate that
|
||||||
|
* temporary files may need to be deleted.
|
||||||
|
* o CLEAN lines track a cache entry that has been successfully published
|
||||||
|
* and may be read. A publish line is followed by the lengths of each of
|
||||||
|
* its values.
|
||||||
|
* o READ lines track accesses for LRU.
|
||||||
|
* o REMOVE lines track entries that have been deleted.
|
||||||
|
*
|
||||||
|
* The journal file is appended to as cache operations occur. The journal may
|
||||||
|
* occasionally be compacted by dropping redundant lines. A temporary file named
|
||||||
|
* "journal.tmp" will be used during compaction; that file should be deleted if
|
||||||
|
* it exists when the cache is opened.
|
||||||
|
*/
|
||||||
|
|
||||||
|
private final File directory;
|
||||||
|
private final File journalFile;
|
||||||
|
private final File journalFileTmp;
|
||||||
|
private final File journalFileBackup;
|
||||||
|
private final int appVersion;
|
||||||
|
private long maxSize;
|
||||||
|
private final int valueCount;
|
||||||
|
private long size = 0;
|
||||||
|
private Writer journalWriter;
|
||||||
|
private final LinkedHashMap<String, Entry> lruEntries =
|
||||||
|
new LinkedHashMap<String, Entry>(0, 0.75f, true);
|
||||||
|
private int redundantOpCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To differentiate between old and current snapshots, each entry is given
|
||||||
|
* a sequence number each time an edit is committed. A snapshot is stale if
|
||||||
|
* its sequence number is not equal to its entry's sequence number.
|
||||||
|
*/
|
||||||
|
private long nextSequenceNumber = 0;
|
||||||
|
|
||||||
|
/** This cache uses a single background thread to evict entries. */
|
||||||
|
final ThreadPoolExecutor executorService =
|
||||||
|
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
|
||||||
|
private final Callable<Void> cleanupCallable = new Callable<Void>() {
|
||||||
|
public Void call() throws Exception {
|
||||||
|
synchronized (DiskLruCache.this) {
|
||||||
|
if (journalWriter == null) {
|
||||||
|
return null; // Closed.
|
||||||
|
}
|
||||||
|
trimToSize();
|
||||||
|
if (journalRebuildRequired()) {
|
||||||
|
rebuildJournal();
|
||||||
|
redundantOpCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
|
||||||
|
this.directory = directory;
|
||||||
|
this.appVersion = appVersion;
|
||||||
|
this.journalFile = new File(directory, JOURNAL_FILE);
|
||||||
|
this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
|
||||||
|
this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
|
||||||
|
this.valueCount = valueCount;
|
||||||
|
this.maxSize = maxSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the cache in {@code directory}, creating a cache if none exists
|
||||||
|
* there.
|
||||||
|
*
|
||||||
|
* @param directory a writable directory
|
||||||
|
* @param valueCount the number of values per cache entry. Must be positive.
|
||||||
|
* @param maxSize the maximum number of bytes this cache should use to store
|
||||||
|
* @throws IOException if reading or writing the cache directory fails
|
||||||
|
*/
|
||||||
|
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
|
||||||
|
throws IOException {
|
||||||
|
if (maxSize <= 0) {
|
||||||
|
throw new IllegalArgumentException("maxSize <= 0");
|
||||||
|
}
|
||||||
|
if (valueCount <= 0) {
|
||||||
|
throw new IllegalArgumentException("valueCount <= 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a bkp file exists, use it instead.
|
||||||
|
File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
|
||||||
|
if (backupFile.exists()) {
|
||||||
|
File journalFile = new File(directory, JOURNAL_FILE);
|
||||||
|
// If journal file also exists just delete backup file.
|
||||||
|
if (journalFile.exists()) {
|
||||||
|
backupFile.delete();
|
||||||
|
} else {
|
||||||
|
renameTo(backupFile, journalFile, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer to pick up where we left off.
|
||||||
|
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
|
||||||
|
if (cache.journalFile.exists()) {
|
||||||
|
try {
|
||||||
|
cache.readJournal();
|
||||||
|
cache.processJournal();
|
||||||
|
cache.journalWriter = new BufferedWriter(
|
||||||
|
new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII));
|
||||||
|
return cache;
|
||||||
|
} catch (IOException journalIsCorrupt) {
|
||||||
|
Platform.get().logW("DiskLruCache " + directory + " is corrupt: "
|
||||||
|
+ journalIsCorrupt.getMessage() + ", removing");
|
||||||
|
cache.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new empty cache.
|
||||||
|
directory.mkdirs();
|
||||||
|
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
|
||||||
|
cache.rebuildJournal();
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readJournal() throws IOException {
|
||||||
|
StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
|
||||||
|
try {
|
||||||
|
String magic = reader.readLine();
|
||||||
|
String version = reader.readLine();
|
||||||
|
String appVersionString = reader.readLine();
|
||||||
|
String valueCountString = reader.readLine();
|
||||||
|
String blank = reader.readLine();
|
||||||
|
if (!MAGIC.equals(magic)
|
||||||
|
|| !VERSION_1.equals(version)
|
||||||
|
|| !Integer.toString(appVersion).equals(appVersionString)
|
||||||
|
|| !Integer.toString(valueCount).equals(valueCountString)
|
||||||
|
|| !"".equals(blank)) {
|
||||||
|
throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
|
||||||
|
+ valueCountString + ", " + blank + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
int lineCount = 0;
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
readJournalLine(reader.readLine());
|
||||||
|
lineCount++;
|
||||||
|
} catch (EOFException endOfJournal) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
redundantOpCount = lineCount - lruEntries.size();
|
||||||
|
} finally {
|
||||||
|
Util.closeQuietly(reader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readJournalLine(String line) throws IOException {
|
||||||
|
int firstSpace = line.indexOf(' ');
|
||||||
|
if (firstSpace == -1) {
|
||||||
|
throw new IOException("unexpected journal line: " + line);
|
||||||
|
}
|
||||||
|
|
||||||
|
int keyBegin = firstSpace + 1;
|
||||||
|
int secondSpace = line.indexOf(' ', keyBegin);
|
||||||
|
final String key;
|
||||||
|
if (secondSpace == -1) {
|
||||||
|
key = line.substring(keyBegin);
|
||||||
|
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
|
||||||
|
lruEntries.remove(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
key = line.substring(keyBegin, secondSpace);
|
||||||
|
}
|
||||||
|
|
||||||
|
Entry entry = lruEntries.get(key);
|
||||||
|
if (entry == null) {
|
||||||
|
entry = new Entry(key);
|
||||||
|
lruEntries.put(key, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
|
||||||
|
String[] parts = line.substring(secondSpace + 1).split(" ");
|
||||||
|
entry.readable = true;
|
||||||
|
entry.currentEditor = null;
|
||||||
|
entry.setLengths(parts);
|
||||||
|
} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
|
||||||
|
entry.currentEditor = new Editor(entry);
|
||||||
|
} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
|
||||||
|
// This work was already done by calling lruEntries.get().
|
||||||
|
} else {
|
||||||
|
throw new IOException("unexpected journal line: " + line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the initial size and collects garbage as a part of opening the
|
||||||
|
* cache. Dirty entries are assumed to be inconsistent and will be deleted.
|
||||||
|
*/
|
||||||
|
private void processJournal() throws IOException {
|
||||||
|
deleteIfExists(journalFileTmp);
|
||||||
|
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
|
||||||
|
Entry entry = i.next();
|
||||||
|
if (entry.currentEditor == null) {
|
||||||
|
for (int t = 0; t < valueCount; t++) {
|
||||||
|
size += entry.lengths[t];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entry.currentEditor = null;
|
||||||
|
for (int t = 0; t < valueCount; t++) {
|
||||||
|
deleteIfExists(entry.getCleanFile(t));
|
||||||
|
deleteIfExists(entry.getDirtyFile(t));
|
||||||
|
}
|
||||||
|
i.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new journal that omits redundant information. This replaces the
|
||||||
|
* current journal if it exists.
|
||||||
|
*/
|
||||||
|
private synchronized void rebuildJournal() throws IOException {
|
||||||
|
if (journalWriter != null) {
|
||||||
|
journalWriter.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
Writer writer = new BufferedWriter(
|
||||||
|
new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
|
||||||
|
try {
|
||||||
|
writer.write(MAGIC);
|
||||||
|
writer.write("\n");
|
||||||
|
writer.write(VERSION_1);
|
||||||
|
writer.write("\n");
|
||||||
|
writer.write(Integer.toString(appVersion));
|
||||||
|
writer.write("\n");
|
||||||
|
writer.write(Integer.toString(valueCount));
|
||||||
|
writer.write("\n");
|
||||||
|
writer.write("\n");
|
||||||
|
|
||||||
|
for (Entry entry : lruEntries.values()) {
|
||||||
|
if (entry.currentEditor != null) {
|
||||||
|
writer.write(DIRTY + ' ' + entry.key + '\n');
|
||||||
|
} else {
|
||||||
|
writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
writer.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (journalFile.exists()) {
|
||||||
|
renameTo(journalFile, journalFileBackup, true);
|
||||||
|
}
|
||||||
|
renameTo(journalFileTmp, journalFile, false);
|
||||||
|
journalFileBackup.delete();
|
||||||
|
|
||||||
|
journalWriter = new BufferedWriter(
|
||||||
|
new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void deleteIfExists(File file) throws IOException {
|
||||||
|
if (file.exists() && !file.delete()) {
|
||||||
|
throw new IOException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
|
||||||
|
if (deleteDestination) {
|
||||||
|
deleteIfExists(to);
|
||||||
|
}
|
||||||
|
if (!from.renameTo(to)) {
|
||||||
|
throw new IOException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a snapshot of the entry named {@code key}, or null if it doesn't
|
||||||
|
* exist is not currently readable. If a value is returned, it is moved to
|
||||||
|
* the head of the LRU queue.
|
||||||
|
*/
|
||||||
|
public synchronized Snapshot get(String key) throws IOException {
|
||||||
|
checkNotClosed();
|
||||||
|
validateKey(key);
|
||||||
|
Entry entry = lruEntries.get(key);
|
||||||
|
if (entry == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entry.readable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open all streams eagerly to guarantee that we see a single published
|
||||||
|
// snapshot. If we opened streams lazily then the streams could come
|
||||||
|
// from different edits.
|
||||||
|
InputStream[] ins = new InputStream[valueCount];
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < valueCount; i++) {
|
||||||
|
ins[i] = new FileInputStream(entry.getCleanFile(i));
|
||||||
|
}
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
// A file must have been deleted manually!
|
||||||
|
for (int i = 0; i < valueCount; i++) {
|
||||||
|
if (ins[i] != null) {
|
||||||
|
Util.closeQuietly(ins[i]);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
redundantOpCount++;
|
||||||
|
journalWriter.append(READ + ' ' + key + '\n');
|
||||||
|
if (journalRebuildRequired()) {
|
||||||
|
executorService.submit(cleanupCallable);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an editor for the entry named {@code key}, or null if another
|
||||||
|
* edit is in progress.
|
||||||
|
*/
|
||||||
|
public Editor edit(String key) throws IOException {
|
||||||
|
return edit(key, ANY_SEQUENCE_NUMBER);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
|
||||||
|
checkNotClosed();
|
||||||
|
validateKey(key);
|
||||||
|
Entry entry = lruEntries.get(key);
|
||||||
|
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|
||||||
|
|| entry.sequenceNumber != expectedSequenceNumber)) {
|
||||||
|
return null; // Snapshot is stale.
|
||||||
|
}
|
||||||
|
if (entry == null) {
|
||||||
|
entry = new Entry(key);
|
||||||
|
lruEntries.put(key, entry);
|
||||||
|
} else if (entry.currentEditor != null) {
|
||||||
|
return null; // Another edit is in progress.
|
||||||
|
}
|
||||||
|
|
||||||
|
Editor editor = new Editor(entry);
|
||||||
|
entry.currentEditor = editor;
|
||||||
|
|
||||||
|
// Flush the journal before creating files to prevent file leaks.
|
||||||
|
journalWriter.write(DIRTY + ' ' + key + '\n');
|
||||||
|
journalWriter.flush();
|
||||||
|
return editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the directory where this cache stores its data. */
|
||||||
|
public File getDirectory() {
|
||||||
|
return directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the maximum number of bytes that this cache should use to store
|
||||||
|
* its data.
|
||||||
|
*/
|
||||||
|
public long getMaxSize() {
|
||||||
|
return maxSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the maximum number of bytes the cache can store and queues a job
|
||||||
|
* to trim the existing store, if necessary.
|
||||||
|
*/
|
||||||
|
public synchronized void setMaxSize(long maxSize) {
|
||||||
|
this.maxSize = maxSize;
|
||||||
|
executorService.submit(cleanupCallable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of bytes currently being used to store the values in
|
||||||
|
* this cache. This may be greater than the max size if a background
|
||||||
|
* deletion is pending.
|
||||||
|
*/
|
||||||
|
public synchronized long size() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
|
||||||
|
Entry entry = editor.entry;
|
||||||
|
if (entry.currentEditor != editor) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this edit is creating the entry for the first time, every index must have a value.
|
||||||
|
if (success && !entry.readable) {
|
||||||
|
for (int i = 0; i < valueCount; i++) {
|
||||||
|
if (!editor.written[i]) {
|
||||||
|
editor.abort();
|
||||||
|
throw new IllegalStateException("Newly created entry didn't create value for index " + i);
|
||||||
|
}
|
||||||
|
if (!entry.getDirtyFile(i).exists()) {
|
||||||
|
editor.abort();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < valueCount; i++) {
|
||||||
|
File dirty = entry.getDirtyFile(i);
|
||||||
|
if (success) {
|
||||||
|
if (dirty.exists()) {
|
||||||
|
File clean = entry.getCleanFile(i);
|
||||||
|
dirty.renameTo(clean);
|
||||||
|
long oldLength = entry.lengths[i];
|
||||||
|
long newLength = clean.length();
|
||||||
|
entry.lengths[i] = newLength;
|
||||||
|
size = size - oldLength + newLength;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
deleteIfExists(dirty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redundantOpCount++;
|
||||||
|
entry.currentEditor = null;
|
||||||
|
if (entry.readable | success) {
|
||||||
|
entry.readable = true;
|
||||||
|
journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
|
||||||
|
if (success) {
|
||||||
|
entry.sequenceNumber = nextSequenceNumber++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lruEntries.remove(entry.key);
|
||||||
|
journalWriter.write(REMOVE + ' ' + entry.key + '\n');
|
||||||
|
}
|
||||||
|
journalWriter.flush();
|
||||||
|
|
||||||
|
if (size > maxSize || journalRebuildRequired()) {
|
||||||
|
executorService.submit(cleanupCallable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We only rebuild the journal when it will halve the size of the journal
|
||||||
|
* and eliminate at least 2000 ops.
|
||||||
|
*/
|
||||||
|
private boolean journalRebuildRequired() {
|
||||||
|
final int redundantOpCompactThreshold = 2000;
|
||||||
|
return redundantOpCount >= redundantOpCompactThreshold //
|
||||||
|
&& redundantOpCount >= lruEntries.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drops the entry for {@code key} if it exists and can be removed. Entries
|
||||||
|
* actively being edited cannot be removed.
|
||||||
|
*
|
||||||
|
* @return true if an entry was removed.
|
||||||
|
*/
|
||||||
|
public synchronized boolean remove(String key) throws IOException {
|
||||||
|
checkNotClosed();
|
||||||
|
validateKey(key);
|
||||||
|
Entry entry = lruEntries.get(key);
|
||||||
|
if (entry == null || entry.currentEditor != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < valueCount; i++) {
|
||||||
|
File file = entry.getCleanFile(i);
|
||||||
|
if (!file.delete()) {
|
||||||
|
throw new IOException("failed to delete " + file);
|
||||||
|
}
|
||||||
|
size -= entry.lengths[i];
|
||||||
|
entry.lengths[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
redundantOpCount++;
|
||||||
|
journalWriter.append(REMOVE + ' ' + key + '\n');
|
||||||
|
lruEntries.remove(key);
|
||||||
|
|
||||||
|
if (journalRebuildRequired()) {
|
||||||
|
executorService.submit(cleanupCallable);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if this cache has been closed. */
|
||||||
|
public boolean isClosed() {
|
||||||
|
return journalWriter == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkNotClosed() {
|
||||||
|
if (journalWriter == null) {
|
||||||
|
throw new IllegalStateException("cache is closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Force buffered operations to the filesystem. */
|
||||||
|
public synchronized void flush() throws IOException {
|
||||||
|
checkNotClosed();
|
||||||
|
trimToSize();
|
||||||
|
journalWriter.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Closes this cache. Stored values will remain on the filesystem. */
|
||||||
|
public synchronized void close() throws IOException {
|
||||||
|
if (journalWriter == null) {
|
||||||
|
return; // Already closed.
|
||||||
|
}
|
||||||
|
for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
|
||||||
|
if (entry.currentEditor != null) {
|
||||||
|
entry.currentEditor.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trimToSize();
|
||||||
|
journalWriter.close();
|
||||||
|
journalWriter = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void trimToSize() throws IOException {
|
||||||
|
while (size > maxSize) {
|
||||||
|
Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
|
||||||
|
remove(toEvict.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the cache and deletes all of its stored values. This will delete
|
||||||
|
* all files in the cache directory including files that weren't created by
|
||||||
|
* the cache.
|
||||||
|
*/
|
||||||
|
public void delete() throws IOException {
|
||||||
|
close();
|
||||||
|
Util.deleteContents(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateKey(String key) {
|
||||||
|
Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
|
||||||
|
if (!matcher.matches()) {
|
||||||
|
throw new IllegalArgumentException("keys must match regex [a-z0-9_-]{1,64}: \"" + key + "\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String inputStreamToString(InputStream in) throws IOException {
|
||||||
|
return Util.readFully(new InputStreamReader(in, Util.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A snapshot of the values for an entry. */
|
||||||
|
public final class Snapshot implements Closeable {
|
||||||
|
private final String key;
|
||||||
|
private final long sequenceNumber;
|
||||||
|
private final InputStream[] ins;
|
||||||
|
private final long[] lengths;
|
||||||
|
|
||||||
|
private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {
|
||||||
|
this.key = key;
|
||||||
|
this.sequenceNumber = sequenceNumber;
|
||||||
|
this.ins = ins;
|
||||||
|
this.lengths = lengths;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an editor for this snapshot's entry, or null if either the
|
||||||
|
* entry has changed since this snapshot was created or if another edit
|
||||||
|
* is in progress.
|
||||||
|
*/
|
||||||
|
public Editor edit() throws IOException {
|
||||||
|
return DiskLruCache.this.edit(key, sequenceNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the unbuffered stream with the value for {@code index}. */
|
||||||
|
public InputStream getInputStream(int index) {
|
||||||
|
return ins[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the string value for {@code index}. */
|
||||||
|
public String getString(int index) throws IOException {
|
||||||
|
return inputStreamToString(getInputStream(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the byte length of the value for {@code index}. */
|
||||||
|
public long getLength(int index) {
|
||||||
|
return lengths[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
for (InputStream in : ins) {
|
||||||
|
Util.closeQuietly(in);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
|
||||||
|
@Override
|
||||||
|
public void write(int b) throws IOException {
|
||||||
|
// Eat all writes silently. Nom nom.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Edits the values for an entry. */
|
||||||
|
public final class Editor {
|
||||||
|
private final Entry entry;
|
||||||
|
private final boolean[] written;
|
||||||
|
private boolean hasErrors;
|
||||||
|
private boolean committed;
|
||||||
|
|
||||||
|
private Editor(Entry entry) {
|
||||||
|
this.entry = entry;
|
||||||
|
this.written = (entry.readable) ? null : new boolean[valueCount];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an unbuffered input stream to read the last committed value,
|
||||||
|
* or null if no value has been committed.
|
||||||
|
*/
|
||||||
|
public InputStream newInputStream(int index) throws IOException {
|
||||||
|
synchronized (DiskLruCache.this) {
|
||||||
|
if (entry.currentEditor != this) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
if (!entry.readable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new FileInputStream(entry.getCleanFile(index));
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the last committed value as a string, or null if no value
|
||||||
|
* has been committed.
|
||||||
|
*/
|
||||||
|
public String getString(int index) throws IOException {
|
||||||
|
InputStream in = newInputStream(index);
|
||||||
|
return in != null ? inputStreamToString(in) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new unbuffered output stream to write the value at
|
||||||
|
* {@code index}. If the underlying output stream encounters errors
|
||||||
|
* when writing to the filesystem, this edit will be aborted when
|
||||||
|
* {@link #commit} is called. The returned output stream does not throw
|
||||||
|
* IOExceptions.
|
||||||
|
*/
|
||||||
|
public OutputStream newOutputStream(int index) throws IOException {
|
||||||
|
synchronized (DiskLruCache.this) {
|
||||||
|
if (entry.currentEditor != this) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
if (!entry.readable) {
|
||||||
|
written[index] = true;
|
||||||
|
}
|
||||||
|
File dirtyFile = entry.getDirtyFile(index);
|
||||||
|
FileOutputStream outputStream;
|
||||||
|
try {
|
||||||
|
outputStream = new FileOutputStream(dirtyFile);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
// Attempt to recreate the cache directory.
|
||||||
|
directory.mkdirs();
|
||||||
|
try {
|
||||||
|
outputStream = new FileOutputStream(dirtyFile);
|
||||||
|
} catch (FileNotFoundException e2) {
|
||||||
|
// We are unable to recover. Silently eat the writes.
|
||||||
|
return NULL_OUTPUT_STREAM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new FaultHidingOutputStream(outputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the value at {@code index} to {@code value}. */
|
||||||
|
public void set(int index, String value) throws IOException {
|
||||||
|
Writer writer = null;
|
||||||
|
try {
|
||||||
|
writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8);
|
||||||
|
writer.write(value);
|
||||||
|
} finally {
|
||||||
|
Util.closeQuietly(writer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commits this edit so it is visible to readers. This releases the
|
||||||
|
* edit lock so another edit may be started on the same key.
|
||||||
|
*/
|
||||||
|
public void commit() throws IOException {
|
||||||
|
if (hasErrors) {
|
||||||
|
completeEdit(this, false);
|
||||||
|
remove(entry.key); // The previous entry is stale.
|
||||||
|
} else {
|
||||||
|
completeEdit(this, true);
|
||||||
|
}
|
||||||
|
committed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aborts this edit. This releases the edit lock so another edit may be
|
||||||
|
* started on the same key.
|
||||||
|
*/
|
||||||
|
public void abort() throws IOException {
|
||||||
|
completeEdit(this, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void abortUnlessCommitted() {
|
||||||
|
if (!committed) {
|
||||||
|
try {
|
||||||
|
abort();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FaultHidingOutputStream extends FilterOutputStream {
|
||||||
|
private FaultHidingOutputStream(OutputStream out) {
|
||||||
|
super(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void write(int oneByte) {
|
||||||
|
try {
|
||||||
|
out.write(oneByte);
|
||||||
|
} catch (IOException e) {
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void write(byte[] buffer, int offset, int length) {
|
||||||
|
try {
|
||||||
|
out.write(buffer, offset, length);
|
||||||
|
} catch (IOException e) {
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void close() {
|
||||||
|
try {
|
||||||
|
out.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void flush() {
|
||||||
|
try {
|
||||||
|
out.flush();
|
||||||
|
} catch (IOException e) {
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class Entry {
|
||||||
|
private final String key;
|
||||||
|
|
||||||
|
/** Lengths of this entry's files. */
|
||||||
|
private final long[] lengths;
|
||||||
|
|
||||||
|
/** True if this entry has ever been published. */
|
||||||
|
private boolean readable;
|
||||||
|
|
||||||
|
/** The ongoing edit or null if this entry is not being edited. */
|
||||||
|
private Editor currentEditor;
|
||||||
|
|
||||||
|
/** The sequence number of the most recently committed edit to this entry. */
|
||||||
|
private long sequenceNumber;
|
||||||
|
|
||||||
|
private Entry(String key) {
|
||||||
|
this.key = key;
|
||||||
|
this.lengths = new long[valueCount];
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLengths() throws IOException {
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
for (long size : lengths) {
|
||||||
|
result.append(' ').append(size);
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set lengths using decimal numbers like "10123". */
|
||||||
|
private void setLengths(String[] strings) throws IOException {
|
||||||
|
if (strings.length != valueCount) {
|
||||||
|
throw invalidLengths(strings);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < strings.length; i++) {
|
||||||
|
lengths[i] = Long.parseLong(strings[i]);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw invalidLengths(strings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IOException invalidLengths(String[] strings) throws IOException {
|
||||||
|
throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getCleanFile(int i) {
|
||||||
|
return new File(directory, key + "." + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getDirtyFile(int i) {
|
||||||
|
return new File(directory, key + "." + i + ".tmp");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2012 Square, Inc.
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp.internal;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain name service. Prefer this over {@link InetAddress#getAllByName} to
|
||||||
|
* make code more testable.
|
||||||
|
*/
|
||||||
|
public interface Dns {
|
||||||
|
Dns DEFAULT = new Dns() {
|
||||||
|
@Override public InetAddress[] getAllByName(String host) throws UnknownHostException {
|
||||||
|
return InetAddress.getAllByName(host);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
InetAddress[] getAllByName(String host) throws UnknownHostException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2013 Square, Inc.
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp.internal;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An output stream wrapper that recovers from failures in the underlying stream
|
||||||
|
* by replacing it with another stream. This class buffers a fixed amount of
|
||||||
|
* data under the assumption that failures occur early in a stream's life.
|
||||||
|
* If a failure occurs after the buffer has been exhausted, no recovery is
|
||||||
|
* attempted.
|
||||||
|
*
|
||||||
|
* <p>Subclasses must override {@link #replacementStream} which will request a
|
||||||
|
* replacement stream each time an {@link IOException} is encountered on the
|
||||||
|
* current stream.
|
||||||
|
*/
|
||||||
|
public abstract class FaultRecoveringOutputStream extends AbstractOutputStream {
|
||||||
|
private final int maxReplayBufferLength;
|
||||||
|
|
||||||
|
/** Bytes to transmit on the replacement stream, or null if no recovery is possible. */
|
||||||
|
private ByteArrayOutputStream replayBuffer;
|
||||||
|
private OutputStream out;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param maxReplayBufferLength the maximum number of successfully written
|
||||||
|
* bytes to buffer so they can be replayed in the event of an error.
|
||||||
|
* Failure recoveries are not possible once this limit has been exceeded.
|
||||||
|
*/
|
||||||
|
public FaultRecoveringOutputStream(int maxReplayBufferLength, OutputStream out) {
|
||||||
|
if (maxReplayBufferLength < 0) throw new IllegalArgumentException();
|
||||||
|
this.maxReplayBufferLength = maxReplayBufferLength;
|
||||||
|
this.replayBuffer = new ByteArrayOutputStream(maxReplayBufferLength);
|
||||||
|
this.out = out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public final void write(byte[] buffer, int offset, int count) throws IOException {
|
||||||
|
if (closed) throw new IOException("stream closed");
|
||||||
|
checkOffsetAndCount(buffer.length, offset, count);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
out.write(buffer, offset, count);
|
||||||
|
|
||||||
|
if (replayBuffer != null) {
|
||||||
|
if (count + replayBuffer.size() > maxReplayBufferLength) {
|
||||||
|
// Failure recovery is no longer possible once we overflow the replay buffer.
|
||||||
|
replayBuffer = null;
|
||||||
|
} else {
|
||||||
|
// Remember the written bytes to the replay buffer.
|
||||||
|
replayBuffer.write(buffer, offset, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (!recover(e)) throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public final void flush() throws IOException {
|
||||||
|
if (closed) {
|
||||||
|
return; // don't throw; this stream might have been closed on the caller's behalf
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
out.flush();
|
||||||
|
return;
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (!recover(e)) throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public final void close() throws IOException {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
out.close();
|
||||||
|
closed = true;
|
||||||
|
return;
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (!recover(e)) throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to replace {@code out} with another equivalent stream. Returns true
|
||||||
|
* if a suitable replacement stream was found.
|
||||||
|
*/
|
||||||
|
private boolean recover(IOException e) {
|
||||||
|
if (replayBuffer == null) {
|
||||||
|
return false; // Can't recover because we've dropped data that we would need to replay.
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
OutputStream replacementStream = null;
|
||||||
|
try {
|
||||||
|
replacementStream = replacementStream(e);
|
||||||
|
if (replacementStream == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
replaceStream(replacementStream);
|
||||||
|
return true;
|
||||||
|
} catch (IOException replacementStreamFailure) {
|
||||||
|
// The replacement was also broken. Loop to ask for another replacement.
|
||||||
|
Util.closeQuietly(replacementStream);
|
||||||
|
e = replacementStreamFailure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if errors in the underlying stream can currently be recovered.
|
||||||
|
*/
|
||||||
|
public boolean isRecoverable() {
|
||||||
|
return replayBuffer != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the current output stream with {@code replacementStream}, writing
|
||||||
|
* any replay bytes to it if they exist. The current output stream is closed.
|
||||||
|
*/
|
||||||
|
public final void replaceStream(OutputStream replacementStream) throws IOException {
|
||||||
|
if (!isRecoverable()) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
if (this.out == replacementStream) {
|
||||||
|
return; // Don't replace a stream with itself.
|
||||||
|
}
|
||||||
|
replayBuffer.writeTo(replacementStream);
|
||||||
|
Util.closeQuietly(out);
|
||||||
|
out = replacementStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a replacement output stream to recover from {@code e} thrown by the
|
||||||
|
* previous stream. Returns a new OutputStream if recovery was successful, in
|
||||||
|
* which case all previously-written data will be replayed. Returns null if
|
||||||
|
* the failure cannot be recovered.
|
||||||
|
*/
|
||||||
|
protected abstract OutputStream replacementStream(IOException e) throws IOException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2013 Square, Inc.
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp.internal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runnable implementation which always sets its thread name.
|
||||||
|
*/
|
||||||
|
public abstract class NamedRunnable implements Runnable {
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public NamedRunnable(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public final void run() {
|
||||||
|
String oldName = Thread.currentThread().getName();
|
||||||
|
Thread.currentThread().setName(name);
|
||||||
|
try {
|
||||||
|
execute();
|
||||||
|
} finally {
|
||||||
|
Thread.currentThread().setName(oldName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void execute();
|
||||||
|
}
|
||||||
@@ -0,0 +1,389 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2012 Square, Inc.
|
||||||
|
* Copyright (C) 2012 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp.internal;
|
||||||
|
|
||||||
|
import com.squareup.okhttp.OkHttpClient;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.InvocationHandler;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import java.util.zip.Deflater;
|
||||||
|
import java.util.zip.DeflaterOutputStream;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access to Platform-specific features necessary for SPDY and advanced TLS.
|
||||||
|
*
|
||||||
|
* <h3>SPDY</h3>
|
||||||
|
* SPDY requires a TLS extension called NPN (Next Protocol Negotiation) that's
|
||||||
|
* available in Android 4.1+ and OpenJDK 7+ (with the npn-boot extension). It
|
||||||
|
* also requires a recent version of {@code DeflaterOutputStream} that is
|
||||||
|
* public API in Java 7 and callable via reflection in Android 4.1+.
|
||||||
|
*/
|
||||||
|
public class Platform {
|
||||||
|
private static final Platform PLATFORM = findPlatform();
|
||||||
|
|
||||||
|
private Constructor<DeflaterOutputStream> deflaterConstructor;
|
||||||
|
|
||||||
|
public static Platform get() {
|
||||||
|
return PLATFORM;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void logW(String warning) {
|
||||||
|
System.out.println(warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tagSocket(Socket socket) throws SocketException {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void untagSocket(Socket socket) throws SocketException {
|
||||||
|
}
|
||||||
|
|
||||||
|
public URI toUriLenient(URL url) throws URISyntaxException {
|
||||||
|
return url.toURI(); // this isn't as good as the built-in toUriLenient
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt a TLS connection with useful extensions enabled. This mode
|
||||||
|
* supports more features, but is less likely to be compatible with older
|
||||||
|
* HTTPS servers.
|
||||||
|
*/
|
||||||
|
public void enableTlsExtensions(SSLSocket socket, String uriHost) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt a secure connection with basic functionality to maximize
|
||||||
|
* compatibility. Currently this uses SSL 3.0.
|
||||||
|
*/
|
||||||
|
public void supportTlsIntolerantServer(SSLSocket socket) {
|
||||||
|
socket.setEnabledProtocols(new String[] {"SSLv3"});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the negotiated protocol, or null if no protocol was negotiated. */
|
||||||
|
public byte[] getNpnSelectedProtocol(SSLSocket socket) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets client-supported protocols on a socket to send to a server. The
|
||||||
|
* protocols are only sent if the socket implementation supports NPN.
|
||||||
|
*/
|
||||||
|
public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a deflater output stream that supports SYNC_FLUSH for SPDY name
|
||||||
|
* value blocks. This throws an {@link UnsupportedOperationException} on
|
||||||
|
* Java 6 and earlier where there is no built-in API to do SYNC_FLUSH.
|
||||||
|
*/
|
||||||
|
public OutputStream newDeflaterOutputStream(OutputStream out, Deflater deflater,
|
||||||
|
boolean syncFlush) {
|
||||||
|
try {
|
||||||
|
Constructor<DeflaterOutputStream> constructor = deflaterConstructor;
|
||||||
|
if (constructor == null) {
|
||||||
|
constructor = deflaterConstructor = DeflaterOutputStream.class.getConstructor(
|
||||||
|
OutputStream.class, Deflater.class, boolean.class);
|
||||||
|
}
|
||||||
|
return constructor.newInstance(out, deflater, syncFlush);
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
throw new UnsupportedOperationException("Cannot SPDY; no SYNC_FLUSH available");
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
throw e.getCause() instanceof RuntimeException ? (RuntimeException) e.getCause()
|
||||||
|
: new RuntimeException(e.getCause());
|
||||||
|
} catch (InstantiationException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the maximum transmission unit of the network interface used by
|
||||||
|
* {@code socket}, or a reasonable default if this platform doesn't expose the
|
||||||
|
* MTU to the application layer.
|
||||||
|
*
|
||||||
|
* <p>The returned value should only be used as an optimization; such as to
|
||||||
|
* size buffers efficiently.
|
||||||
|
*/
|
||||||
|
public int getMtu(Socket socket) throws IOException {
|
||||||
|
return 1400; // Smaller than 1500 to leave room for headers on interfaces like PPPoE.
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Attempt to match the host runtime to a capable Platform implementation. */
|
||||||
|
private static Platform findPlatform() {
|
||||||
|
Method getMtu;
|
||||||
|
try {
|
||||||
|
getMtu = NetworkInterface.class.getMethod("getMTU");
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
return new Platform(); // No Java 1.6 APIs. It's either Java 1.5, Android 2.2 or earlier.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to find Android 2.3+ APIs.
|
||||||
|
Class<?> openSslSocketClass;
|
||||||
|
Method setUseSessionTickets;
|
||||||
|
Method setHostname;
|
||||||
|
try {
|
||||||
|
openSslSocketClass = Class.forName("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
|
||||||
|
setUseSessionTickets = openSslSocketClass.getMethod("setUseSessionTickets", boolean.class);
|
||||||
|
setHostname = openSslSocketClass.getMethod("setHostname", String.class);
|
||||||
|
|
||||||
|
// Attempt to find Android 4.1+ APIs.
|
||||||
|
try {
|
||||||
|
Method setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class);
|
||||||
|
Method getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol");
|
||||||
|
return new Android41(getMtu, openSslSocketClass, setUseSessionTickets, setHostname,
|
||||||
|
setNpnProtocols, getNpnSelectedProtocol);
|
||||||
|
} catch (NoSuchMethodException ignored) {
|
||||||
|
return new Android23(getMtu, openSslSocketClass, setUseSessionTickets, setHostname);
|
||||||
|
}
|
||||||
|
} catch (ClassNotFoundException ignored) {
|
||||||
|
// This isn't an Android runtime.
|
||||||
|
} catch (NoSuchMethodException ignored) {
|
||||||
|
// This isn't Android 2.3 or better.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to find the Jetty's NPN extension for OpenJDK.
|
||||||
|
try {
|
||||||
|
String npnClassName = "org.eclipse.jetty.npn.NextProtoNego";
|
||||||
|
Class<?> nextProtoNegoClass = Class.forName(npnClassName);
|
||||||
|
Class<?> providerClass = Class.forName(npnClassName + "$Provider");
|
||||||
|
Class<?> clientProviderClass = Class.forName(npnClassName + "$ClientProvider");
|
||||||
|
Class<?> serverProviderClass = Class.forName(npnClassName + "$ServerProvider");
|
||||||
|
Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass);
|
||||||
|
Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class);
|
||||||
|
return new JdkWithJettyNpnPlatform(getMtu, putMethod, getMethod, clientProviderClass,
|
||||||
|
serverProviderClass);
|
||||||
|
} catch (ClassNotFoundException ignored) {
|
||||||
|
// NPN isn't on the classpath.
|
||||||
|
} catch (NoSuchMethodException ignored) {
|
||||||
|
// The NPN version isn't what we expect.
|
||||||
|
}
|
||||||
|
|
||||||
|
return getMtu != null ? new Java5(getMtu) : new Platform();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Java5 extends Platform {
|
||||||
|
private final Method getMtu;
|
||||||
|
|
||||||
|
private Java5(Method getMtu) {
|
||||||
|
this.getMtu = getMtu;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public int getMtu(Socket socket) throws IOException {
|
||||||
|
try {
|
||||||
|
NetworkInterface networkInterface = NetworkInterface.getByInetAddress(
|
||||||
|
socket.getLocalAddress());
|
||||||
|
return (Integer) getMtu.invoke(networkInterface);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
if (e.getCause() instanceof IOException) throw (IOException) e.getCause();
|
||||||
|
throw new RuntimeException(e.getCause());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android version 2.3 and newer support TLS session tickets and server name
|
||||||
|
* indication (SNI).
|
||||||
|
*/
|
||||||
|
private static class Android23 extends Java5 {
|
||||||
|
protected final Class<?> openSslSocketClass;
|
||||||
|
private final Method setUseSessionTickets;
|
||||||
|
private final Method setHostname;
|
||||||
|
|
||||||
|
private Android23(Method getMtu, Class<?> openSslSocketClass, Method setUseSessionTickets,
|
||||||
|
Method setHostname) {
|
||||||
|
super(getMtu);
|
||||||
|
this.openSslSocketClass = openSslSocketClass;
|
||||||
|
this.setUseSessionTickets = setUseSessionTickets;
|
||||||
|
this.setHostname = setHostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void enableTlsExtensions(SSLSocket socket, String uriHost) {
|
||||||
|
super.enableTlsExtensions(socket, uriHost);
|
||||||
|
if (openSslSocketClass.isInstance(socket)) {
|
||||||
|
// This is Android: use reflection on OpenSslSocketImpl.
|
||||||
|
try {
|
||||||
|
setUseSessionTickets.invoke(socket, true);
|
||||||
|
setHostname.invoke(socket, uriHost);
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Android version 4.1 and newer support NPN. */
|
||||||
|
private static class Android41 extends Android23 {
|
||||||
|
private final Method setNpnProtocols;
|
||||||
|
private final Method getNpnSelectedProtocol;
|
||||||
|
|
||||||
|
private Android41(Method getMtu, Class<?> openSslSocketClass, Method setUseSessionTickets,
|
||||||
|
Method setHostname, Method setNpnProtocols, Method getNpnSelectedProtocol) {
|
||||||
|
super(getMtu, openSslSocketClass, setUseSessionTickets, setHostname);
|
||||||
|
this.setNpnProtocols = setNpnProtocols;
|
||||||
|
this.getNpnSelectedProtocol = getNpnSelectedProtocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
|
||||||
|
if (!openSslSocketClass.isInstance(socket)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setNpnProtocols.invoke(socket, new Object[] {npnProtocols});
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public byte[] getNpnSelectedProtocol(SSLSocket socket) {
|
||||||
|
if (!openSslSocketClass.isInstance(socket)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return (byte[]) getNpnSelectedProtocol.invoke(socket);
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class
|
||||||
|
* path.
|
||||||
|
*/
|
||||||
|
private static class JdkWithJettyNpnPlatform extends Java5 {
|
||||||
|
private final Method getMethod;
|
||||||
|
private final Method putMethod;
|
||||||
|
private final Class<?> clientProviderClass;
|
||||||
|
private final Class<?> serverProviderClass;
|
||||||
|
|
||||||
|
public JdkWithJettyNpnPlatform(Method getMtu, Method putMethod, Method getMethod,
|
||||||
|
Class<?> clientProviderClass, Class<?> serverProviderClass) {
|
||||||
|
super(getMtu);
|
||||||
|
this.putMethod = putMethod;
|
||||||
|
this.getMethod = getMethod;
|
||||||
|
this.clientProviderClass = clientProviderClass;
|
||||||
|
this.serverProviderClass = serverProviderClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
|
||||||
|
try {
|
||||||
|
List<String> strings = new ArrayList<String>();
|
||||||
|
for (int i = 0; i < npnProtocols.length; ) {
|
||||||
|
int length = npnProtocols[i++];
|
||||||
|
strings.add(new String(npnProtocols, i, length, "US-ASCII"));
|
||||||
|
i += length;
|
||||||
|
}
|
||||||
|
Object provider = Proxy.newProxyInstance(Platform.class.getClassLoader(),
|
||||||
|
new Class[] {clientProviderClass, serverProviderClass},
|
||||||
|
new JettyNpnProvider(strings));
|
||||||
|
putMethod.invoke(null, socket, provider);
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public byte[] getNpnSelectedProtocol(SSLSocket socket) {
|
||||||
|
try {
|
||||||
|
JettyNpnProvider provider =
|
||||||
|
(JettyNpnProvider) Proxy.getInvocationHandler(getMethod.invoke(null, socket));
|
||||||
|
if (!provider.unsupported && provider.selected == null) {
|
||||||
|
Logger logger = Logger.getLogger(OkHttpClient.class.getName());
|
||||||
|
logger.log(Level.INFO,
|
||||||
|
"NPN callback dropped so SPDY is disabled. " + "Is npn-boot on the boot class path?");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return provider.unsupported ? null : provider.selected.getBytes("US-ASCII");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new AssertionError();
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
throw new AssertionError();
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the methods of NextProtoNego's ClientProvider and ServerProvider
|
||||||
|
* without a compile-time dependency on those interfaces.
|
||||||
|
*/
|
||||||
|
private static class JettyNpnProvider implements InvocationHandler {
|
||||||
|
private final List<String> protocols;
|
||||||
|
private boolean unsupported;
|
||||||
|
private String selected;
|
||||||
|
|
||||||
|
public JettyNpnProvider(List<String> protocols) {
|
||||||
|
this.protocols = protocols;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||||
|
String methodName = method.getName();
|
||||||
|
Class<?> returnType = method.getReturnType();
|
||||||
|
if (args == null) {
|
||||||
|
args = Util.EMPTY_STRING_ARRAY;
|
||||||
|
}
|
||||||
|
if (methodName.equals("supports") && boolean.class == returnType) {
|
||||||
|
return true;
|
||||||
|
} else if (methodName.equals("unsupported") && void.class == returnType) {
|
||||||
|
this.unsupported = true;
|
||||||
|
return null;
|
||||||
|
} else if (methodName.equals("protocols") && args.length == 0) {
|
||||||
|
return protocols;
|
||||||
|
} else if (methodName.equals("selectProtocol")
|
||||||
|
&& String.class == returnType
|
||||||
|
&& args.length == 1
|
||||||
|
&& (args[0] == null || args[0] instanceof List)) {
|
||||||
|
// TODO: use OpenSSL's algorithm which uses both lists
|
||||||
|
List<?> serverProtocols = (List) args[0];
|
||||||
|
this.selected = protocols.get(0);
|
||||||
|
return selected;
|
||||||
|
} else if (methodName.equals("protocolSelected") && args.length == 1) {
|
||||||
|
this.selected = (String) args[0];
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return method.invoke(this, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2012 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp.internal;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buffers input from an {@link InputStream} for reading lines.
|
||||||
|
*
|
||||||
|
* <p>This class is used for buffered reading of lines. For purposes of this class, a line ends with
|
||||||
|
* "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated line at
|
||||||
|
* end of input is invalid and will be ignored, the caller may use {@code hasUnterminatedLine()}
|
||||||
|
* to detect it after catching the {@code EOFException}.
|
||||||
|
*
|
||||||
|
* <p>This class is intended for reading input that strictly consists of lines, such as line-based
|
||||||
|
* cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction
|
||||||
|
* with {@link java.io.InputStreamReader} provides similar functionality, this class uses different
|
||||||
|
* end-of-input reporting and a more restrictive definition of a line.
|
||||||
|
*
|
||||||
|
* <p>This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
|
||||||
|
* and 10, respectively, and the representation of no other character contains these values.
|
||||||
|
* We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.
|
||||||
|
* The default charset is US_ASCII.
|
||||||
|
*/
|
||||||
|
public class StrictLineReader implements Closeable {
|
||||||
|
private static final byte CR = (byte) '\r';
|
||||||
|
private static final byte LF = (byte) '\n';
|
||||||
|
|
||||||
|
private final InputStream in;
|
||||||
|
private final Charset charset;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
|
||||||
|
* and the data in the range [pos, end) is buffered for reading. At end of input, if there is
|
||||||
|
* an unterminated line, we set end == -1, otherwise end == pos. If the underlying
|
||||||
|
* {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
|
||||||
|
*/
|
||||||
|
private byte[] buf;
|
||||||
|
private int pos;
|
||||||
|
private int end;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code LineReader} with the specified charset and the default capacity.
|
||||||
|
*
|
||||||
|
* @param in the {@code InputStream} to read data from.
|
||||||
|
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
|
||||||
|
* supported.
|
||||||
|
* @throws NullPointerException if {@code in} or {@code charset} is null.
|
||||||
|
* @throws IllegalArgumentException if the specified charset is not supported.
|
||||||
|
*/
|
||||||
|
public StrictLineReader(InputStream in, Charset charset) {
|
||||||
|
this(in, 8192, charset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code LineReader} with the specified capacity and charset.
|
||||||
|
*
|
||||||
|
* @param in the {@code InputStream} to read data from.
|
||||||
|
* @param capacity the capacity of the buffer.
|
||||||
|
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
|
||||||
|
* supported.
|
||||||
|
* @throws NullPointerException if {@code in} or {@code charset} is null.
|
||||||
|
* @throws IllegalArgumentException if {@code capacity} is negative or zero
|
||||||
|
* or the specified charset is not supported.
|
||||||
|
*/
|
||||||
|
public StrictLineReader(InputStream in, int capacity, Charset charset) {
|
||||||
|
if (in == null || charset == null) {
|
||||||
|
throw new NullPointerException();
|
||||||
|
}
|
||||||
|
if (capacity < 0) {
|
||||||
|
throw new IllegalArgumentException("capacity <= 0");
|
||||||
|
}
|
||||||
|
if (!(charset.equals(Util.US_ASCII))) {
|
||||||
|
throw new IllegalArgumentException("Unsupported encoding");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.in = in;
|
||||||
|
this.charset = charset;
|
||||||
|
buf = new byte[capacity];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the reader by closing the underlying {@code InputStream} and
|
||||||
|
* marking this reader as closed.
|
||||||
|
*
|
||||||
|
* @throws IOException for errors when closing the underlying {@code InputStream}.
|
||||||
|
*/
|
||||||
|
public void close() throws IOException {
|
||||||
|
synchronized (in) {
|
||||||
|
if (buf != null) {
|
||||||
|
buf = null;
|
||||||
|
in.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},
|
||||||
|
* this end of line marker is not included in the result.
|
||||||
|
*
|
||||||
|
* @return the next line from the input.
|
||||||
|
* @throws IOException for underlying {@code InputStream} errors.
|
||||||
|
* @throws EOFException for the end of source stream.
|
||||||
|
*/
|
||||||
|
public String readLine() throws IOException {
|
||||||
|
synchronized (in) {
|
||||||
|
if (buf == null) {
|
||||||
|
throw new IOException("LineReader is closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read more data if we are at the end of the buffered data.
|
||||||
|
// Though it's an error to read after an exception, we will let {@code fillBuf()}
|
||||||
|
// throw again if that happens; thus we need to handle end == -1 as well as end == pos.
|
||||||
|
if (pos >= end) {
|
||||||
|
fillBuf();
|
||||||
|
}
|
||||||
|
// Try to find LF in the buffered data and return the line if successful.
|
||||||
|
for (int i = pos; i != end; ++i) {
|
||||||
|
if (buf[i] == LF) {
|
||||||
|
int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
|
||||||
|
String res = new String(buf, pos, lineEnd - pos, charset.name());
|
||||||
|
pos = i + 1;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's anticipate up to 80 characters on top of those already read.
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
|
||||||
|
try {
|
||||||
|
return new String(buf, 0, length, charset.name());
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new AssertionError(e); // Since we control the charset this will never happen.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
out.write(buf, pos, end - pos);
|
||||||
|
// Mark unterminated line in case fillBuf throws EOFException or IOException.
|
||||||
|
end = -1;
|
||||||
|
fillBuf();
|
||||||
|
// Try to find LF in the buffered data and return the line if successful.
|
||||||
|
for (int i = pos; i != end; ++i) {
|
||||||
|
if (buf[i] == LF) {
|
||||||
|
if (i != pos) {
|
||||||
|
out.write(buf, pos, i - pos);
|
||||||
|
}
|
||||||
|
pos = i + 1;
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read an {@code int} from a line containing its decimal representation.
|
||||||
|
*
|
||||||
|
* @return the value of the {@code int} from the next line.
|
||||||
|
* @throws IOException for underlying {@code InputStream} errors or conversion error.
|
||||||
|
* @throws EOFException for the end of source stream.
|
||||||
|
*/
|
||||||
|
public int readInt() throws IOException {
|
||||||
|
String intString = readLine();
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(intString);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new IOException("expected an int but was \"" + intString + "\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads new input data into the buffer. Call only with pos == end or end == -1,
|
||||||
|
* depending on the desired outcome if the function throws.
|
||||||
|
*/
|
||||||
|
private void fillBuf() throws IOException {
|
||||||
|
int result = in.read(buf, 0, buf.length);
|
||||||
|
if (result == -1) {
|
||||||
|
throw new EOFException();
|
||||||
|
}
|
||||||
|
pos = 0;
|
||||||
|
end = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2012 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp.internal;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
/** Junk drawer of utility methods. */
|
||||||
|
public final class Util {
|
||||||
|
public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
|
||||||
|
public static final String[] EMPTY_STRING_ARRAY = new String[0];
|
||||||
|
|
||||||
|
/** A cheap and type-safe constant for the ISO-8859-1 Charset. */
|
||||||
|
public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
|
||||||
|
|
||||||
|
/** A cheap and type-safe constant for the US-ASCII Charset. */
|
||||||
|
public static final Charset US_ASCII = Charset.forName("US-ASCII");
|
||||||
|
|
||||||
|
/** A cheap and type-safe constant for the UTF-8 Charset. */
|
||||||
|
public static final Charset UTF_8 = Charset.forName("UTF-8");
|
||||||
|
private static AtomicReference<byte[]> skipBuffer = new AtomicReference<byte[]>();
|
||||||
|
|
||||||
|
private Util() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getEffectivePort(URI uri) {
|
||||||
|
return getEffectivePort(uri.getScheme(), uri.getPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getEffectivePort(URL url) {
|
||||||
|
return getEffectivePort(url.getProtocol(), url.getPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getEffectivePort(String scheme, int specifiedPort) {
|
||||||
|
return specifiedPort != -1 ? specifiedPort : getDefaultPort(scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getDefaultPort(String scheme) {
|
||||||
|
if ("http".equalsIgnoreCase(scheme)) {
|
||||||
|
return 80;
|
||||||
|
} else if ("https".equalsIgnoreCase(scheme)) {
|
||||||
|
return 443;
|
||||||
|
} else {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void checkOffsetAndCount(int arrayLength, int offset, int count) {
|
||||||
|
if ((offset | count) < 0 || offset > arrayLength || arrayLength - offset < count) {
|
||||||
|
throw new ArrayIndexOutOfBoundsException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void pokeInt(byte[] dst, int offset, int value, ByteOrder order) {
|
||||||
|
if (order == ByteOrder.BIG_ENDIAN) {
|
||||||
|
dst[offset++] = (byte) ((value >> 24) & 0xff);
|
||||||
|
dst[offset++] = (byte) ((value >> 16) & 0xff);
|
||||||
|
dst[offset++] = (byte) ((value >> 8) & 0xff);
|
||||||
|
dst[offset] = (byte) ((value >> 0) & 0xff);
|
||||||
|
} else {
|
||||||
|
dst[offset++] = (byte) ((value >> 0) & 0xff);
|
||||||
|
dst[offset++] = (byte) ((value >> 8) & 0xff);
|
||||||
|
dst[offset++] = (byte) ((value >> 16) & 0xff);
|
||||||
|
dst[offset] = (byte) ((value >> 24) & 0xff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if two possibly-null objects are equal. */
|
||||||
|
public static boolean equal(Object a, Object b) {
|
||||||
|
return a == b || (a != null && a.equals(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes {@code closeable}, ignoring any checked exceptions. Does nothing
|
||||||
|
* if {@code closeable} is null.
|
||||||
|
*/
|
||||||
|
public static void closeQuietly(Closeable closeable) {
|
||||||
|
if (closeable != null) {
|
||||||
|
try {
|
||||||
|
closeable.close();
|
||||||
|
} catch (RuntimeException rethrown) {
|
||||||
|
throw rethrown;
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes {@code socket}, ignoring any checked exceptions. Does nothing if
|
||||||
|
* {@code socket} is null.
|
||||||
|
*/
|
||||||
|
public static void closeQuietly(Socket socket) {
|
||||||
|
if (socket != null) {
|
||||||
|
try {
|
||||||
|
socket.close();
|
||||||
|
} catch (RuntimeException rethrown) {
|
||||||
|
throw rethrown;
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes {@code a} and {@code b}. If either close fails, this completes
|
||||||
|
* the other close and rethrows the first encountered exception.
|
||||||
|
*/
|
||||||
|
public static void closeAll(Closeable a, Closeable b) throws IOException {
|
||||||
|
Throwable thrown = null;
|
||||||
|
try {
|
||||||
|
a.close();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
thrown = e;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
b.close();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
if (thrown == null) thrown = e;
|
||||||
|
}
|
||||||
|
if (thrown == null) return;
|
||||||
|
if (thrown instanceof IOException) throw (IOException) thrown;
|
||||||
|
if (thrown instanceof RuntimeException) throw (RuntimeException) thrown;
|
||||||
|
if (thrown instanceof Error) throw (Error) thrown;
|
||||||
|
throw new AssertionError(thrown);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the contents of {@code dir}. Throws an IOException if any file
|
||||||
|
* could not be deleted, or if {@code dir} is not a readable directory.
|
||||||
|
*/
|
||||||
|
public static void deleteContents(File dir) throws IOException {
|
||||||
|
File[] files = dir.listFiles();
|
||||||
|
if (files == null) {
|
||||||
|
throw new IOException("not a readable directory: " + dir);
|
||||||
|
}
|
||||||
|
for (File file : files) {
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
deleteContents(file);
|
||||||
|
}
|
||||||
|
if (!file.delete()) {
|
||||||
|
throw new IOException("failed to delete file: " + file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements InputStream.read(int) in terms of InputStream.read(byte[], int, int).
|
||||||
|
* InputStream assumes that you implement InputStream.read(int) and provides default
|
||||||
|
* implementations of the others, but often the opposite is more efficient.
|
||||||
|
*/
|
||||||
|
public static int readSingleByte(InputStream in) throws IOException {
|
||||||
|
byte[] buffer = new byte[1];
|
||||||
|
int result = in.read(buffer, 0, 1);
|
||||||
|
return (result != -1) ? buffer[0] & 0xff : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements OutputStream.write(int) in terms of OutputStream.write(byte[], int, int).
|
||||||
|
* OutputStream assumes that you implement OutputStream.write(int) and provides default
|
||||||
|
* implementations of the others, but often the opposite is more efficient.
|
||||||
|
*/
|
||||||
|
public static void writeSingleByte(OutputStream out, int b) throws IOException {
|
||||||
|
byte[] buffer = new byte[1];
|
||||||
|
buffer[0] = (byte) (b & 0xff);
|
||||||
|
out.write(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills 'dst' with bytes from 'in', throwing EOFException if insufficient bytes are available.
|
||||||
|
*/
|
||||||
|
public static void readFully(InputStream in, byte[] dst) throws IOException {
|
||||||
|
readFully(in, dst, 0, dst.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads exactly 'byteCount' bytes from 'in' (into 'dst' at offset 'offset'), and throws
|
||||||
|
* EOFException if insufficient bytes are available.
|
||||||
|
*
|
||||||
|
* Used to implement {@link java.io.DataInputStream#readFully(byte[], int, int)}.
|
||||||
|
*/
|
||||||
|
public static void readFully(InputStream in, byte[] dst, int offset, int byteCount)
|
||||||
|
throws IOException {
|
||||||
|
if (byteCount == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (in == null) {
|
||||||
|
throw new NullPointerException("in == null");
|
||||||
|
}
|
||||||
|
if (dst == null) {
|
||||||
|
throw new NullPointerException("dst == null");
|
||||||
|
}
|
||||||
|
checkOffsetAndCount(dst.length, offset, byteCount);
|
||||||
|
while (byteCount > 0) {
|
||||||
|
int bytesRead = in.read(dst, offset, byteCount);
|
||||||
|
if (bytesRead < 0) {
|
||||||
|
throw new EOFException();
|
||||||
|
}
|
||||||
|
offset += bytesRead;
|
||||||
|
byteCount -= bytesRead;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the remainder of 'reader' as a string, closing it when done. */
|
||||||
|
public static String readFully(Reader reader) throws IOException {
|
||||||
|
try {
|
||||||
|
StringWriter writer = new StringWriter();
|
||||||
|
char[] buffer = new char[1024];
|
||||||
|
int count;
|
||||||
|
while ((count = reader.read(buffer)) != -1) {
|
||||||
|
writer.write(buffer, 0, count);
|
||||||
|
}
|
||||||
|
return writer.toString();
|
||||||
|
} finally {
|
||||||
|
reader.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void skipAll(InputStream in) throws IOException {
|
||||||
|
do {
|
||||||
|
in.skip(Long.MAX_VALUE);
|
||||||
|
} while (in.read() != -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call {@code in.read()} repeatedly until either the stream is exhausted or
|
||||||
|
* {@code byteCount} bytes have been read.
|
||||||
|
*
|
||||||
|
* <p>This method reuses the skip buffer but is careful to never use it at
|
||||||
|
* the same time that another stream is using it. Otherwise streams that use
|
||||||
|
* the caller's buffer for consistency checks like CRC could be clobbered by
|
||||||
|
* other threads. A thread-local buffer is also insufficient because some
|
||||||
|
* streams may call other streams in their skip() method, also clobbering the
|
||||||
|
* buffer.
|
||||||
|
*/
|
||||||
|
public static long skipByReading(InputStream in, long byteCount) throws IOException {
|
||||||
|
// acquire the shared skip buffer.
|
||||||
|
byte[] buffer = skipBuffer.getAndSet(null);
|
||||||
|
if (buffer == null) {
|
||||||
|
buffer = new byte[4096];
|
||||||
|
}
|
||||||
|
|
||||||
|
long skipped = 0;
|
||||||
|
while (skipped < byteCount) {
|
||||||
|
int toRead = (int) Math.min(byteCount - skipped, buffer.length);
|
||||||
|
int read = in.read(buffer, 0, toRead);
|
||||||
|
if (read == -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
skipped += read;
|
||||||
|
if (read < toRead) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// release the shared skip buffer.
|
||||||
|
skipBuffer.set(buffer);
|
||||||
|
|
||||||
|
return skipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed.
|
||||||
|
* Returns the total number of bytes transferred.
|
||||||
|
*/
|
||||||
|
public static int copy(InputStream in, OutputStream out) throws IOException {
|
||||||
|
int total = 0;
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
int c;
|
||||||
|
while ((c = in.read(buffer)) != -1) {
|
||||||
|
total += c;
|
||||||
|
out.write(buffer, 0, c);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ASCII characters up to but not including the next "\r\n", or
|
||||||
|
* "\n".
|
||||||
|
*
|
||||||
|
* @throws java.io.EOFException if the stream is exhausted before the next newline
|
||||||
|
* character.
|
||||||
|
*/
|
||||||
|
public static String readAsciiLine(InputStream in) throws IOException {
|
||||||
|
// TODO: support UTF-8 here instead
|
||||||
|
StringBuilder result = new StringBuilder(80);
|
||||||
|
while (true) {
|
||||||
|
int c = in.read();
|
||||||
|
if (c == -1) {
|
||||||
|
throw new EOFException();
|
||||||
|
} else if (c == '\n') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.append((char) c);
|
||||||
|
}
|
||||||
|
int length = result.length();
|
||||||
|
if (length > 0 && result.charAt(length - 1) == '\r') {
|
||||||
|
result.setLength(length - 1);
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2010 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp.internal.http;
|
||||||
|
|
||||||
|
import com.squareup.okhttp.internal.Util;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.CacheRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An input stream for the body of an HTTP response.
|
||||||
|
*
|
||||||
|
* <p>Since a single socket's input stream may be used to read multiple HTTP
|
||||||
|
* responses from the same server, subclasses shouldn't close the socket stream.
|
||||||
|
*
|
||||||
|
* <p>A side effect of reading an HTTP response is that the response cache
|
||||||
|
* is populated. If the stream is closed early, that cache entry will be
|
||||||
|
* invalidated.
|
||||||
|
*/
|
||||||
|
abstract class AbstractHttpInputStream extends InputStream {
|
||||||
|
protected final InputStream in;
|
||||||
|
protected final HttpEngine httpEngine;
|
||||||
|
private final CacheRequest cacheRequest;
|
||||||
|
private final OutputStream cacheBody;
|
||||||
|
protected boolean closed;
|
||||||
|
|
||||||
|
AbstractHttpInputStream(InputStream in, HttpEngine httpEngine, CacheRequest cacheRequest)
|
||||||
|
throws IOException {
|
||||||
|
this.in = in;
|
||||||
|
this.httpEngine = httpEngine;
|
||||||
|
|
||||||
|
OutputStream cacheBody = cacheRequest != null ? cacheRequest.getBody() : null;
|
||||||
|
|
||||||
|
// some apps return a null body; for compatibility we treat that like a null cache request
|
||||||
|
if (cacheBody == null) {
|
||||||
|
cacheRequest = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cacheBody = cacheBody;
|
||||||
|
this.cacheRequest = cacheRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* read() is implemented using read(byte[], int, int) so subclasses only
|
||||||
|
* need to override the latter.
|
||||||
|
*/
|
||||||
|
@Override public final int read() throws IOException {
|
||||||
|
return Util.readSingleByte(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final void checkNotClosed() throws IOException {
|
||||||
|
if (closed) {
|
||||||
|
throw new IOException("stream closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final void cacheWrite(byte[] buffer, int offset, int count) throws IOException {
|
||||||
|
if (cacheBody != null) {
|
||||||
|
cacheBody.write(buffer, offset, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the cache entry and makes the socket available for reuse. This
|
||||||
|
* should be invoked when the end of the body has been reached.
|
||||||
|
*/
|
||||||
|
protected final void endOfInput(boolean streamCancelled) throws IOException {
|
||||||
|
if (cacheRequest != null) {
|
||||||
|
cacheBody.close();
|
||||||
|
}
|
||||||
|
httpEngine.release(streamCancelled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls abort on the cache entry and disconnects the socket. This
|
||||||
|
* should be invoked when the connection is closed unexpectedly to
|
||||||
|
* invalidate the cache entry and to prevent the HTTP connection from
|
||||||
|
* being reused. HTTP messages are sent in serial so whenever a message
|
||||||
|
* cannot be read to completion, subsequent messages cannot be read
|
||||||
|
* either and the connection must be discarded.
|
||||||
|
*
|
||||||
|
* <p>An earlier implementation skipped the remaining bytes, but this
|
||||||
|
* requires that the entire transfer be completed. If the intention was
|
||||||
|
* to cancel the transfer, closing the connection is the only solution.
|
||||||
|
*/
|
||||||
|
protected final void unexpectedEndOfInput() {
|
||||||
|
if (cacheRequest != null) {
|
||||||
|
cacheRequest.abort();
|
||||||
|
}
|
||||||
|
httpEngine.release(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2010 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp.internal.http;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An output stream for the body of an HTTP request.
|
||||||
|
*
|
||||||
|
* <p>Since a single socket's output stream may be used to write multiple HTTP
|
||||||
|
* requests to the same server, subclasses should not close the socket stream.
|
||||||
|
*/
|
||||||
|
abstract class AbstractHttpOutputStream extends OutputStream {
|
||||||
|
protected boolean closed;
|
||||||
|
|
||||||
|
@Override public final void write(int data) throws IOException {
|
||||||
|
write(new byte[] { (byte) data });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final void checkNotClosed() throws IOException {
|
||||||
|
if (closed) {
|
||||||
|
throw new IOException("stream closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp.internal.http;
|
||||||
|
|
||||||
|
final class HeaderParser {
|
||||||
|
|
||||||
|
public interface CacheControlHandler {
|
||||||
|
void handle(String directive, String parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse a comma-separated list of cache control header values. */
|
||||||
|
public static void parseCacheControl(String value, CacheControlHandler handler) {
|
||||||
|
int pos = 0;
|
||||||
|
while (pos < value.length()) {
|
||||||
|
int tokenStart = pos;
|
||||||
|
pos = skipUntil(value, pos, "=,");
|
||||||
|
String directive = value.substring(tokenStart, pos).trim();
|
||||||
|
|
||||||
|
if (pos == value.length() || value.charAt(pos) == ',') {
|
||||||
|
pos++; // consume ',' (if necessary)
|
||||||
|
handler.handle(directive, null);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
pos++; // consume '='
|
||||||
|
pos = skipWhitespace(value, pos);
|
||||||
|
|
||||||
|
String parameter;
|
||||||
|
|
||||||
|
// quoted string
|
||||||
|
if (pos < value.length() && value.charAt(pos) == '\"') {
|
||||||
|
pos++; // consume '"' open quote
|
||||||
|
int parameterStart = pos;
|
||||||
|
pos = skipUntil(value, pos, "\"");
|
||||||
|
parameter = value.substring(parameterStart, pos);
|
||||||
|
pos++; // consume '"' close quote (if necessary)
|
||||||
|
|
||||||
|
// unquoted string
|
||||||
|
} else {
|
||||||
|
int parameterStart = pos;
|
||||||
|
pos = skipUntil(value, pos, ",");
|
||||||
|
parameter = value.substring(parameterStart, pos).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.handle(directive, parameter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next index in {@code input} at or after {@code pos} that
|
||||||
|
* contains a character from {@code characters}. Returns the input length if
|
||||||
|
* none of the requested characters can be found.
|
||||||
|
*/
|
||||||
|
public static int skipUntil(String input, int pos, String characters) {
|
||||||
|
for (; pos < input.length(); pos++) {
|
||||||
|
if (characters.indexOf(input.charAt(pos)) != -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next non-whitespace character in {@code input} that is white
|
||||||
|
* space. Result is undefined if input contains newline characters.
|
||||||
|
*/
|
||||||
|
public static int skipWhitespace(String input, int pos) {
|
||||||
|
for (; pos < input.length(); pos++) {
|
||||||
|
char c = input.charAt(pos);
|
||||||
|
if (c != ' ' && c != '\t') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code value} as a positive integer, or 0 if it is negative, or
|
||||||
|
* -1 if it cannot be parsed.
|
||||||
|
*/
|
||||||
|
public static int parseSeconds(String value) {
|
||||||
|
try {
|
||||||
|
long seconds = Long.parseLong(value);
|
||||||
|
if (seconds > Integer.MAX_VALUE) {
|
||||||
|
return Integer.MAX_VALUE;
|
||||||
|
} else if (seconds < 0) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return (int) seconds;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HeaderParser() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2012 Square, Inc.
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp.internal.http;
|
||||||
|
|
||||||
|
import com.squareup.okhttp.internal.Base64;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.Authenticator;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.PasswordAuthentication;
|
||||||
|
import java.net.Proxy;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
|
||||||
|
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
|
||||||
|
|
||||||
|
/** Handles HTTP authentication headers from origin and proxy servers. */
|
||||||
|
public final class HttpAuthenticator {
|
||||||
|
private HttpAuthenticator() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React to a failed authorization response by looking up new credentials.
|
||||||
|
*
|
||||||
|
* @return true if credentials have been added to successorRequestHeaders
|
||||||
|
* and another request should be attempted.
|
||||||
|
*/
|
||||||
|
public static boolean processAuthHeader(int responseCode, RawHeaders responseHeaders,
|
||||||
|
RawHeaders successorRequestHeaders, Proxy proxy, URL url) throws IOException {
|
||||||
|
if (responseCode != HTTP_PROXY_AUTH && responseCode != HTTP_UNAUTHORIZED) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep asking for username/password until authorized.
|
||||||
|
String challengeHeader =
|
||||||
|
responseCode == HTTP_PROXY_AUTH ? "Proxy-Authenticate" : "WWW-Authenticate";
|
||||||
|
String credentials = getCredentials(responseHeaders, challengeHeader, proxy, url);
|
||||||
|
if (credentials == null) {
|
||||||
|
return false; // Could not find credentials so end the request cycle.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add authorization credentials, bypassing the already-connected check.
|
||||||
|
String fieldName = responseCode == HTTP_PROXY_AUTH ? "Proxy-Authorization" : "Authorization";
|
||||||
|
successorRequestHeaders.set(fieldName, credentials);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the authorization credentials that may satisfy the challenge.
|
||||||
|
* Returns null if a challenge header was not provided or if credentials
|
||||||
|
* were not available.
|
||||||
|
*/
|
||||||
|
private static String getCredentials(RawHeaders responseHeaders, String challengeHeader,
|
||||||
|
Proxy proxy, URL url) throws IOException {
|
||||||
|
List<Challenge> challenges = parseChallenges(responseHeaders, challengeHeader);
|
||||||
|
if (challenges.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Challenge challenge : challenges) {
|
||||||
|
// Use the global authenticator to get the password.
|
||||||
|
PasswordAuthentication auth;
|
||||||
|
if (responseHeaders.getResponseCode() == HTTP_PROXY_AUTH) {
|
||||||
|
InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
|
||||||
|
auth = Authenticator.requestPasswordAuthentication(proxyAddress.getHostName(),
|
||||||
|
getConnectToInetAddress(proxy, url), proxyAddress.getPort(), url.getProtocol(),
|
||||||
|
challenge.realm, challenge.scheme, url, Authenticator.RequestorType.PROXY);
|
||||||
|
} else {
|
||||||
|
auth = Authenticator.requestPasswordAuthentication(url.getHost(),
|
||||||
|
getConnectToInetAddress(proxy, url), url.getPort(), url.getProtocol(), challenge.realm,
|
||||||
|
challenge.scheme, url, Authenticator.RequestorType.SERVER);
|
||||||
|
}
|
||||||
|
if (auth == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use base64 to encode the username and password.
|
||||||
|
String usernameAndPassword = auth.getUserName() + ":" + new String(auth.getPassword());
|
||||||
|
byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1");
|
||||||
|
String encoded = Base64.encode(bytes);
|
||||||
|
return challenge.scheme + " " + encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InetAddress getConnectToInetAddress(Proxy proxy, URL url) throws IOException {
|
||||||
|
return (proxy != null && proxy.type() != Proxy.Type.DIRECT)
|
||||||
|
? ((InetSocketAddress) proxy.address()).getAddress() : InetAddress.getByName(url.getHost());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse RFC 2617 challenges. This API is only interested in the scheme
|
||||||
|
* name and realm.
|
||||||
|
*/
|
||||||
|
private static List<Challenge> parseChallenges(RawHeaders responseHeaders,
|
||||||
|
String challengeHeader) {
|
||||||
|
// auth-scheme = token
|
||||||
|
// auth-param = token "=" ( token | quoted-string )
|
||||||
|
// challenge = auth-scheme 1*SP 1#auth-param
|
||||||
|
// realm = "realm" "=" realm-value
|
||||||
|
// realm-value = quoted-string
|
||||||
|
List<Challenge> result = new ArrayList<Challenge>();
|
||||||
|
for (int h = 0; h < responseHeaders.length(); h++) {
|
||||||
|
if (!challengeHeader.equalsIgnoreCase(responseHeaders.getFieldName(h))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String value = responseHeaders.getValue(h);
|
||||||
|
int pos = 0;
|
||||||
|
while (pos < value.length()) {
|
||||||
|
int tokenStart = pos;
|
||||||
|
pos = HeaderParser.skipUntil(value, pos, " ");
|
||||||
|
|
||||||
|
String scheme = value.substring(tokenStart, pos).trim();
|
||||||
|
pos = HeaderParser.skipWhitespace(value, pos);
|
||||||
|
|
||||||
|
// TODO: This currently only handles schemes with a 'realm' parameter;
|
||||||
|
// It needs to be fixed to handle any scheme and any parameters
|
||||||
|
// http://code.google.com/p/android/issues/detail?id=11140
|
||||||
|
|
||||||
|
if (!value.regionMatches(pos, "realm=\"", 0, "realm=\"".length())) {
|
||||||
|
break; // Unexpected challenge parameter; give up!
|
||||||
|
}
|
||||||
|
|
||||||
|
pos += "realm=\"".length();
|
||||||
|
int realmStart = pos;
|
||||||
|
pos = HeaderParser.skipUntil(value, pos, "\"");
|
||||||
|
String realm = value.substring(realmStart, pos);
|
||||||
|
pos++; // Consume '"' close quote.
|
||||||
|
pos = HeaderParser.skipUntil(value, pos, ",");
|
||||||
|
pos++; // Consume ',' comma.
|
||||||
|
pos = HeaderParser.skipWhitespace(value, pos);
|
||||||
|
result.add(new Challenge(scheme, realm));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An RFC 2617 challenge. */
|
||||||
|
private static final class Challenge {
|
||||||
|
final String scheme;
|
||||||
|
final String realm;
|
||||||
|
|
||||||
|
Challenge(String scheme, String realm) {
|
||||||
|
this.scheme = scheme;
|
||||||
|
this.realm = realm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public boolean equals(Object o) {
|
||||||
|
return o instanceof Challenge
|
||||||
|
&& ((Challenge) o).scheme.equals(scheme)
|
||||||
|
&& ((Challenge) o).realm.equals(realm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public int hashCode() {
|
||||||
|
return scheme.hashCode() + 31 * realm.hashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp.internal.http;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Best-effort parser for HTTP dates.
|
||||||
|
*/
|
||||||
|
final class HttpDate {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Most websites serve cookies in the blessed format. Eagerly create the parser to ensure such
|
||||||
|
* cookies are on the fast path.
|
||||||
|
*/
|
||||||
|
private static final ThreadLocal<DateFormat> STANDARD_DATE_FORMAT =
|
||||||
|
new ThreadLocal<DateFormat>() {
|
||||||
|
@Override protected DateFormat initialValue() {
|
||||||
|
DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
|
||||||
|
rfc1123.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||||
|
return rfc1123;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** If we fail to parse a date in a non-standard format, try each of these formats in sequence. */
|
||||||
|
private static final String[] BROWSER_COMPATIBLE_DATE_FORMATS = new String[] {
|
||||||
|
/* This list comes from {@code org.apache.http.impl.cookie.BrowserCompatSpec}. */
|
||||||
|
"EEEE, dd-MMM-yy HH:mm:ss zzz", // RFC 1036
|
||||||
|
"EEE MMM d HH:mm:ss yyyy", // ANSI C asctime()
|
||||||
|
"EEE, dd-MMM-yyyy HH:mm:ss z", "EEE, dd-MMM-yyyy HH-mm-ss z", "EEE, dd MMM yy HH:mm:ss z",
|
||||||
|
"EEE dd-MMM-yyyy HH:mm:ss z", "EEE dd MMM yyyy HH:mm:ss z", "EEE dd-MMM-yyyy HH-mm-ss z",
|
||||||
|
"EEE dd-MMM-yy HH:mm:ss z", "EEE dd MMM yy HH:mm:ss z", "EEE,dd-MMM-yy HH:mm:ss z",
|
||||||
|
"EEE,dd-MMM-yyyy HH:mm:ss z", "EEE, dd-MM-yyyy HH:mm:ss z",
|
||||||
|
|
||||||
|
/* RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com */
|
||||||
|
"EEE MMM d yyyy HH:mm:ss z", };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the date for {@code value}. Returns null if the value couldn't be
|
||||||
|
* parsed.
|
||||||
|
*/
|
||||||
|
public static Date parse(String value) {
|
||||||
|
try {
|
||||||
|
return STANDARD_DATE_FORMAT.get().parse(value);
|
||||||
|
} catch (ParseException ignore) {
|
||||||
|
}
|
||||||
|
for (String formatString : BROWSER_COMPATIBLE_DATE_FORMATS) {
|
||||||
|
try {
|
||||||
|
return new SimpleDateFormat(formatString, Locale.US).parse(value);
|
||||||
|
} catch (ParseException ignore) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the string for {@code value}. */
|
||||||
|
public static String format(Date value) {
|
||||||
|
return STANDARD_DATE_FORMAT.get().format(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpDate() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,664 @@
|
|||||||
|
/*
|
||||||
|
* 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.squareup.okhttp.internal.http;
|
||||||
|
|
||||||
|
import com.squareup.okhttp.Address;
|
||||||
|
import com.squareup.okhttp.Connection;
|
||||||
|
import com.squareup.okhttp.ResponseSource;
|
||||||
|
import com.squareup.okhttp.TunnelRequest;
|
||||||
|
import com.squareup.okhttp.internal.Dns;
|
||||||
|
import com.squareup.okhttp.internal.Platform;
|
||||||
|
import com.squareup.okhttp.internal.Util;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.CacheRequest;
|
||||||
|
import java.net.CacheResponse;
|
||||||
|
import java.net.CookieHandler;
|
||||||
|
import java.net.Proxy;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.zip.GZIPInputStream;
|
||||||
|
import javax.net.ssl.HostnameVerifier;
|
||||||
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
|
|
||||||
|
import static com.squareup.okhttp.internal.Util.EMPTY_BYTE_ARRAY;
|
||||||
|
import static com.squareup.okhttp.internal.Util.getDefaultPort;
|
||||||
|
import static com.squareup.okhttp.internal.Util.getEffectivePort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a single HTTP request/response pair. Each HTTP engine follows this
|
||||||
|
* lifecycle:
|
||||||
|
* <ol>
|
||||||
|
* <li>It is created.
|
||||||
|
* <li>The HTTP request message is sent with sendRequest(). Once the request
|
||||||
|
* is sent it is an error to modify the request headers. After
|
||||||
|
* sendRequest() has been called the request body can be written to if
|
||||||
|
* it exists.
|
||||||
|
* <li>The HTTP response message is read with readResponse(). After the
|
||||||
|
* response has been read the response headers and body can be read.
|
||||||
|
* All responses have a response body input stream, though in some
|
||||||
|
* instances this stream is empty.
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>The request and response may be served by the HTTP response cache, by the
|
||||||
|
* network, or by both in the event of a conditional GET.
|
||||||
|
*
|
||||||
|
* <p>This class may hold a socket connection that needs to be released or
|
||||||
|
* recycled. By default, this socket connection is held when the last byte of
|
||||||
|
* the response is consumed. To release the connection when it is no longer
|
||||||
|
* required, use {@link #automaticallyReleaseConnectionToPool()}.
|
||||||
|
*/
|
||||||
|
public class HttpEngine {
|
||||||
|
private static final CacheResponse GATEWAY_TIMEOUT_RESPONSE = new CacheResponse() {
|
||||||
|
@Override public Map<String, List<String>> getHeaders() throws IOException {
|
||||||
|
Map<String, List<String>> result = new HashMap<String, List<String>>();
|
||||||
|
result.put(null, Collections.singletonList("HTTP/1.1 504 Gateway Timeout"));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
@Override public InputStream getBody() throws IOException {
|
||||||
|
return new ByteArrayInputStream(EMPTY_BYTE_ARRAY);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
public static final int HTTP_CONTINUE = 100;
|
||||||
|
|
||||||
|
protected final HttpURLConnectionImpl policy;
|
||||||
|
|
||||||
|
protected final String method;
|
||||||
|
|
||||||
|
private ResponseSource responseSource;
|
||||||
|
|
||||||
|
protected Connection connection;
|
||||||
|
protected RouteSelector routeSelector;
|
||||||
|
private OutputStream requestBodyOut;
|
||||||
|
|
||||||
|
private Transport transport;
|
||||||
|
|
||||||
|
private InputStream responseTransferIn;
|
||||||
|
private InputStream responseBodyIn;
|
||||||
|
|
||||||
|
private CacheResponse cacheResponse;
|
||||||
|
private CacheRequest cacheRequest;
|
||||||
|
|
||||||
|
/** The time when the request headers were written, or -1 if they haven't been written yet. */
|
||||||
|
long sentRequestMillis = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if this client added an "Accept-Encoding: gzip" header field and is
|
||||||
|
* therefore responsible for also decompressing the transfer stream.
|
||||||
|
*/
|
||||||
|
private boolean transparentGzip;
|
||||||
|
|
||||||
|
final URI uri;
|
||||||
|
|
||||||
|
final RequestHeaders requestHeaders;
|
||||||
|
|
||||||
|
/** Null until a response is received from the network or the cache. */
|
||||||
|
ResponseHeaders responseHeaders;
|
||||||
|
|
||||||
|
// The cache response currently being validated on a conditional get. Null
|
||||||
|
// if the cached response doesn't exist or doesn't need validation. If the
|
||||||
|
// conditional get succeeds, these will be used for the response headers and
|
||||||
|
// body. If it fails, these be closed and set to null.
|
||||||
|
private ResponseHeaders cachedResponseHeaders;
|
||||||
|
private InputStream cachedResponseBody;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the socket connection should be released to the connection pool
|
||||||
|
* when the response has been fully read.
|
||||||
|
*/
|
||||||
|
private boolean automaticallyReleaseConnectionToPool;
|
||||||
|
|
||||||
|
/** True if the socket connection is no longer needed by this engine. */
|
||||||
|
private boolean connectionReleased;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param requestHeaders the client's supplied request headers. This class
|
||||||
|
* creates a private copy that it can mutate.
|
||||||
|
* @param connection the connection used for an intermediate response
|
||||||
|
* immediately prior to this request/response pair, such as a same-host
|
||||||
|
* redirect. This engine assumes ownership of the connection and must
|
||||||
|
* release it when it is unneeded.
|
||||||
|
*/
|
||||||
|
public HttpEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
|
||||||
|
Connection connection, RetryableOutputStream requestBodyOut) throws IOException {
|
||||||
|
this.policy = policy;
|
||||||
|
this.method = method;
|
||||||
|
this.connection = connection;
|
||||||
|
this.requestBodyOut = requestBodyOut;
|
||||||
|
|
||||||
|
try {
|
||||||
|
uri = Platform.get().toUriLenient(policy.getURL());
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
throw new IOException(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requestHeaders = new RequestHeaders(uri, new RawHeaders(requestHeaders));
|
||||||
|
}
|
||||||
|
|
||||||
|
public URI getUri() {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Figures out what the response source will be, and opens a socket to that
|
||||||
|
* source if necessary. Prepares the request headers and gets ready to start
|
||||||
|
* writing the request body if it exists.
|
||||||
|
*/
|
||||||
|
public final void sendRequest() throws IOException {
|
||||||
|
if (responseSource != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareRawRequestHeaders();
|
||||||
|
initResponseSource();
|
||||||
|
if (policy.responseCache != null) {
|
||||||
|
policy.responseCache.trackResponse(responseSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The raw response source may require the network, but the request
|
||||||
|
// headers may forbid network use. In that case, dispose of the network
|
||||||
|
// response and use a GATEWAY_TIMEOUT response instead, as specified
|
||||||
|
// by http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4.
|
||||||
|
if (requestHeaders.isOnlyIfCached() && responseSource.requiresConnection()) {
|
||||||
|
if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
|
||||||
|
Util.closeQuietly(cachedResponseBody);
|
||||||
|
}
|
||||||
|
this.responseSource = ResponseSource.CACHE;
|
||||||
|
this.cacheResponse = GATEWAY_TIMEOUT_RESPONSE;
|
||||||
|
RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(cacheResponse.getHeaders(), true);
|
||||||
|
setResponse(new ResponseHeaders(uri, rawResponseHeaders), cacheResponse.getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseSource.requiresConnection()) {
|
||||||
|
sendSocketRequest();
|
||||||
|
} else if (connection != null) {
|
||||||
|
policy.connectionPool.recycle(connection);
|
||||||
|
policy.getFailedRoutes().remove(connection.getRoute());
|
||||||
|
connection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the source for this response. It may be corrected later if the
|
||||||
|
* request headers forbids network use.
|
||||||
|
*/
|
||||||
|
private void initResponseSource() throws IOException {
|
||||||
|
responseSource = ResponseSource.NETWORK;
|
||||||
|
if (!policy.getUseCaches() || policy.responseCache == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheResponse candidate =
|
||||||
|
policy.responseCache.get(uri, method, requestHeaders.getHeaders().toMultimap(false));
|
||||||
|
if (candidate == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, List<String>> responseHeadersMap = candidate.getHeaders();
|
||||||
|
cachedResponseBody = candidate.getBody();
|
||||||
|
if (!acceptCacheResponseType(candidate)
|
||||||
|
|| responseHeadersMap == null
|
||||||
|
|| cachedResponseBody == null) {
|
||||||
|
Util.closeQuietly(cachedResponseBody);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(responseHeadersMap, true);
|
||||||
|
cachedResponseHeaders = new ResponseHeaders(uri, rawResponseHeaders);
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
this.responseSource = cachedResponseHeaders.chooseResponseSource(now, requestHeaders);
|
||||||
|
if (responseSource == ResponseSource.CACHE) {
|
||||||
|
this.cacheResponse = candidate;
|
||||||
|
setResponse(cachedResponseHeaders, cachedResponseBody);
|
||||||
|
} else if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
|
||||||
|
this.cacheResponse = candidate;
|
||||||
|
} else if (responseSource == ResponseSource.NETWORK) {
|
||||||
|
Util.closeQuietly(cachedResponseBody);
|
||||||
|
} else {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendSocketRequest() throws IOException {
|
||||||
|
if (connection == null) {
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transport != null) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
transport = (Transport) connection.newTransport(this);
|
||||||
|
|
||||||
|
if (hasRequestBody() && requestBodyOut == null) {
|
||||||
|
// Create a request body if we don't have one already. We'll already
|
||||||
|
// have one if we're retrying a failed POST.
|
||||||
|
requestBodyOut = transport.createRequestBody();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Connect to the origin server either directly or via a proxy. */
|
||||||
|
protected final void connect() throws IOException {
|
||||||
|
if (connection != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (routeSelector == null) {
|
||||||
|
String uriHost = uri.getHost();
|
||||||
|
if (uriHost == null) {
|
||||||
|
throw new UnknownHostException(uri.toString());
|
||||||
|
}
|
||||||
|
SSLSocketFactory sslSocketFactory = null;
|
||||||
|
HostnameVerifier hostnameVerifier = null;
|
||||||
|
if (uri.getScheme().equalsIgnoreCase("https")) {
|
||||||
|
sslSocketFactory = policy.sslSocketFactory;
|
||||||
|
hostnameVerifier = policy.hostnameVerifier;
|
||||||
|
}
|
||||||
|
Address address = new Address(uriHost, getEffectivePort(uri), sslSocketFactory,
|
||||||
|
hostnameVerifier, policy.requestedProxy);
|
||||||
|
routeSelector = new RouteSelector(address, uri, policy.proxySelector, policy.connectionPool,
|
||||||
|
Dns.DEFAULT, policy.getFailedRoutes());
|
||||||
|
}
|
||||||
|
connection = routeSelector.next();
|
||||||
|
if (!connection.isConnected()) {
|
||||||
|
connection.connect(policy.getConnectTimeout(), policy.getReadTimeout(), getTunnelConfig());
|
||||||
|
policy.connectionPool.maybeShare(connection);
|
||||||
|
policy.getFailedRoutes().remove(connection.getRoute());
|
||||||
|
}
|
||||||
|
connected(connection);
|
||||||
|
if (connection.getRoute().getProxy() != policy.requestedProxy) {
|
||||||
|
// Update the request line if the proxy changed; it may need a host name.
|
||||||
|
requestHeaders.getHeaders().setRequestLine(getRequestLine());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after a socket connection has been created or retrieved from the
|
||||||
|
* pool. Subclasses use this hook to get a reference to the TLS data.
|
||||||
|
*/
|
||||||
|
protected void connected(Connection connection) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called immediately before the transport transmits HTTP request headers.
|
||||||
|
* This is used to observe the sent time should the request be cached.
|
||||||
|
*/
|
||||||
|
public void writingRequestHeaders() {
|
||||||
|
if (sentRequestMillis != -1) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
sentRequestMillis = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param body the response body, or null if it doesn't exist or isn't
|
||||||
|
* available.
|
||||||
|
*/
|
||||||
|
private void setResponse(ResponseHeaders headers, InputStream body) throws IOException {
|
||||||
|
if (this.responseBodyIn != null) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
this.responseHeaders = headers;
|
||||||
|
if (body != null) {
|
||||||
|
initContentStream(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasRequestBody() {
|
||||||
|
return method.equals("POST") || method.equals("PUT");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the request body or null if this request doesn't have a body. */
|
||||||
|
public final OutputStream getRequestBody() {
|
||||||
|
if (responseSource == null) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
return requestBodyOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final boolean hasResponse() {
|
||||||
|
return responseHeaders != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final RequestHeaders getRequestHeaders() {
|
||||||
|
return requestHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final ResponseHeaders getResponseHeaders() {
|
||||||
|
if (responseHeaders == null) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
return responseHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final int getResponseCode() {
|
||||||
|
if (responseHeaders == null) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
return responseHeaders.getHeaders().getResponseCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public final InputStream getResponseBody() {
|
||||||
|
if (responseHeaders == null) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
return responseBodyIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final CacheResponse getCacheResponse() {
|
||||||
|
return cacheResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final Connection getConnection() {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if {@code cacheResponse} is of the right type. This
|
||||||
|
* condition is necessary but not sufficient for the cached response to
|
||||||
|
* be used.
|
||||||
|
*/
|
||||||
|
protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeCache() throws IOException {
|
||||||
|
// Are we caching at all?
|
||||||
|
if (!policy.getUseCaches() || policy.responseCache == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should we cache this response for this request?
|
||||||
|
if (!responseHeaders.isCacheable(requestHeaders)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offer this request to the cache.
|
||||||
|
cacheRequest = policy.responseCache.put(uri, policy.getHttpConnectionToCache());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cause the socket connection to be released to the connection pool when
|
||||||
|
* it is no longer needed. If it is already unneeded, it will be pooled
|
||||||
|
* immediately. Otherwise the connection is held so that redirects can be
|
||||||
|
* handled by the same connection.
|
||||||
|
*/
|
||||||
|
public final void automaticallyReleaseConnectionToPool() {
|
||||||
|
automaticallyReleaseConnectionToPool = true;
|
||||||
|
if (connection != null && connectionReleased) {
|
||||||
|
policy.connectionPool.recycle(connection);
|
||||||
|
connection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Releases this engine so that its resources may be either reused or
|
||||||
|
* closed. Also call {@link #automaticallyReleaseConnectionToPool} unless
|
||||||
|
* the connection will be used to follow a redirect.
|
||||||
|
*/
|
||||||
|
public final void release(boolean streamCancelled) {
|
||||||
|
// If the response body comes from the cache, close it.
|
||||||
|
if (responseBodyIn == cachedResponseBody) {
|
||||||
|
Util.closeQuietly(responseBodyIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connectionReleased && connection != null) {
|
||||||
|
connectionReleased = true;
|
||||||
|
|
||||||
|
if (transport == null || !transport.makeReusable(streamCancelled, requestBodyOut,
|
||||||
|
responseTransferIn)) {
|
||||||
|
Util.closeQuietly(connection);
|
||||||
|
connection = null;
|
||||||
|
} else if (automaticallyReleaseConnectionToPool) {
|
||||||
|
policy.connectionPool.recycle(connection);
|
||||||
|
connection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initContentStream(InputStream transferStream) throws IOException {
|
||||||
|
responseTransferIn = transferStream;
|
||||||
|
if (transparentGzip && responseHeaders.isContentEncodingGzip()) {
|
||||||
|
// If the response was transparently gzipped, remove the gzip header field
|
||||||
|
// so clients don't double decompress. http://b/3009828
|
||||||
|
//
|
||||||
|
// Also remove the Content-Length in this case because it contains the
|
||||||
|
// length 528 of the gzipped response. This isn't terribly useful and is
|
||||||
|
// dangerous because 529 clients can query the content length, but not
|
||||||
|
// the content encoding.
|
||||||
|
responseHeaders.stripContentEncoding();
|
||||||
|
responseHeaders.stripContentLength();
|
||||||
|
responseBodyIn = new GZIPInputStream(transferStream);
|
||||||
|
} else {
|
||||||
|
responseBodyIn = transferStream;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the response must have a (possibly 0-length) body.
|
||||||
|
* See RFC 2616 section 4.3.
|
||||||
|
*/
|
||||||
|
public final boolean hasResponseBody() {
|
||||||
|
int responseCode = responseHeaders.getHeaders().getResponseCode();
|
||||||
|
|
||||||
|
// HEAD requests never yield a body regardless of the response headers.
|
||||||
|
if (method.equals("HEAD")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((responseCode < HTTP_CONTINUE || responseCode >= 200)
|
||||||
|
&& responseCode != HttpURLConnectionImpl.HTTP_NO_CONTENT
|
||||||
|
&& responseCode != HttpURLConnectionImpl.HTTP_NOT_MODIFIED) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the Content-Length or Transfer-Encoding headers disagree with the
|
||||||
|
// response code, the response is malformed. For best compatibility, we
|
||||||
|
// honor the headers.
|
||||||
|
if (responseHeaders.getContentLength() != -1 || responseHeaders.isChunked()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates requestHeaders with defaults and cookies.
|
||||||
|
*
|
||||||
|
* <p>This client doesn't specify a default {@code Accept} header because it
|
||||||
|
* doesn't know what content types the application is interested in.
|
||||||
|
*/
|
||||||
|
private void prepareRawRequestHeaders() throws IOException {
|
||||||
|
requestHeaders.getHeaders().setRequestLine(getRequestLine());
|
||||||
|
|
||||||
|
if (requestHeaders.getUserAgent() == null) {
|
||||||
|
requestHeaders.setUserAgent(getDefaultUserAgent());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestHeaders.getHost() == null) {
|
||||||
|
requestHeaders.setHost(getOriginAddress(policy.getURL()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((connection == null || connection.getHttpMinorVersion() != 0)
|
||||||
|
&& requestHeaders.getConnection() == null) {
|
||||||
|
requestHeaders.setConnection("Keep-Alive");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestHeaders.getAcceptEncoding() == null) {
|
||||||
|
transparentGzip = true;
|
||||||
|
requestHeaders.setAcceptEncoding("gzip");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasRequestBody() && requestHeaders.getContentType() == null) {
|
||||||
|
requestHeaders.setContentType("application/x-www-form-urlencoded");
|
||||||
|
}
|
||||||
|
|
||||||
|
long ifModifiedSince = policy.getIfModifiedSince();
|
||||||
|
if (ifModifiedSince != 0) {
|
||||||
|
requestHeaders.setIfModifiedSince(new Date(ifModifiedSince));
|
||||||
|
}
|
||||||
|
|
||||||
|
CookieHandler cookieHandler = policy.cookieHandler;
|
||||||
|
if (cookieHandler != null) {
|
||||||
|
requestHeaders.addCookies(
|
||||||
|
cookieHandler.get(uri, requestHeaders.getHeaders().toMultimap(false)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request status line, like "GET / HTTP/1.1". This is exposed
|
||||||
|
* to the application by {@link HttpURLConnectionImpl#getHeaderFields}, so
|
||||||
|
* it needs to be set even if the transport is SPDY.
|
||||||
|
*/
|
||||||
|
String getRequestLine() {
|
||||||
|
String protocol =
|
||||||
|
(connection == null || connection.getHttpMinorVersion() != 0) ? "HTTP/1.1" : "HTTP/1.0";
|
||||||
|
return method + " " + requestString() + " " + protocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String requestString() {
|
||||||
|
URL url = policy.getURL();
|
||||||
|
if (includeAuthorityInRequestLine()) {
|
||||||
|
return url.toString();
|
||||||
|
} else {
|
||||||
|
return requestPath(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path to request, like the '/' in 'GET / HTTP/1.1'. Never
|
||||||
|
* empty, even if the request URL is. Includes the query component if it
|
||||||
|
* exists.
|
||||||
|
*/
|
||||||
|
public static String requestPath(URL url) {
|
||||||
|
String fileOnly = url.getFile();
|
||||||
|
if (fileOnly == null) {
|
||||||
|
return "/";
|
||||||
|
} else if (!fileOnly.startsWith("/")) {
|
||||||
|
return "/" + fileOnly;
|
||||||
|
} else {
|
||||||
|
return fileOnly;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the request line should contain the full URL with host
|
||||||
|
* and port (like "GET http://android.com/foo HTTP/1.1") or only the path
|
||||||
|
* (like "GET /foo HTTP/1.1").
|
||||||
|
*
|
||||||
|
* <p>This is non-final because for HTTPS it's never necessary to supply the
|
||||||
|
* full URL, even if a proxy is in use.
|
||||||
|
*/
|
||||||
|
protected boolean includeAuthorityInRequestLine() {
|
||||||
|
return connection == null
|
||||||
|
? policy.usingProxy() // A proxy was requested.
|
||||||
|
: connection.getRoute().getProxy().type() == Proxy.Type.HTTP; // A proxy was selected.
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getDefaultUserAgent() {
|
||||||
|
String agent = System.getProperty("http.agent");
|
||||||
|
return agent != null ? agent : ("Java" + System.getProperty("java.version"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getOriginAddress(URL url) {
|
||||||
|
int port = url.getPort();
|
||||||
|
String result = url.getHost();
|
||||||
|
if (port > 0 && port != getDefaultPort(url.getProtocol())) {
|
||||||
|
result = result + ":" + port;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flushes the remaining request header and body, parses the HTTP response
|
||||||
|
* headers and starts reading the HTTP response body if it exists.
|
||||||
|
*/
|
||||||
|
public final void readResponse() throws IOException {
|
||||||
|
if (hasResponse()) {
|
||||||
|
responseHeaders.setResponseSource(responseSource);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseSource == null) {
|
||||||
|
throw new IllegalStateException("readResponse() without sendRequest()");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!responseSource.requiresConnection()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sentRequestMillis == -1) {
|
||||||
|
if (requestBodyOut instanceof RetryableOutputStream) {
|
||||||
|
int contentLength = ((RetryableOutputStream) requestBodyOut).contentLength();
|
||||||
|
requestHeaders.setContentLength(contentLength);
|
||||||
|
}
|
||||||
|
transport.writeRequestHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestBodyOut != null) {
|
||||||
|
requestBodyOut.close();
|
||||||
|
if (requestBodyOut instanceof RetryableOutputStream) {
|
||||||
|
transport.writeRequestBody((RetryableOutputStream) requestBodyOut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transport.flushRequest();
|
||||||
|
|
||||||
|
responseHeaders = transport.readResponseHeaders();
|
||||||
|
responseHeaders.setLocalTimestamps(sentRequestMillis, System.currentTimeMillis());
|
||||||
|
responseHeaders.setResponseSource(responseSource);
|
||||||
|
|
||||||
|
if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
|
||||||
|
if (cachedResponseHeaders.validate(responseHeaders)) {
|
||||||
|
release(false);
|
||||||
|
ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders);
|
||||||
|
setResponse(combinedHeaders, cachedResponseBody);
|
||||||
|
policy.responseCache.trackConditionalCacheHit();
|
||||||
|
policy.responseCache.update(cacheResponse, policy.getHttpConnectionToCache());
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
Util.closeQuietly(cachedResponseBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasResponseBody()) {
|
||||||
|
maybeCache(); // reentrant. this calls into user code which may call back into this!
|
||||||
|
}
|
||||||
|
|
||||||
|
initContentStream(transport.getTransferStream(cacheRequest));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected TunnelRequest getTunnelConfig() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void receiveHeaders(RawHeaders headers) throws IOException {
|
||||||
|
CookieHandler cookieHandler = policy.cookieHandler;
|
||||||
|
if (cookieHandler != null) {
|
||||||
|
cookieHandler.put(uri, headers.toMultimap(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,608 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2010 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp.internal.http;
|
||||||
|
|
||||||
|
import com.squareup.okhttp.OkResponseCache;
|
||||||
|
import com.squareup.okhttp.ResponseSource;
|
||||||
|
import com.squareup.okhttp.internal.Base64;
|
||||||
|
import com.squareup.okhttp.internal.DiskLruCache;
|
||||||
|
import com.squareup.okhttp.internal.StrictLineReader;
|
||||||
|
import com.squareup.okhttp.internal.Util;
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FilterInputStream;
|
||||||
|
import java.io.FilterOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.net.CacheRequest;
|
||||||
|
import java.net.CacheResponse;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.ResponseCache;
|
||||||
|
import java.net.SecureCacheResponse;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
|
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||||
|
|
||||||
|
import static com.squareup.okhttp.internal.Util.US_ASCII;
|
||||||
|
import static com.squareup.okhttp.internal.Util.UTF_8;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache responses in a directory on the file system. Most clients should use
|
||||||
|
* {@code android.net.HttpResponseCache}, the stable, documented front end for
|
||||||
|
* this.
|
||||||
|
*/
|
||||||
|
public final class HttpResponseCache extends ResponseCache implements OkResponseCache {
|
||||||
|
private static final char[] DIGITS =
|
||||||
|
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
|
||||||
|
|
||||||
|
// TODO: add APIs to iterate the cache?
|
||||||
|
private static final int VERSION = 201105;
|
||||||
|
private static final int ENTRY_METADATA = 0;
|
||||||
|
private static final int ENTRY_BODY = 1;
|
||||||
|
private static final int ENTRY_COUNT = 2;
|
||||||
|
|
||||||
|
private final DiskLruCache cache;
|
||||||
|
|
||||||
|
/* read and write statistics, all guarded by 'this' */
|
||||||
|
private int writeSuccessCount;
|
||||||
|
private int writeAbortCount;
|
||||||
|
private int networkCount;
|
||||||
|
private int hitCount;
|
||||||
|
private int requestCount;
|
||||||
|
|
||||||
|
public HttpResponseCache(File directory, long maxSize) throws IOException {
|
||||||
|
cache = DiskLruCache.open(directory, VERSION, ENTRY_COUNT, maxSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String uriToKey(URI uri) {
|
||||||
|
try {
|
||||||
|
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
|
||||||
|
byte[] md5bytes = messageDigest.digest(uri.toString().getBytes("UTF-8"));
|
||||||
|
return bytesToHexString(md5bytes);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String bytesToHexString(byte[] bytes) {
|
||||||
|
char[] digits = DIGITS;
|
||||||
|
char[] buf = new char[bytes.length * 2];
|
||||||
|
int c = 0;
|
||||||
|
for (byte b : bytes) {
|
||||||
|
buf[c++] = digits[(b >> 4) & 0xf];
|
||||||
|
buf[c++] = digits[b & 0xf];
|
||||||
|
}
|
||||||
|
return new String(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public CacheResponse get(URI uri, String requestMethod,
|
||||||
|
Map<String, List<String>> requestHeaders) {
|
||||||
|
String key = uriToKey(uri);
|
||||||
|
DiskLruCache.Snapshot snapshot;
|
||||||
|
Entry entry;
|
||||||
|
try {
|
||||||
|
snapshot = cache.get(key);
|
||||||
|
if (snapshot == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
entry = new Entry(snapshot.getInputStream(ENTRY_METADATA));
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Give up because the cache cannot be read.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entry.matches(uri, requestMethod, requestHeaders)) {
|
||||||
|
snapshot.close();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry.isHttps() ? new EntrySecureCacheResponse(entry, snapshot)
|
||||||
|
: new EntryCacheResponse(entry, snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
|
||||||
|
if (!(urlConnection instanceof HttpURLConnection)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpURLConnection httpConnection = (HttpURLConnection) urlConnection;
|
||||||
|
String requestMethod = httpConnection.getRequestMethod();
|
||||||
|
String key = uriToKey(uri);
|
||||||
|
|
||||||
|
if (requestMethod.equals("POST") || requestMethod.equals("PUT") || requestMethod.equals(
|
||||||
|
"DELETE")) {
|
||||||
|
try {
|
||||||
|
cache.remove(key);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
// The cache cannot be written.
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} else if (!requestMethod.equals("GET")) {
|
||||||
|
// Don't cache non-GET responses. We're technically allowed to cache
|
||||||
|
// HEAD requests and some POST requests, but the complexity of doing
|
||||||
|
// so is high and the benefit is low.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpEngine httpEngine = getHttpEngine(httpConnection);
|
||||||
|
if (httpEngine == null) {
|
||||||
|
// Don't cache unless the HTTP implementation is ours.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResponseHeaders response = httpEngine.getResponseHeaders();
|
||||||
|
if (response.hasVaryAll()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
RawHeaders varyHeaders =
|
||||||
|
httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields());
|
||||||
|
Entry entry = new Entry(uri, varyHeaders, httpConnection);
|
||||||
|
DiskLruCache.Editor editor = null;
|
||||||
|
try {
|
||||||
|
editor = cache.edit(key);
|
||||||
|
if (editor == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
entry.writeTo(editor);
|
||||||
|
return new CacheRequestImpl(editor);
|
||||||
|
} catch (IOException e) {
|
||||||
|
abortQuietly(editor);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a conditional request hit by updating the stored cache response
|
||||||
|
* with the headers from {@code httpConnection}. The cached response body is
|
||||||
|
* not updated. If the stored response has changed since {@code
|
||||||
|
* conditionalCacheHit} was returned, this does nothing.
|
||||||
|
*/
|
||||||
|
@Override public void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
|
||||||
|
throws IOException {
|
||||||
|
HttpEngine httpEngine = getHttpEngine(httpConnection);
|
||||||
|
URI uri = httpEngine.getUri();
|
||||||
|
ResponseHeaders response = httpEngine.getResponseHeaders();
|
||||||
|
RawHeaders varyHeaders =
|
||||||
|
httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields());
|
||||||
|
Entry entry = new Entry(uri, varyHeaders, httpConnection);
|
||||||
|
DiskLruCache.Snapshot snapshot = (conditionalCacheHit instanceof EntryCacheResponse)
|
||||||
|
? ((EntryCacheResponse) conditionalCacheHit).snapshot
|
||||||
|
: ((EntrySecureCacheResponse) conditionalCacheHit).snapshot;
|
||||||
|
DiskLruCache.Editor editor = null;
|
||||||
|
try {
|
||||||
|
editor = snapshot.edit(); // returns null if snapshot is not current
|
||||||
|
if (editor != null) {
|
||||||
|
entry.writeTo(editor);
|
||||||
|
editor.commit();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
abortQuietly(editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void abortQuietly(DiskLruCache.Editor editor) {
|
||||||
|
// Give up because the cache cannot be written.
|
||||||
|
try {
|
||||||
|
if (editor != null) {
|
||||||
|
editor.abort();
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpEngine getHttpEngine(URLConnection httpConnection) {
|
||||||
|
if (httpConnection instanceof HttpURLConnectionImpl) {
|
||||||
|
return ((HttpURLConnectionImpl) httpConnection).getHttpEngine();
|
||||||
|
} else if (httpConnection instanceof HttpsURLConnectionImpl) {
|
||||||
|
return ((HttpsURLConnectionImpl) httpConnection).getHttpEngine();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DiskLruCache getCache() {
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int getWriteAbortCount() {
|
||||||
|
return writeAbortCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int getWriteSuccessCount() {
|
||||||
|
return writeSuccessCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void trackResponse(ResponseSource source) {
|
||||||
|
requestCount++;
|
||||||
|
|
||||||
|
switch (source) {
|
||||||
|
case CACHE:
|
||||||
|
hitCount++;
|
||||||
|
break;
|
||||||
|
case CONDITIONAL_CACHE:
|
||||||
|
case NETWORK:
|
||||||
|
networkCount++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void trackConditionalCacheHit() {
|
||||||
|
hitCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int getNetworkCount() {
|
||||||
|
return networkCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int getHitCount() {
|
||||||
|
return hitCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int getRequestCount() {
|
||||||
|
return requestCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class CacheRequestImpl extends CacheRequest {
|
||||||
|
private final DiskLruCache.Editor editor;
|
||||||
|
private OutputStream cacheOut;
|
||||||
|
private boolean done;
|
||||||
|
private OutputStream body;
|
||||||
|
|
||||||
|
public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException {
|
||||||
|
this.editor = editor;
|
||||||
|
this.cacheOut = editor.newOutputStream(ENTRY_BODY);
|
||||||
|
this.body = new FilterOutputStream(cacheOut) {
|
||||||
|
@Override public void close() throws IOException {
|
||||||
|
synchronized (HttpResponseCache.this) {
|
||||||
|
if (done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
done = true;
|
||||||
|
writeSuccessCount++;
|
||||||
|
}
|
||||||
|
super.close();
|
||||||
|
editor.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte[] buffer, int offset, int length) throws IOException {
|
||||||
|
// Since we don't override "write(int oneByte)", we can write directly to "out"
|
||||||
|
// and avoid the inefficient implementation from the FilterOutputStream.
|
||||||
|
out.write(buffer, offset, length);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void abort() {
|
||||||
|
synchronized (HttpResponseCache.this) {
|
||||||
|
if (done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
done = true;
|
||||||
|
writeAbortCount++;
|
||||||
|
}
|
||||||
|
Util.closeQuietly(cacheOut);
|
||||||
|
try {
|
||||||
|
editor.abort();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public OutputStream getBody() throws IOException {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Entry {
|
||||||
|
private final String uri;
|
||||||
|
private final RawHeaders varyHeaders;
|
||||||
|
private final String requestMethod;
|
||||||
|
private final RawHeaders responseHeaders;
|
||||||
|
private final String cipherSuite;
|
||||||
|
private final Certificate[] peerCertificates;
|
||||||
|
private final Certificate[] localCertificates;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an entry from an input stream. A typical entry looks like this:
|
||||||
|
* <pre>{@code
|
||||||
|
* http://google.com/foo
|
||||||
|
* GET
|
||||||
|
* 2
|
||||||
|
* Accept-Language: fr-CA
|
||||||
|
* Accept-Charset: UTF-8
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* 3
|
||||||
|
* Content-Type: image/png
|
||||||
|
* Content-Length: 100
|
||||||
|
* Cache-Control: max-age=600
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>A typical HTTPS file looks like this:
|
||||||
|
* <pre>{@code
|
||||||
|
* https://google.com/foo
|
||||||
|
* GET
|
||||||
|
* 2
|
||||||
|
* Accept-Language: fr-CA
|
||||||
|
* Accept-Charset: UTF-8
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* 3
|
||||||
|
* Content-Type: image/png
|
||||||
|
* Content-Length: 100
|
||||||
|
* Cache-Control: max-age=600
|
||||||
|
*
|
||||||
|
* AES_256_WITH_MD5
|
||||||
|
* 2
|
||||||
|
* base64-encoded peerCertificate[0]
|
||||||
|
* base64-encoded peerCertificate[1]
|
||||||
|
* -1
|
||||||
|
* }</pre>
|
||||||
|
* The file is newline separated. The first two lines are the URL and
|
||||||
|
* the request method. Next is the number of HTTP Vary request header
|
||||||
|
* lines, followed by those lines.
|
||||||
|
*
|
||||||
|
* <p>Next is the response status line, followed by the number of HTTP
|
||||||
|
* response header lines, followed by those lines.
|
||||||
|
*
|
||||||
|
* <p>HTTPS responses also contain SSL session information. This begins
|
||||||
|
* with a blank line, and then a line containing the cipher suite. Next
|
||||||
|
* is the length of the peer certificate chain. These certificates are
|
||||||
|
* base64-encoded and appear each on their own line. The next line
|
||||||
|
* contains the length of the local certificate chain. These
|
||||||
|
* certificates are also base64-encoded and appear each on their own
|
||||||
|
* line. A length of -1 is used to encode a null array.
|
||||||
|
*/
|
||||||
|
public Entry(InputStream in) throws IOException {
|
||||||
|
try {
|
||||||
|
StrictLineReader reader = new StrictLineReader(in, US_ASCII);
|
||||||
|
uri = reader.readLine();
|
||||||
|
requestMethod = reader.readLine();
|
||||||
|
varyHeaders = new RawHeaders();
|
||||||
|
int varyRequestHeaderLineCount = reader.readInt();
|
||||||
|
for (int i = 0; i < varyRequestHeaderLineCount; i++) {
|
||||||
|
varyHeaders.addLine(reader.readLine());
|
||||||
|
}
|
||||||
|
|
||||||
|
responseHeaders = new RawHeaders();
|
||||||
|
responseHeaders.setStatusLine(reader.readLine());
|
||||||
|
int responseHeaderLineCount = reader.readInt();
|
||||||
|
for (int i = 0; i < responseHeaderLineCount; i++) {
|
||||||
|
responseHeaders.addLine(reader.readLine());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHttps()) {
|
||||||
|
String blank = reader.readLine();
|
||||||
|
if (!blank.isEmpty()) {
|
||||||
|
throw new IOException("expected \"\" but was \"" + blank + "\"");
|
||||||
|
}
|
||||||
|
cipherSuite = reader.readLine();
|
||||||
|
peerCertificates = readCertArray(reader);
|
||||||
|
localCertificates = readCertArray(reader);
|
||||||
|
} else {
|
||||||
|
cipherSuite = null;
|
||||||
|
peerCertificates = null;
|
||||||
|
localCertificates = null;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
in.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Entry(URI uri, RawHeaders varyHeaders, HttpURLConnection httpConnection)
|
||||||
|
throws IOException {
|
||||||
|
this.uri = uri.toString();
|
||||||
|
this.varyHeaders = varyHeaders;
|
||||||
|
this.requestMethod = httpConnection.getRequestMethod();
|
||||||
|
this.responseHeaders = RawHeaders.fromMultimap(httpConnection.getHeaderFields(), true);
|
||||||
|
|
||||||
|
if (isHttps()) {
|
||||||
|
HttpsURLConnection httpsConnection = (HttpsURLConnection) httpConnection;
|
||||||
|
cipherSuite = httpsConnection.getCipherSuite();
|
||||||
|
Certificate[] peerCertificatesNonFinal = null;
|
||||||
|
try {
|
||||||
|
peerCertificatesNonFinal = httpsConnection.getServerCertificates();
|
||||||
|
} catch (SSLPeerUnverifiedException ignored) {
|
||||||
|
}
|
||||||
|
peerCertificates = peerCertificatesNonFinal;
|
||||||
|
localCertificates = httpsConnection.getLocalCertificates();
|
||||||
|
} else {
|
||||||
|
cipherSuite = null;
|
||||||
|
peerCertificates = null;
|
||||||
|
localCertificates = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeTo(DiskLruCache.Editor editor) throws IOException {
|
||||||
|
OutputStream out = editor.newOutputStream(ENTRY_METADATA);
|
||||||
|
Writer writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
|
||||||
|
|
||||||
|
writer.write(uri + '\n');
|
||||||
|
writer.write(requestMethod + '\n');
|
||||||
|
writer.write(Integer.toString(varyHeaders.length()) + '\n');
|
||||||
|
for (int i = 0; i < varyHeaders.length(); i++) {
|
||||||
|
writer.write(varyHeaders.getFieldName(i) + ": " + varyHeaders.getValue(i) + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.write(responseHeaders.getStatusLine() + '\n');
|
||||||
|
writer.write(Integer.toString(responseHeaders.length()) + '\n');
|
||||||
|
for (int i = 0; i < responseHeaders.length(); i++) {
|
||||||
|
writer.write(responseHeaders.getFieldName(i) + ": " + responseHeaders.getValue(i) + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHttps()) {
|
||||||
|
writer.write('\n');
|
||||||
|
writer.write(cipherSuite + '\n');
|
||||||
|
writeCertArray(writer, peerCertificates);
|
||||||
|
writeCertArray(writer, localCertificates);
|
||||||
|
}
|
||||||
|
writer.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isHttps() {
|
||||||
|
return uri.startsWith("https://");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Certificate[] readCertArray(StrictLineReader reader) throws IOException {
|
||||||
|
int length = reader.readInt();
|
||||||
|
if (length == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
|
||||||
|
Certificate[] result = new Certificate[length];
|
||||||
|
for (int i = 0; i < result.length; i++) {
|
||||||
|
String line = reader.readLine();
|
||||||
|
byte[] bytes = Base64.decode(line.getBytes("US-ASCII"));
|
||||||
|
result[i] = certificateFactory.generateCertificate(new ByteArrayInputStream(bytes));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeCertArray(Writer writer, Certificate[] certificates) throws IOException {
|
||||||
|
if (certificates == null) {
|
||||||
|
writer.write("-1\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
writer.write(Integer.toString(certificates.length) + '\n');
|
||||||
|
for (Certificate certificate : certificates) {
|
||||||
|
byte[] bytes = certificate.getEncoded();
|
||||||
|
String line = Base64.encode(bytes);
|
||||||
|
writer.write(line + '\n');
|
||||||
|
}
|
||||||
|
} catch (CertificateEncodingException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean matches(URI uri, String requestMethod,
|
||||||
|
Map<String, List<String>> requestHeaders) {
|
||||||
|
return this.uri.equals(uri.toString())
|
||||||
|
&& this.requestMethod.equals(requestMethod)
|
||||||
|
&& new ResponseHeaders(uri, responseHeaders).varyMatches(varyHeaders.toMultimap(false),
|
||||||
|
requestHeaders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an input stream that reads the body of a snapshot, closing the
|
||||||
|
* snapshot when the stream is closed.
|
||||||
|
*/
|
||||||
|
private static InputStream newBodyInputStream(final DiskLruCache.Snapshot snapshot) {
|
||||||
|
return new FilterInputStream(snapshot.getInputStream(ENTRY_BODY)) {
|
||||||
|
@Override public void close() throws IOException {
|
||||||
|
snapshot.close();
|
||||||
|
super.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static class EntryCacheResponse extends CacheResponse {
|
||||||
|
private final Entry entry;
|
||||||
|
private final DiskLruCache.Snapshot snapshot;
|
||||||
|
private final InputStream in;
|
||||||
|
|
||||||
|
public EntryCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
|
||||||
|
this.entry = entry;
|
||||||
|
this.snapshot = snapshot;
|
||||||
|
this.in = newBodyInputStream(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public Map<String, List<String>> getHeaders() {
|
||||||
|
return entry.responseHeaders.toMultimap(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public InputStream getBody() {
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class EntrySecureCacheResponse extends SecureCacheResponse {
|
||||||
|
private final Entry entry;
|
||||||
|
private final DiskLruCache.Snapshot snapshot;
|
||||||
|
private final InputStream in;
|
||||||
|
|
||||||
|
public EntrySecureCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
|
||||||
|
this.entry = entry;
|
||||||
|
this.snapshot = snapshot;
|
||||||
|
this.in = newBodyInputStream(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public Map<String, List<String>> getHeaders() {
|
||||||
|
return entry.responseHeaders.toMultimap(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public InputStream getBody() {
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public String getCipherSuite() {
|
||||||
|
return entry.cipherSuite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public List<Certificate> getServerCertificateChain()
|
||||||
|
throws SSLPeerUnverifiedException {
|
||||||
|
if (entry.peerCertificates == null || entry.peerCertificates.length == 0) {
|
||||||
|
throw new SSLPeerUnverifiedException(null);
|
||||||
|
}
|
||||||
|
return Arrays.asList(entry.peerCertificates.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
|
||||||
|
if (entry.peerCertificates == null || entry.peerCertificates.length == 0) {
|
||||||
|
throw new SSLPeerUnverifiedException(null);
|
||||||
|
}
|
||||||
|
return ((X509Certificate) entry.peerCertificates[0]).getSubjectX500Principal();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public List<Certificate> getLocalCertificateChain() {
|
||||||
|
if (entry.localCertificates == null || entry.localCertificates.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Arrays.asList(entry.localCertificates.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public Principal getLocalPrincipal() {
|
||||||
|
if (entry.localCertificates == null || entry.localCertificates.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ((X509Certificate) entry.localCertificates[0]).getSubjectX500Principal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,485 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2012 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp.internal.http;
|
||||||
|
|
||||||
|
import com.squareup.okhttp.Connection;
|
||||||
|
import com.squareup.okhttp.internal.AbstractOutputStream;
|
||||||
|
import com.squareup.okhttp.internal.Util;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.CacheRequest;
|
||||||
|
import java.net.ProtocolException;
|
||||||
|
import java.net.Socket;
|
||||||
|
|
||||||
|
import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
|
||||||
|
|
||||||
|
public final class HttpTransport implements Transport {
|
||||||
|
/**
|
||||||
|
* The timeout to use while discarding a stream of input data. Since this is
|
||||||
|
* used for connection reuse, this timeout should be significantly less than
|
||||||
|
* the time it takes to establish a new connection.
|
||||||
|
*/
|
||||||
|
private static final int DISCARD_STREAM_TIMEOUT_MILLIS = 100;
|
||||||
|
|
||||||
|
public static final int DEFAULT_CHUNK_LENGTH = 1024;
|
||||||
|
|
||||||
|
private final HttpEngine httpEngine;
|
||||||
|
private final InputStream socketIn;
|
||||||
|
private final OutputStream socketOut;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This stream buffers the request headers and the request body when their
|
||||||
|
* combined size is less than MAX_REQUEST_BUFFER_LENGTH. By combining them
|
||||||
|
* we can save socket writes, which in turn saves a packet transmission.
|
||||||
|
* This is socketOut if the request size is large or unknown.
|
||||||
|
*/
|
||||||
|
private OutputStream requestOut;
|
||||||
|
|
||||||
|
public HttpTransport(HttpEngine httpEngine, OutputStream outputStream, InputStream inputStream) {
|
||||||
|
this.httpEngine = httpEngine;
|
||||||
|
this.socketOut = outputStream;
|
||||||
|
this.requestOut = outputStream;
|
||||||
|
this.socketIn = inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public OutputStream createRequestBody() throws IOException {
|
||||||
|
boolean chunked = httpEngine.requestHeaders.isChunked();
|
||||||
|
if (!chunked
|
||||||
|
&& httpEngine.policy.getChunkLength() > 0
|
||||||
|
&& httpEngine.connection.getHttpMinorVersion() != 0) {
|
||||||
|
httpEngine.requestHeaders.setChunked();
|
||||||
|
chunked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream a request body of unknown length.
|
||||||
|
if (chunked) {
|
||||||
|
int chunkLength = httpEngine.policy.getChunkLength();
|
||||||
|
if (chunkLength == -1) {
|
||||||
|
chunkLength = DEFAULT_CHUNK_LENGTH;
|
||||||
|
}
|
||||||
|
writeRequestHeaders();
|
||||||
|
return new ChunkedOutputStream(requestOut, chunkLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream a request body of a known length.
|
||||||
|
int fixedContentLength = httpEngine.policy.getFixedContentLength();
|
||||||
|
if (fixedContentLength != -1) {
|
||||||
|
httpEngine.requestHeaders.setContentLength(fixedContentLength);
|
||||||
|
writeRequestHeaders();
|
||||||
|
return new FixedLengthOutputStream(requestOut, fixedContentLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer a request body of a known length.
|
||||||
|
int contentLength = httpEngine.requestHeaders.getContentLength();
|
||||||
|
if (contentLength != -1) {
|
||||||
|
writeRequestHeaders();
|
||||||
|
return new RetryableOutputStream(contentLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer a request body of an unknown length. Don't write request
|
||||||
|
// headers until the entire body is ready; otherwise we can't set the
|
||||||
|
// Content-Length header correctly.
|
||||||
|
return new RetryableOutputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void flushRequest() throws IOException {
|
||||||
|
requestOut.flush();
|
||||||
|
requestOut = socketOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException {
|
||||||
|
requestBody.writeToSocket(requestOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares the HTTP headers and sends them to the server.
|
||||||
|
*
|
||||||
|
* <p>For streaming requests with a body, headers must be prepared
|
||||||
|
* <strong>before</strong> the output stream has been written to. Otherwise
|
||||||
|
* the body would need to be buffered!
|
||||||
|
*
|
||||||
|
* <p>For non-streaming requests with a body, headers must be prepared
|
||||||
|
* <strong>after</strong> the output stream has been written to and closed.
|
||||||
|
* This ensures that the {@code Content-Length} header field receives the
|
||||||
|
* proper value.
|
||||||
|
*/
|
||||||
|
public void writeRequestHeaders() throws IOException {
|
||||||
|
httpEngine.writingRequestHeaders();
|
||||||
|
RawHeaders headersToSend = httpEngine.requestHeaders.getHeaders();
|
||||||
|
byte[] bytes = headersToSend.toBytes();
|
||||||
|
requestOut.write(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public ResponseHeaders readResponseHeaders() throws IOException {
|
||||||
|
RawHeaders headers = RawHeaders.fromBytes(socketIn);
|
||||||
|
httpEngine.connection.setHttpMinorVersion(headers.getHttpMinorVersion());
|
||||||
|
httpEngine.receiveHeaders(headers);
|
||||||
|
return new ResponseHeaders(httpEngine.uri, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean makeReusable(boolean streamCancelled, OutputStream requestBodyOut,
|
||||||
|
InputStream responseBodyIn) {
|
||||||
|
if (streamCancelled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We cannot reuse sockets that have incomplete output.
|
||||||
|
if (requestBodyOut != null && !((AbstractOutputStream) requestBodyOut).isClosed()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the request specified that the connection shouldn't be reused, don't reuse it.
|
||||||
|
if (httpEngine.requestHeaders.hasConnectionClose()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the response specified that the connection shouldn't be reused, don't reuse it.
|
||||||
|
if (httpEngine.responseHeaders != null && httpEngine.responseHeaders.hasConnectionClose()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseBodyIn instanceof UnknownLengthHttpInputStream) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseBodyIn != null) {
|
||||||
|
return discardStream(httpEngine, responseBodyIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discards the response body so that the connection can be reused. This
|
||||||
|
* needs to be done judiciously, since it delays the current request in
|
||||||
|
* order to speed up a potential future request that may never occur.
|
||||||
|
*/
|
||||||
|
private static boolean discardStream(HttpEngine httpEngine, InputStream responseBodyIn) {
|
||||||
|
Connection connection = httpEngine.connection;
|
||||||
|
if (connection == null) return false;
|
||||||
|
Socket socket = connection.getSocket();
|
||||||
|
if (socket == null) return false;
|
||||||
|
try {
|
||||||
|
int socketTimeout = socket.getSoTimeout();
|
||||||
|
socket.setSoTimeout(DISCARD_STREAM_TIMEOUT_MILLIS);
|
||||||
|
try {
|
||||||
|
Util.skipAll(responseBodyIn);
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
socket.setSoTimeout(socketTimeout);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {
|
||||||
|
if (!httpEngine.hasResponseBody()) {
|
||||||
|
return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (httpEngine.responseHeaders.isChunked()) {
|
||||||
|
return new ChunkedInputStream(socketIn, cacheRequest, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (httpEngine.responseHeaders.getContentLength() != -1) {
|
||||||
|
return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine,
|
||||||
|
httpEngine.responseHeaders.getContentLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap the input stream from the connection (rather than just returning
|
||||||
|
// "socketIn" directly here), so that we can control its use after the
|
||||||
|
// reference escapes.
|
||||||
|
return new UnknownLengthHttpInputStream(socketIn, cacheRequest, httpEngine);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An HTTP body with a fixed length known in advance. */
|
||||||
|
private static final class FixedLengthOutputStream extends AbstractOutputStream {
|
||||||
|
private final OutputStream socketOut;
|
||||||
|
private int bytesRemaining;
|
||||||
|
|
||||||
|
private FixedLengthOutputStream(OutputStream socketOut, int bytesRemaining) {
|
||||||
|
this.socketOut = socketOut;
|
||||||
|
this.bytesRemaining = bytesRemaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void write(byte[] buffer, int offset, int count) throws IOException {
|
||||||
|
checkNotClosed();
|
||||||
|
checkOffsetAndCount(buffer.length, offset, count);
|
||||||
|
if (count > bytesRemaining) {
|
||||||
|
throw new ProtocolException("expected " + bytesRemaining + " bytes but received " + count);
|
||||||
|
}
|
||||||
|
socketOut.write(buffer, offset, count);
|
||||||
|
bytesRemaining -= count;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void flush() throws IOException {
|
||||||
|
if (closed) {
|
||||||
|
return; // don't throw; this stream might have been closed on the caller's behalf
|
||||||
|
}
|
||||||
|
socketOut.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void close() throws IOException {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closed = true;
|
||||||
|
if (bytesRemaining > 0) {
|
||||||
|
throw new ProtocolException("unexpected end of stream");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An HTTP body with alternating chunk sizes and chunk bodies. Chunks are
|
||||||
|
* buffered until {@code maxChunkLength} bytes are ready, at which point the
|
||||||
|
* chunk is written and the buffer is cleared.
|
||||||
|
*/
|
||||||
|
private static final class ChunkedOutputStream extends AbstractOutputStream {
|
||||||
|
private static final byte[] CRLF = { '\r', '\n' };
|
||||||
|
private static final byte[] HEX_DIGITS = {
|
||||||
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
|
||||||
|
};
|
||||||
|
private static final byte[] FINAL_CHUNK = new byte[] { '0', '\r', '\n', '\r', '\n' };
|
||||||
|
|
||||||
|
/** Scratch space for up to 8 hex digits, and then a constant CRLF. */
|
||||||
|
private final byte[] hex = { 0, 0, 0, 0, 0, 0, 0, 0, '\r', '\n' };
|
||||||
|
|
||||||
|
private final OutputStream socketOut;
|
||||||
|
private final int maxChunkLength;
|
||||||
|
private final ByteArrayOutputStream bufferedChunk;
|
||||||
|
|
||||||
|
private ChunkedOutputStream(OutputStream socketOut, int maxChunkLength) {
|
||||||
|
this.socketOut = socketOut;
|
||||||
|
this.maxChunkLength = Math.max(1, dataLength(maxChunkLength));
|
||||||
|
this.bufferedChunk = new ByteArrayOutputStream(maxChunkLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the amount of data that can be transmitted in a chunk whose total
|
||||||
|
* length (data+headers) is {@code dataPlusHeaderLength}. This is presumably
|
||||||
|
* useful to match sizes with wire-protocol packets.
|
||||||
|
*/
|
||||||
|
private int dataLength(int dataPlusHeaderLength) {
|
||||||
|
int headerLength = 4; // "\r\n" after the size plus another "\r\n" after the data
|
||||||
|
for (int i = dataPlusHeaderLength - headerLength; i > 0; i >>= 4) {
|
||||||
|
headerLength++;
|
||||||
|
}
|
||||||
|
return dataPlusHeaderLength - headerLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public synchronized void write(byte[] buffer, int offset, int count)
|
||||||
|
throws IOException {
|
||||||
|
checkNotClosed();
|
||||||
|
checkOffsetAndCount(buffer.length, offset, count);
|
||||||
|
|
||||||
|
while (count > 0) {
|
||||||
|
int numBytesWritten;
|
||||||
|
|
||||||
|
if (bufferedChunk.size() > 0 || count < maxChunkLength) {
|
||||||
|
// fill the buffered chunk and then maybe write that to the stream
|
||||||
|
numBytesWritten = Math.min(count, maxChunkLength - bufferedChunk.size());
|
||||||
|
// TODO: skip unnecessary copies from buffer->bufferedChunk?
|
||||||
|
bufferedChunk.write(buffer, offset, numBytesWritten);
|
||||||
|
if (bufferedChunk.size() == maxChunkLength) {
|
||||||
|
writeBufferedChunkToSocket();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// write a single chunk of size maxChunkLength to the stream
|
||||||
|
numBytesWritten = maxChunkLength;
|
||||||
|
writeHex(numBytesWritten);
|
||||||
|
socketOut.write(buffer, offset, numBytesWritten);
|
||||||
|
socketOut.write(CRLF);
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += numBytesWritten;
|
||||||
|
count -= numBytesWritten;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equivalent to, but cheaper than writing Integer.toHexString().getBytes()
|
||||||
|
* followed by CRLF.
|
||||||
|
*/
|
||||||
|
private void writeHex(int i) throws IOException {
|
||||||
|
int cursor = 8;
|
||||||
|
do {
|
||||||
|
hex[--cursor] = HEX_DIGITS[i & 0xf];
|
||||||
|
} while ((i >>>= 4) != 0);
|
||||||
|
socketOut.write(hex, cursor, hex.length - cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public synchronized void flush() throws IOException {
|
||||||
|
if (closed) {
|
||||||
|
return; // don't throw; this stream might have been closed on the caller's behalf
|
||||||
|
}
|
||||||
|
writeBufferedChunkToSocket();
|
||||||
|
socketOut.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public synchronized void close() throws IOException {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closed = true;
|
||||||
|
writeBufferedChunkToSocket();
|
||||||
|
socketOut.write(FINAL_CHUNK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeBufferedChunkToSocket() throws IOException {
|
||||||
|
int size = bufferedChunk.size();
|
||||||
|
if (size <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeHex(size);
|
||||||
|
bufferedChunk.writeTo(socketOut);
|
||||||
|
bufferedChunk.reset();
|
||||||
|
socketOut.write(CRLF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An HTTP body with a fixed length specified in advance. */
|
||||||
|
private static class FixedLengthInputStream extends AbstractHttpInputStream {
|
||||||
|
private int bytesRemaining;
|
||||||
|
|
||||||
|
public FixedLengthInputStream(InputStream is, CacheRequest cacheRequest, HttpEngine httpEngine,
|
||||||
|
int length) throws IOException {
|
||||||
|
super(is, httpEngine, cacheRequest);
|
||||||
|
bytesRemaining = length;
|
||||||
|
if (bytesRemaining == 0) {
|
||||||
|
endOfInput(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public int read(byte[] buffer, int offset, int count) throws IOException {
|
||||||
|
checkOffsetAndCount(buffer.length, offset, count);
|
||||||
|
checkNotClosed();
|
||||||
|
if (bytesRemaining == 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int read = in.read(buffer, offset, Math.min(count, bytesRemaining));
|
||||||
|
if (read == -1) {
|
||||||
|
unexpectedEndOfInput(); // the server didn't supply the promised content length
|
||||||
|
throw new ProtocolException("unexpected end of stream");
|
||||||
|
}
|
||||||
|
bytesRemaining -= read;
|
||||||
|
cacheWrite(buffer, offset, read);
|
||||||
|
if (bytesRemaining == 0) {
|
||||||
|
endOfInput(false);
|
||||||
|
}
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public int available() throws IOException {
|
||||||
|
checkNotClosed();
|
||||||
|
return bytesRemaining == 0 ? 0 : Math.min(in.available(), bytesRemaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void close() throws IOException {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bytesRemaining != 0 && !discardStream(httpEngine, this)) {
|
||||||
|
unexpectedEndOfInput();
|
||||||
|
}
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An HTTP body with alternating chunk sizes and chunk bodies. */
|
||||||
|
private static class ChunkedInputStream extends AbstractHttpInputStream {
|
||||||
|
private static final int NO_CHUNK_YET = -1;
|
||||||
|
private final HttpTransport transport;
|
||||||
|
private int bytesRemainingInChunk = NO_CHUNK_YET;
|
||||||
|
private boolean hasMoreChunks = true;
|
||||||
|
|
||||||
|
ChunkedInputStream(InputStream is, CacheRequest cacheRequest, HttpTransport transport)
|
||||||
|
throws IOException {
|
||||||
|
super(is, transport.httpEngine, cacheRequest);
|
||||||
|
this.transport = transport;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public int read(byte[] buffer, int offset, int count) throws IOException {
|
||||||
|
checkOffsetAndCount(buffer.length, offset, count);
|
||||||
|
checkNotClosed();
|
||||||
|
|
||||||
|
if (!hasMoreChunks) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (bytesRemainingInChunk == 0 || bytesRemainingInChunk == NO_CHUNK_YET) {
|
||||||
|
readChunkSize();
|
||||||
|
if (!hasMoreChunks) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int read = in.read(buffer, offset, Math.min(count, bytesRemainingInChunk));
|
||||||
|
if (read == -1) {
|
||||||
|
unexpectedEndOfInput(); // the server didn't supply the promised chunk length
|
||||||
|
throw new IOException("unexpected end of stream");
|
||||||
|
}
|
||||||
|
bytesRemainingInChunk -= read;
|
||||||
|
cacheWrite(buffer, offset, read);
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readChunkSize() throws IOException {
|
||||||
|
// read the suffix of the previous chunk
|
||||||
|
if (bytesRemainingInChunk != NO_CHUNK_YET) {
|
||||||
|
Util.readAsciiLine(in);
|
||||||
|
}
|
||||||
|
String chunkSizeString = Util.readAsciiLine(in);
|
||||||
|
int index = chunkSizeString.indexOf(";");
|
||||||
|
if (index != -1) {
|
||||||
|
chunkSizeString = chunkSizeString.substring(0, index);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
bytesRemainingInChunk = Integer.parseInt(chunkSizeString.trim(), 16);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new ProtocolException("Expected a hex chunk size but was " + chunkSizeString);
|
||||||
|
}
|
||||||
|
if (bytesRemainingInChunk == 0) {
|
||||||
|
hasMoreChunks = false;
|
||||||
|
RawHeaders rawResponseHeaders = httpEngine.responseHeaders.getHeaders();
|
||||||
|
RawHeaders.readHeaders(transport.socketIn, rawResponseHeaders);
|
||||||
|
httpEngine.receiveHeaders(rawResponseHeaders);
|
||||||
|
endOfInput(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public int available() throws IOException {
|
||||||
|
checkNotClosed();
|
||||||
|
if (!hasMoreChunks || bytesRemainingInChunk == NO_CHUNK_YET) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Math.min(in.available(), bytesRemainingInChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void close() throws IOException {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hasMoreChunks && !discardStream(httpEngine, this)) {
|
||||||
|
unexpectedEndOfInput();
|
||||||
|
}
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,556 @@
|
|||||||
|
/*
|
||||||
|
* 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.squareup.okhttp.internal.http;
|
||||||
|
|
||||||
|
import com.squareup.okhttp.Connection;
|
||||||
|
import com.squareup.okhttp.ConnectionPool;
|
||||||
|
import com.squareup.okhttp.OkHttpClient;
|
||||||
|
import com.squareup.okhttp.Route;
|
||||||
|
import com.squareup.okhttp.internal.AbstractOutputStream;
|
||||||
|
import com.squareup.okhttp.internal.FaultRecoveringOutputStream;
|
||||||
|
import com.squareup.okhttp.internal.Util;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.CookieHandler;
|
||||||
|
import java.net.HttpRetryException;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.ProtocolException;
|
||||||
|
import java.net.Proxy;
|
||||||
|
import java.net.ProxySelector;
|
||||||
|
import java.net.SocketPermission;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.Permission;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import javax.net.ssl.HostnameVerifier;
|
||||||
|
import javax.net.ssl.SSLHandshakeException;
|
||||||
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
|
|
||||||
|
import static com.squareup.okhttp.internal.Util.getEffectivePort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation uses HttpEngine to send requests and receive responses.
|
||||||
|
* This class may use multiple HttpEngines to follow redirects, authentication
|
||||||
|
* retries, etc. to retrieve the final response body.
|
||||||
|
*
|
||||||
|
* <h3>What does 'connected' mean?</h3>
|
||||||
|
* This class inherits a {@code connected} field from the superclass. That field
|
||||||
|
* is <strong>not</strong> used to indicate not whether this URLConnection is
|
||||||
|
* currently connected. Instead, it indicates whether a connection has ever been
|
||||||
|
* attempted. Once a connection has been attempted, certain properties (request
|
||||||
|
* header fields, request method, etc.) are immutable. Test the {@code
|
||||||
|
* connection} field on this class for null/non-null to determine of an instance
|
||||||
|
* is currently connected to a server.
|
||||||
|
*/
|
||||||
|
public class HttpURLConnectionImpl extends HttpURLConnection {
|
||||||
|
|
||||||
|
/** Numeric status code, 307: Temporary Redirect. */
|
||||||
|
static final int HTTP_TEMP_REDIRECT = 307;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How many redirects should we follow? Chrome follows 21; Firefox, curl,
|
||||||
|
* and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
|
||||||
|
*/
|
||||||
|
private static final int MAX_REDIRECTS = 20;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimum number of request body bytes to transmit before we're willing
|
||||||
|
* to let a routine {@link IOException} bubble up to the user. This is used to
|
||||||
|
* size a buffer for data that will be replayed upon error.
|
||||||
|
*/
|
||||||
|
private static final int MAX_REPLAY_BUFFER_LENGTH = 8192;
|
||||||
|
|
||||||
|
private final boolean followProtocolRedirects;
|
||||||
|
|
||||||
|
/** The proxy requested by the client, or null for a proxy to be selected automatically. */
|
||||||
|
final Proxy requestedProxy;
|
||||||
|
|
||||||
|
final ProxySelector proxySelector;
|
||||||
|
final CookieHandler cookieHandler;
|
||||||
|
final OkResponseCache responseCache;
|
||||||
|
final ConnectionPool connectionPool;
|
||||||
|
/* SSL configuration; necessary for HTTP requests that get redirected to HTTPS. */
|
||||||
|
SSLSocketFactory sslSocketFactory;
|
||||||
|
HostnameVerifier hostnameVerifier;
|
||||||
|
final Set<Route> failedRoutes;
|
||||||
|
|
||||||
|
private final RawHeaders rawRequestHeaders = new RawHeaders();
|
||||||
|
|
||||||
|
private int redirectionCount;
|
||||||
|
private FaultRecoveringOutputStream faultRecoveringRequestBody;
|
||||||
|
|
||||||
|
protected IOException httpEngineFailure;
|
||||||
|
protected HttpEngine httpEngine;
|
||||||
|
|
||||||
|
public HttpURLConnectionImpl(URL url, OkHttpClient client, OkResponseCache responseCache,
|
||||||
|
Set<Route> failedRoutes) {
|
||||||
|
super(url);
|
||||||
|
this.followProtocolRedirects = client.getFollowProtocolRedirects();
|
||||||
|
this.failedRoutes = failedRoutes;
|
||||||
|
this.requestedProxy = client.getProxy();
|
||||||
|
this.proxySelector = client.getProxySelector();
|
||||||
|
this.cookieHandler = client.getCookieHandler();
|
||||||
|
this.connectionPool = client.getConnectionPool();
|
||||||
|
this.sslSocketFactory = client.getSslSocketFactory();
|
||||||
|
this.hostnameVerifier = client.getHostnameVerifier();
|
||||||
|
this.responseCache = responseCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Route> getFailedRoutes() {
|
||||||
|
return failedRoutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public final void connect() throws IOException {
|
||||||
|
initHttpEngine();
|
||||||
|
boolean success;
|
||||||
|
do {
|
||||||
|
success = execute(false);
|
||||||
|
} while (!success);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public final void disconnect() {
|
||||||
|
// Calling disconnect() before a connection exists should have no effect.
|
||||||
|
if (httpEngine != null) {
|
||||||
|
// We close the response body here instead of in
|
||||||
|
// HttpEngine.release because that is called when input
|
||||||
|
// has been completely read from the underlying socket.
|
||||||
|
// However the response body can be a GZIPInputStream that
|
||||||
|
// still has unread data.
|
||||||
|
if (httpEngine.hasResponse()) {
|
||||||
|
Util.closeQuietly(httpEngine.getResponseBody());
|
||||||
|
}
|
||||||
|
httpEngine.release(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an input stream from the server in the case of error such as the
|
||||||
|
* requested file (txt, htm, html) is not found on the remote server.
|
||||||
|
*/
|
||||||
|
@Override public final InputStream getErrorStream() {
|
||||||
|
try {
|
||||||
|
HttpEngine response = getResponse();
|
||||||
|
if (response.hasResponseBody() && response.getResponseCode() >= HTTP_BAD_REQUEST) {
|
||||||
|
return response.getResponseBody();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of the field at {@code position}. Returns null if there
|
||||||
|
* are fewer than {@code position} headers.
|
||||||
|
*/
|
||||||
|
@Override public final String getHeaderField(int position) {
|
||||||
|
try {
|
||||||
|
return getResponse().getResponseHeaders().getHeaders().getValue(position);
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of the field corresponding to the {@code fieldName}, or
|
||||||
|
* null if there is no such field. If the field has multiple values, the
|
||||||
|
* last value is returned.
|
||||||
|
*/
|
||||||
|
@Override public final String getHeaderField(String fieldName) {
|
||||||
|
try {
|
||||||
|
RawHeaders rawHeaders = getResponse().getResponseHeaders().getHeaders();
|
||||||
|
return fieldName == null ? rawHeaders.getStatusLine() : rawHeaders.get(fieldName);
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public final String getHeaderFieldKey(int position) {
|
||||||
|
try {
|
||||||
|
return getResponse().getResponseHeaders().getHeaders().getFieldName(position);
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public final Map<String, List<String>> getHeaderFields() {
|
||||||
|
try {
|
||||||
|
return getResponse().getResponseHeaders().getHeaders().toMultimap(true);
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public final Map<String, List<String>> getRequestProperties() {
|
||||||
|
if (connected) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Cannot access request header fields after connection is set");
|
||||||
|
}
|
||||||
|
return rawRequestHeaders.toMultimap(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public final InputStream getInputStream() throws IOException {
|
||||||
|
if (!doInput) {
|
||||||
|
throw new ProtocolException("This protocol does not support input");
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpEngine response = getResponse();
|
||||||
|
|
||||||
|
// if the requested file does not exist, throw an exception formerly the
|
||||||
|
// Error page from the server was returned if the requested file was
|
||||||
|
// text/html this has changed to return FileNotFoundException for all
|
||||||
|
// file types
|
||||||
|
if (getResponseCode() >= HTTP_BAD_REQUEST) {
|
||||||
|
throw new FileNotFoundException(url.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStream result = response.getResponseBody();
|
||||||
|
if (result == null) {
|
||||||
|
throw new ProtocolException("No response body exists; responseCode=" + getResponseCode());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public final OutputStream getOutputStream() throws IOException {
|
||||||
|
connect();
|
||||||
|
|
||||||
|
OutputStream out = httpEngine.getRequestBody();
|
||||||
|
if (out == null) {
|
||||||
|
throw new ProtocolException("method does not support a request body: " + method);
|
||||||
|
} else if (httpEngine.hasResponse()) {
|
||||||
|
throw new ProtocolException("cannot write request body after response has been read");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faultRecoveringRequestBody == null) {
|
||||||
|
faultRecoveringRequestBody = new FaultRecoveringOutputStream(MAX_REPLAY_BUFFER_LENGTH, out) {
|
||||||
|
@Override protected OutputStream replacementStream(IOException e) throws IOException {
|
||||||
|
if (httpEngine.getRequestBody() instanceof AbstractOutputStream
|
||||||
|
&& ((AbstractOutputStream) httpEngine.getRequestBody()).isClosed()) {
|
||||||
|
return null; // Don't recover once the underlying stream has been closed.
|
||||||
|
}
|
||||||
|
if (handleFailure(e)) {
|
||||||
|
return httpEngine.getRequestBody();
|
||||||
|
}
|
||||||
|
return null; // This is a permanent failure.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return faultRecoveringRequestBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public final Permission getPermission() throws IOException {
|
||||||
|
String hostName = getURL().getHost();
|
||||||
|
int hostPort = Util.getEffectivePort(getURL());
|
||||||
|
if (usingProxy()) {
|
||||||
|
InetSocketAddress proxyAddress = (InetSocketAddress) requestedProxy.address();
|
||||||
|
hostName = proxyAddress.getHostName();
|
||||||
|
hostPort = proxyAddress.getPort();
|
||||||
|
}
|
||||||
|
return new SocketPermission(hostName + ":" + hostPort, "connect, resolve");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public final String getRequestProperty(String field) {
|
||||||
|
if (field == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return rawRequestHeaders.get(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initHttpEngine() throws IOException {
|
||||||
|
if (httpEngineFailure != null) {
|
||||||
|
throw httpEngineFailure;
|
||||||
|
} else if (httpEngine != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connected = true;
|
||||||
|
try {
|
||||||
|
if (doOutput) {
|
||||||
|
if (method.equals("GET")) {
|
||||||
|
// they are requesting a stream to write to. This implies a POST method
|
||||||
|
method = "POST";
|
||||||
|
} else if (!method.equals("POST") && !method.equals("PUT")) {
|
||||||
|
// If the request method is neither POST nor PUT, then you're not writing
|
||||||
|
throw new ProtocolException(method + " does not support writing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
httpEngine = newHttpEngine(method, rawRequestHeaders, null, null);
|
||||||
|
} catch (IOException e) {
|
||||||
|
httpEngineFailure = e;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected HttpURLConnection getHttpConnectionToCache() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpEngine newHttpEngine(String method, RawHeaders requestHeaders,
|
||||||
|
Connection connection, RetryableOutputStream requestBody) throws IOException {
|
||||||
|
if (url.getProtocol().equals("http")) {
|
||||||
|
return new HttpEngine(this, method, requestHeaders, connection, requestBody);
|
||||||
|
} else if (url.getProtocol().equals("https")) {
|
||||||
|
return new HttpsURLConnectionImpl.HttpsEngine(
|
||||||
|
this, method, requestHeaders, connection, requestBody);
|
||||||
|
} else {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggressively tries to get the final HTTP response, potentially making
|
||||||
|
* many HTTP requests in the process in order to cope with redirects and
|
||||||
|
* authentication.
|
||||||
|
*/
|
||||||
|
private HttpEngine getResponse() throws IOException {
|
||||||
|
initHttpEngine();
|
||||||
|
|
||||||
|
if (httpEngine.hasResponse()) {
|
||||||
|
return httpEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (!execute(true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Retry retry = processResponseHeaders();
|
||||||
|
if (retry == Retry.NONE) {
|
||||||
|
httpEngine.automaticallyReleaseConnectionToPool();
|
||||||
|
return httpEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first request was insufficient. Prepare for another...
|
||||||
|
String retryMethod = method;
|
||||||
|
OutputStream requestBody = httpEngine.getRequestBody();
|
||||||
|
|
||||||
|
// Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM
|
||||||
|
// redirect should keep the same method, Chrome, Firefox and the
|
||||||
|
// RI all issue GETs when following any redirect.
|
||||||
|
int responseCode = getResponseCode();
|
||||||
|
if (responseCode == HTTP_MULT_CHOICE
|
||||||
|
|| responseCode == HTTP_MOVED_PERM
|
||||||
|
|| responseCode == HTTP_MOVED_TEMP
|
||||||
|
|| responseCode == HTTP_SEE_OTHER) {
|
||||||
|
retryMethod = "GET";
|
||||||
|
requestBody = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) {
|
||||||
|
throw new HttpRetryException("Cannot retry streamed HTTP body",
|
||||||
|
httpEngine.getResponseCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retry == Retry.DIFFERENT_CONNECTION) {
|
||||||
|
httpEngine.automaticallyReleaseConnectionToPool();
|
||||||
|
}
|
||||||
|
|
||||||
|
httpEngine.release(false);
|
||||||
|
|
||||||
|
httpEngine = newHttpEngine(retryMethod, rawRequestHeaders, httpEngine.getConnection(),
|
||||||
|
(RetryableOutputStream) requestBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a request and optionally reads a response. Returns true if the
|
||||||
|
* request was successfully executed, and false if the request can be
|
||||||
|
* retried. Throws an exception if the request failed permanently.
|
||||||
|
*/
|
||||||
|
private boolean execute(boolean readResponse) throws IOException {
|
||||||
|
try {
|
||||||
|
httpEngine.sendRequest();
|
||||||
|
if (readResponse) {
|
||||||
|
httpEngine.readResponse();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (handleFailure(e)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report and attempt to recover from {@code e}. Returns true if the HTTP
|
||||||
|
* engine was replaced and the request should be retried. Otherwise the
|
||||||
|
* failure is permanent.
|
||||||
|
*/
|
||||||
|
private boolean handleFailure(IOException e) throws IOException {
|
||||||
|
RouteSelector routeSelector = httpEngine.routeSelector;
|
||||||
|
if (routeSelector != null && httpEngine.connection != null) {
|
||||||
|
routeSelector.connectFailed(httpEngine.connection, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
OutputStream requestBody = httpEngine.getRequestBody();
|
||||||
|
boolean canRetryRequestBody = requestBody == null
|
||||||
|
|| requestBody instanceof RetryableOutputStream
|
||||||
|
|| (faultRecoveringRequestBody != null && faultRecoveringRequestBody.isRecoverable());
|
||||||
|
if (routeSelector == null && httpEngine.connection == null // No connection.
|
||||||
|
|| routeSelector != null && !routeSelector.hasNext() // No more routes to attempt.
|
||||||
|
|| !isRecoverable(e)
|
||||||
|
|| !canRetryRequestBody) {
|
||||||
|
httpEngineFailure = e;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpEngine.release(true);
|
||||||
|
RetryableOutputStream retryableOutputStream = requestBody instanceof RetryableOutputStream
|
||||||
|
? (RetryableOutputStream) requestBody
|
||||||
|
: null;
|
||||||
|
httpEngine = newHttpEngine(method, rawRequestHeaders, null, retryableOutputStream);
|
||||||
|
httpEngine.routeSelector = routeSelector; // Keep the same routeSelector.
|
||||||
|
if (faultRecoveringRequestBody != null && faultRecoveringRequestBody.isRecoverable()) {
|
||||||
|
httpEngine.sendRequest();
|
||||||
|
faultRecoveringRequestBody.replaceStream(httpEngine.getRequestBody());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRecoverable(IOException e) {
|
||||||
|
// If the problem was a CertificateException from the X509TrustManager,
|
||||||
|
// do not retry, we didn't have an abrupt server initiated exception.
|
||||||
|
boolean sslFailure =
|
||||||
|
e instanceof SSLHandshakeException && e.getCause() instanceof CertificateException;
|
||||||
|
boolean protocolFailure = e instanceof ProtocolException;
|
||||||
|
return !sslFailure && !protocolFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpEngine getHttpEngine() {
|
||||||
|
return httpEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Retry {
|
||||||
|
NONE,
|
||||||
|
SAME_CONNECTION,
|
||||||
|
DIFFERENT_CONNECTION
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the retry action to take for the current response headers. The
|
||||||
|
* headers, proxy and target URL or this connection may be adjusted to
|
||||||
|
* prepare for a follow up request.
|
||||||
|
*/
|
||||||
|
private Retry processResponseHeaders() throws IOException {
|
||||||
|
Proxy selectedProxy = httpEngine.connection != null
|
||||||
|
? httpEngine.connection.getRoute().getProxy()
|
||||||
|
: requestedProxy;
|
||||||
|
final int responseCode = getResponseCode();
|
||||||
|
switch (responseCode) {
|
||||||
|
case HTTP_PROXY_AUTH:
|
||||||
|
if (selectedProxy.type() != Proxy.Type.HTTP) {
|
||||||
|
throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
|
||||||
|
}
|
||||||
|
// fall-through
|
||||||
|
case HTTP_UNAUTHORIZED:
|
||||||
|
boolean credentialsFound = HttpAuthenticator.processAuthHeader(getResponseCode(),
|
||||||
|
httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders, selectedProxy, url);
|
||||||
|
return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE;
|
||||||
|
|
||||||
|
case HTTP_MULT_CHOICE:
|
||||||
|
case HTTP_MOVED_PERM:
|
||||||
|
case HTTP_MOVED_TEMP:
|
||||||
|
case HTTP_SEE_OTHER:
|
||||||
|
case HTTP_TEMP_REDIRECT:
|
||||||
|
if (!getInstanceFollowRedirects()) {
|
||||||
|
return Retry.NONE;
|
||||||
|
}
|
||||||
|
if (++redirectionCount > MAX_REDIRECTS) {
|
||||||
|
throw new ProtocolException("Too many redirects: " + redirectionCount);
|
||||||
|
}
|
||||||
|
if (responseCode == HTTP_TEMP_REDIRECT && !method.equals("GET") && !method.equals("HEAD")) {
|
||||||
|
// "If the 307 status code is received in response to a request other than GET or HEAD,
|
||||||
|
// the user agent MUST NOT automatically redirect the request"
|
||||||
|
return Retry.NONE;
|
||||||
|
}
|
||||||
|
String location = getHeaderField("Location");
|
||||||
|
if (location == null) {
|
||||||
|
return Retry.NONE;
|
||||||
|
}
|
||||||
|
URL previousUrl = url;
|
||||||
|
url = new URL(previousUrl, location);
|
||||||
|
if (!url.getProtocol().equals("https") && !url.getProtocol().equals("http")) {
|
||||||
|
return Retry.NONE; // Don't follow redirects to unsupported protocols.
|
||||||
|
}
|
||||||
|
boolean sameProtocol = previousUrl.getProtocol().equals(url.getProtocol());
|
||||||
|
if (!sameProtocol && !followProtocolRedirects) {
|
||||||
|
return Retry.NONE; // This client doesn't follow redirects across protocols.
|
||||||
|
}
|
||||||
|
boolean sameHost = previousUrl.getHost().equals(url.getHost());
|
||||||
|
boolean samePort = getEffectivePort(previousUrl) == getEffectivePort(url);
|
||||||
|
if (sameHost && samePort && sameProtocol) {
|
||||||
|
return Retry.SAME_CONNECTION;
|
||||||
|
} else {
|
||||||
|
return Retry.DIFFERENT_CONNECTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return Retry.NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */
|
||||||
|
final int getFixedContentLength() {
|
||||||
|
return fixedContentLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @see java.net.HttpURLConnection#setChunkedStreamingMode(int) */
|
||||||
|
final int getChunkLength() {
|
||||||
|
return chunkLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public final boolean usingProxy() {
|
||||||
|
return (requestedProxy != null && requestedProxy.type() != Proxy.Type.DIRECT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public String getResponseMessage() throws IOException {
|
||||||
|
return getResponse().getResponseHeaders().getHeaders().getResponseMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public final int getResponseCode() throws IOException {
|
||||||
|
return getResponse().getResponseCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public final void setRequestProperty(String field, String newValue) {
|
||||||
|
if (connected) {
|
||||||
|
throw new IllegalStateException("Cannot set request property after connection is made");
|
||||||
|
}
|
||||||
|
if (field == null) {
|
||||||
|
throw new NullPointerException("field == null");
|
||||||
|
}
|
||||||
|
rawRequestHeaders.set(field, newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public final void addRequestProperty(String field, String value) {
|
||||||
|
if (connected) {
|
||||||
|
throw new IllegalStateException("Cannot add request property after connection is made");
|
||||||
|
}
|
||||||
|
if (field == null) {
|
||||||
|
throw new NullPointerException("field == null");
|
||||||
|
}
|
||||||
|
rawRequestHeaders.add(field, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,461 @@
|
|||||||
|
/*
|
||||||
|
* 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.squareup.okhttp.internal.http;
|
||||||
|
|
||||||
|
import com.squareup.okhttp.Connection;
|
||||||
|
import com.squareup.okhttp.OkHttpClient;
|
||||||
|
import com.squareup.okhttp.Route;
|
||||||
|
import com.squareup.okhttp.TunnelRequest;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.CacheResponse;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.ProtocolException;
|
||||||
|
import java.net.SecureCacheResponse;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.Permission;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import javax.net.ssl.HostnameVerifier;
|
||||||
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
|
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
|
|
||||||
|
import static com.squareup.okhttp.internal.Util.getEffectivePort;
|
||||||
|
|
||||||
|
public final class HttpsURLConnectionImpl extends HttpsURLConnection {
|
||||||
|
|
||||||
|
/** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl. */
|
||||||
|
private final HttpUrlConnectionDelegate delegate;
|
||||||
|
|
||||||
|
public HttpsURLConnectionImpl(URL url, OkHttpClient client, OkResponseCache responseCache,
|
||||||
|
Set<Route> failedRoutes) {
|
||||||
|
super(url);
|
||||||
|
delegate = new HttpUrlConnectionDelegate(url, client, responseCache, failedRoutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public String getCipherSuite() {
|
||||||
|
SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
|
||||||
|
if (cacheResponse != null) {
|
||||||
|
return cacheResponse.getCipherSuite();
|
||||||
|
}
|
||||||
|
SSLSocket sslSocket = getSslSocket();
|
||||||
|
if (sslSocket != null) {
|
||||||
|
return sslSocket.getSession().getCipherSuite();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public Certificate[] getLocalCertificates() {
|
||||||
|
SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
|
||||||
|
if (cacheResponse != null) {
|
||||||
|
List<Certificate> result = cacheResponse.getLocalCertificateChain();
|
||||||
|
return result != null ? result.toArray(new Certificate[result.size()]) : null;
|
||||||
|
}
|
||||||
|
SSLSocket sslSocket = getSslSocket();
|
||||||
|
if (sslSocket != null) {
|
||||||
|
return sslSocket.getSession().getLocalCertificates();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException {
|
||||||
|
SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
|
||||||
|
if (cacheResponse != null) {
|
||||||
|
List<Certificate> result = cacheResponse.getServerCertificateChain();
|
||||||
|
return result != null ? result.toArray(new Certificate[result.size()]) : null;
|
||||||
|
}
|
||||||
|
SSLSocket sslSocket = getSslSocket();
|
||||||
|
if (sslSocket != null) {
|
||||||
|
return sslSocket.getSession().getPeerCertificates();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
|
||||||
|
SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
|
||||||
|
if (cacheResponse != null) {
|
||||||
|
return cacheResponse.getPeerPrincipal();
|
||||||
|
}
|
||||||
|
SSLSocket sslSocket = getSslSocket();
|
||||||
|
if (sslSocket != null) {
|
||||||
|
return sslSocket.getSession().getPeerPrincipal();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public Principal getLocalPrincipal() {
|
||||||
|
SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
|
||||||
|
if (cacheResponse != null) {
|
||||||
|
return cacheResponse.getLocalPrincipal();
|
||||||
|
}
|
||||||
|
SSLSocket sslSocket = getSslSocket();
|
||||||
|
if (sslSocket != null) {
|
||||||
|
return sslSocket.getSession().getLocalPrincipal();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpEngine getHttpEngine() {
|
||||||
|
return delegate.getHttpEngine();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SSLSocket getSslSocket() {
|
||||||
|
if (delegate.httpEngine == null || delegate.httpEngine.sentRequestMillis == -1) {
|
||||||
|
throw new IllegalStateException("Connection has not yet been established");
|
||||||
|
}
|
||||||
|
return delegate.httpEngine instanceof HttpsEngine
|
||||||
|
? ((HttpsEngine) delegate.httpEngine).sslSocket
|
||||||
|
: null; // Not HTTPS! Probably an https:// to http:// redirect.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disconnect() {
|
||||||
|
delegate.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getErrorStream() {
|
||||||
|
return delegate.getErrorStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRequestMethod() {
|
||||||
|
return delegate.getRequestMethod();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getResponseCode() throws IOException {
|
||||||
|
return delegate.getResponseCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getResponseMessage() throws IOException {
|
||||||
|
return delegate.getResponseMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRequestMethod(String method) throws ProtocolException {
|
||||||
|
delegate.setRequestMethod(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean usingProxy() {
|
||||||
|
return delegate.usingProxy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getInstanceFollowRedirects() {
|
||||||
|
return delegate.getInstanceFollowRedirects();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setInstanceFollowRedirects(boolean followRedirects) {
|
||||||
|
delegate.setInstanceFollowRedirects(followRedirects);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connect() throws IOException {
|
||||||
|
connected = true;
|
||||||
|
delegate.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getAllowUserInteraction() {
|
||||||
|
return delegate.getAllowUserInteraction();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getContent() throws IOException {
|
||||||
|
return delegate.getContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") // Spec does not generify
|
||||||
|
@Override
|
||||||
|
public Object getContent(Class[] types) throws IOException {
|
||||||
|
return delegate.getContent(types);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getContentEncoding() {
|
||||||
|
return delegate.getContentEncoding();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getContentLength() {
|
||||||
|
return delegate.getContentLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getContentType() {
|
||||||
|
return delegate.getContentType();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getDate() {
|
||||||
|
return delegate.getDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getDefaultUseCaches() {
|
||||||
|
return delegate.getDefaultUseCaches();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getDoInput() {
|
||||||
|
return delegate.getDoInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getDoOutput() {
|
||||||
|
return delegate.getDoOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getExpiration() {
|
||||||
|
return delegate.getExpiration();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHeaderField(int pos) {
|
||||||
|
return delegate.getHeaderField(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, List<String>> getHeaderFields() {
|
||||||
|
return delegate.getHeaderFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, List<String>> getRequestProperties() {
|
||||||
|
return delegate.getRequestProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addRequestProperty(String field, String newValue) {
|
||||||
|
delegate.addRequestProperty(field, newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHeaderField(String key) {
|
||||||
|
return delegate.getHeaderField(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getHeaderFieldDate(String field, long defaultValue) {
|
||||||
|
return delegate.getHeaderFieldDate(field, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getHeaderFieldInt(String field, int defaultValue) {
|
||||||
|
return delegate.getHeaderFieldInt(field, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHeaderFieldKey(int position) {
|
||||||
|
return delegate.getHeaderFieldKey(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getIfModifiedSince() {
|
||||||
|
return delegate.getIfModifiedSince();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() throws IOException {
|
||||||
|
return delegate.getInputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getLastModified() {
|
||||||
|
return delegate.getLastModified();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputStream getOutputStream() throws IOException {
|
||||||
|
return delegate.getOutputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Permission getPermission() throws IOException {
|
||||||
|
return delegate.getPermission();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRequestProperty(String field) {
|
||||||
|
return delegate.getRequestProperty(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL getURL() {
|
||||||
|
return delegate.getURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getUseCaches() {
|
||||||
|
return delegate.getUseCaches();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAllowUserInteraction(boolean newValue) {
|
||||||
|
delegate.setAllowUserInteraction(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDefaultUseCaches(boolean newValue) {
|
||||||
|
delegate.setDefaultUseCaches(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDoInput(boolean newValue) {
|
||||||
|
delegate.setDoInput(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDoOutput(boolean newValue) {
|
||||||
|
delegate.setDoOutput(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setIfModifiedSince(long newValue) {
|
||||||
|
delegate.setIfModifiedSince(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRequestProperty(String field, String newValue) {
|
||||||
|
delegate.setRequestProperty(field, newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setUseCaches(boolean newValue) {
|
||||||
|
delegate.setUseCaches(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setConnectTimeout(int timeoutMillis) {
|
||||||
|
delegate.setConnectTimeout(timeoutMillis);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getConnectTimeout() {
|
||||||
|
return delegate.getConnectTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setReadTimeout(int timeoutMillis) {
|
||||||
|
delegate.setReadTimeout(timeoutMillis);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getReadTimeout() {
|
||||||
|
return delegate.getReadTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return delegate.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setFixedLengthStreamingMode(int contentLength) {
|
||||||
|
delegate.setFixedLengthStreamingMode(contentLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setChunkedStreamingMode(int chunkLength) {
|
||||||
|
delegate.setChunkedStreamingMode(chunkLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
|
||||||
|
delegate.hostnameVerifier = hostnameVerifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public HostnameVerifier getHostnameVerifier() {
|
||||||
|
return delegate.hostnameVerifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
|
||||||
|
delegate.sslSocketFactory = sslSocketFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public SSLSocketFactory getSSLSocketFactory() {
|
||||||
|
return delegate.sslSocketFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl {
|
||||||
|
private HttpUrlConnectionDelegate(URL url, OkHttpClient client, OkResponseCache responseCache,
|
||||||
|
Set<Route> failedRoutes) {
|
||||||
|
super(url, client, responseCache, failedRoutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected HttpURLConnection getHttpConnectionToCache() {
|
||||||
|
return HttpsURLConnectionImpl.this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SecureCacheResponse getSecureCacheResponse() {
|
||||||
|
return httpEngine instanceof HttpsEngine
|
||||||
|
? (SecureCacheResponse) httpEngine.getCacheResponse()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class HttpsEngine extends HttpEngine {
|
||||||
|
/**
|
||||||
|
* Stash of HttpsEngine.connection.socket to implement requests like
|
||||||
|
* {@link #getCipherSuite} even after the connection has been recycled.
|
||||||
|
*/
|
||||||
|
private SSLSocket sslSocket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param policy the HttpURLConnectionImpl with connection configuration
|
||||||
|
*/
|
||||||
|
public HttpsEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
|
||||||
|
Connection connection, RetryableOutputStream requestBody) throws IOException {
|
||||||
|
super(policy, method, requestHeaders, connection, requestBody);
|
||||||
|
this.sslSocket = connection != null ? (SSLSocket) connection.getSocket() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void connected(Connection connection) {
|
||||||
|
this.sslSocket = (SSLSocket) connection.getSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
|
||||||
|
return cacheResponse instanceof SecureCacheResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected boolean includeAuthorityInRequestLine() {
|
||||||
|
// Even if there is a proxy, it isn't involved. Always request just the file.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected TunnelRequest getTunnelConfig() {
|
||||||
|
String userAgent = requestHeaders.getUserAgent();
|
||||||
|
if (userAgent == null) {
|
||||||
|
userAgent = getDefaultUserAgent();
|
||||||
|
}
|
||||||
|
|
||||||
|
URL url = policy.getURL();
|
||||||
|
return new TunnelRequest(url.getHost(), getEffectivePort(url), userAgent,
|
||||||
|
requestHeaders.getProxyAuthorization());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2013 Square, Inc.
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp.internal.http;
|
||||||
|
|
||||||
|
import com.squareup.okhttp.ResponseSource;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.CacheRequest;
|
||||||
|
import java.net.CacheResponse;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An extended response cache API. Unlike {@link java.net.ResponseCache}, this
|
||||||
|
* interface supports conditional caching and statistics.
|
||||||
|
*
|
||||||
|
* <p>Along with the rest of the {@code internal} package, this is not a public
|
||||||
|
* API. Applications wishing to supply their own caches must use the more
|
||||||
|
* limited {@link java.net.ResponseCache} interface.
|
||||||
|
*/
|
||||||
|
public interface OkResponseCache {
|
||||||
|
CacheResponse get(URI uri, String requestMethod, Map<String, List<String>> requestHeaders)
|
||||||
|
throws IOException;
|
||||||
|
|
||||||
|
CacheRequest put(URI uri, URLConnection urlConnection) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a conditional request hit by updating the stored cache response
|
||||||
|
* with the headers from {@code httpConnection}. The cached response body is
|
||||||
|
* not updated. If the stored response has changed since {@code
|
||||||
|
* conditionalCacheHit} was returned, this does nothing.
|
||||||
|
*/
|
||||||
|
void update(CacheResponse conditionalCacheHit, HttpURLConnection connection) throws IOException;
|
||||||
|
|
||||||
|
/** Track an conditional GET that was satisfied by this cache. */
|
||||||
|
void trackConditionalCacheHit();
|
||||||
|
|
||||||
|
/** Track an HTTP response being satisfied by {@code source}. */
|
||||||
|
void trackResponse(ResponseSource source);
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2013 Square, Inc.
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp.internal.http;
|
||||||
|
|
||||||
|
import com.squareup.okhttp.ResponseSource;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.CacheRequest;
|
||||||
|
import java.net.CacheResponse;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.ResponseCache;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class OkResponseCacheAdapter implements OkResponseCache {
|
||||||
|
private final ResponseCache responseCache;
|
||||||
|
public OkResponseCacheAdapter(ResponseCache responseCache) {
|
||||||
|
this.responseCache = responseCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public CacheResponse get(URI uri, String requestMethod,
|
||||||
|
Map<String, List<String>> requestHeaders) throws IOException {
|
||||||
|
return responseCache.get(uri, requestMethod, requestHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
|
||||||
|
return responseCache.put(uri, urlConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void update(CacheResponse conditionalCacheHit, HttpURLConnection connection)
|
||||||
|
throws IOException {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void trackConditionalCacheHit() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void trackResponse(ResponseSource source) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
/*
|
||||||
|
* 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.squareup.okhttp.internal.http;
|
||||||
|
|
||||||
|
import com.squareup.okhttp.internal.Util;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.ProtocolException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The HTTP status and unparsed header fields of a single HTTP message. Values
|
||||||
|
* are represented as uninterpreted strings; use {@link RequestHeaders} and
|
||||||
|
* {@link ResponseHeaders} for interpreted headers. This class maintains the
|
||||||
|
* order of the header fields within the HTTP message.
|
||||||
|
*
|
||||||
|
* <p>This class tracks fields line-by-line. A field with multiple comma-
|
||||||
|
* separated values on the same line will be treated as a field with a single
|
||||||
|
* value by this class. It is the caller's responsibility to detect and split
|
||||||
|
* on commas if their field permits multiple values. This simplifies use of
|
||||||
|
* single-valued fields whose values routinely contain commas, such as cookies
|
||||||
|
* or dates.
|
||||||
|
*
|
||||||
|
* <p>This class trims whitespace from values. It never returns values with
|
||||||
|
* leading or trailing whitespace.
|
||||||
|
*/
|
||||||
|
public final class RawHeaders {
|
||||||
|
private static final Comparator<String> FIELD_NAME_COMPARATOR = new Comparator<String>() {
|
||||||
|
// @FindBugsSuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ")
|
||||||
|
@Override public int compare(String a, String b) {
|
||||||
|
if (a == b) {
|
||||||
|
return 0;
|
||||||
|
} else if (a == null) {
|
||||||
|
return -1;
|
||||||
|
} else if (b == null) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return String.CASE_INSENSITIVE_ORDER.compare(a, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final List<String> namesAndValues = new ArrayList<String>(20);
|
||||||
|
private String requestLine;
|
||||||
|
private String statusLine;
|
||||||
|
private int httpMinorVersion = 1;
|
||||||
|
private int responseCode = -1;
|
||||||
|
private String responseMessage;
|
||||||
|
|
||||||
|
public RawHeaders() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public RawHeaders(RawHeaders copyFrom) {
|
||||||
|
namesAndValues.addAll(copyFrom.namesAndValues);
|
||||||
|
requestLine = copyFrom.requestLine;
|
||||||
|
statusLine = copyFrom.statusLine;
|
||||||
|
httpMinorVersion = copyFrom.httpMinorVersion;
|
||||||
|
responseCode = copyFrom.responseCode;
|
||||||
|
responseMessage = copyFrom.responseMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the request line (like "GET / HTTP/1.1"). */
|
||||||
|
public void setRequestLine(String requestLine) {
|
||||||
|
requestLine = requestLine.trim();
|
||||||
|
this.requestLine = requestLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the response status line (like "HTTP/1.0 200 OK"). */
|
||||||
|
public void setStatusLine(String statusLine) throws IOException {
|
||||||
|
// H T T P / 1 . 1 2 0 0 T e m p o r a r y R e d i r e c t
|
||||||
|
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
|
||||||
|
if (this.responseMessage != null) {
|
||||||
|
throw new IllegalStateException("statusLine is already set");
|
||||||
|
}
|
||||||
|
// We allow empty message without leading white space since some servers
|
||||||
|
// do not send the white space when the message is empty.
|
||||||
|
boolean hasMessage = statusLine.length() > 13;
|
||||||
|
if (!statusLine.startsWith("HTTP/1.")
|
||||||
|
|| statusLine.length() < 12
|
||||||
|
|| statusLine.charAt(8) != ' '
|
||||||
|
|| (hasMessage && statusLine.charAt(12) != ' ')) {
|
||||||
|
throw new ProtocolException("Unexpected status line: " + statusLine);
|
||||||
|
}
|
||||||
|
int httpMinorVersion = statusLine.charAt(7) - '0';
|
||||||
|
if (httpMinorVersion < 0 || httpMinorVersion > 9) {
|
||||||
|
throw new ProtocolException("Unexpected status line: " + statusLine);
|
||||||
|
}
|
||||||
|
int responseCode;
|
||||||
|
try {
|
||||||
|
responseCode = Integer.parseInt(statusLine.substring(9, 12));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new ProtocolException("Unexpected status line: " + statusLine);
|
||||||
|
}
|
||||||
|
this.responseMessage = hasMessage ? statusLine.substring(13) : "";
|
||||||
|
this.responseCode = responseCode;
|
||||||
|
this.statusLine = statusLine;
|
||||||
|
this.httpMinorVersion = httpMinorVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void computeResponseStatusLineFromSpdyHeaders() throws IOException {
|
||||||
|
String status = null;
|
||||||
|
String version = null;
|
||||||
|
for (int i = 0; i < namesAndValues.size(); i += 2) {
|
||||||
|
String name = namesAndValues.get(i);
|
||||||
|
if (":status".equals(name)) {
|
||||||
|
status = namesAndValues.get(i + 1);
|
||||||
|
} else if (":version".equals(name)) {
|
||||||
|
version = namesAndValues.get(i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (status == null || version == null) {
|
||||||
|
throw new ProtocolException("Expected ':status' and ':version' headers not present");
|
||||||
|
}
|
||||||
|
setStatusLine(version + " " + status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param method like "GET", "POST", "HEAD", etc.
|
||||||
|
* @param path like "/foo/bar.html"
|
||||||
|
* @param version like "HTTP/1.1"
|
||||||
|
* @param host like "www.android.com:1234"
|
||||||
|
* @param scheme like "https"
|
||||||
|
*/
|
||||||
|
public void addSpdyRequestHeaders(String method, String path, String version, String host,
|
||||||
|
String scheme) {
|
||||||
|
// TODO: populate the statusLine for the client's benefit?
|
||||||
|
add(":method", method);
|
||||||
|
add(":scheme", scheme);
|
||||||
|
add(":path", path);
|
||||||
|
add(":version", version);
|
||||||
|
add(":host", host);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatusLine() {
|
||||||
|
return statusLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the status line's HTTP minor version. This returns 0 for HTTP/1.0
|
||||||
|
* and 1 for HTTP/1.1. This returns 1 if the HTTP version is unknown.
|
||||||
|
*/
|
||||||
|
public int getHttpMinorVersion() {
|
||||||
|
return httpMinorVersion != -1 ? httpMinorVersion : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the HTTP status code or -1 if it is unknown. */
|
||||||
|
public int getResponseCode() {
|
||||||
|
return responseCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the HTTP status message or null if it is unknown. */
|
||||||
|
public String getResponseMessage() {
|
||||||
|
return responseMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an HTTP header line containing a field name, a literal colon, and a
|
||||||
|
* value.
|
||||||
|
*/
|
||||||
|
public void addLine(String line) {
|
||||||
|
int index = line.indexOf(":");
|
||||||
|
if (index == -1) {
|
||||||
|
addLenient("", line);
|
||||||
|
} else {
|
||||||
|
addLenient(line.substring(0, index), line.substring(index + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add a field with the specified value. */
|
||||||
|
public void add(String fieldName, String value) {
|
||||||
|
if (fieldName == null) throw new IllegalArgumentException("fieldname == null");
|
||||||
|
if (value == null) throw new IllegalArgumentException("value == null");
|
||||||
|
if (fieldName.length() == 0 || fieldName.indexOf('\0') != -1 || value.indexOf('\0') != -1) {
|
||||||
|
throw new IllegalArgumentException("Unexpected header: " + fieldName + ": " + value);
|
||||||
|
}
|
||||||
|
addLenient(fieldName, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a field with the specified value without any validation. Only
|
||||||
|
* appropriate for headers from the remote peer.
|
||||||
|
*/
|
||||||
|
private void addLenient(String fieldName, String value) {
|
||||||
|
namesAndValues.add(fieldName);
|
||||||
|
namesAndValues.add(value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeAll(String fieldName) {
|
||||||
|
for (int i = 0; i < namesAndValues.size(); i += 2) {
|
||||||
|
if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
|
||||||
|
namesAndValues.remove(i); // field name
|
||||||
|
namesAndValues.remove(i); // value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAll(String fieldName, List<String> headerFields) {
|
||||||
|
for (String value : headerFields) {
|
||||||
|
add(fieldName, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a field with the specified value. If the field is not found, it is
|
||||||
|
* added. If the field is found, the existing values are replaced.
|
||||||
|
*/
|
||||||
|
public void set(String fieldName, String value) {
|
||||||
|
removeAll(fieldName);
|
||||||
|
add(fieldName, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the number of field values. */
|
||||||
|
public int length() {
|
||||||
|
return namesAndValues.size() / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the field at {@code position} or null if that is out of range. */
|
||||||
|
public String getFieldName(int index) {
|
||||||
|
int fieldNameIndex = index * 2;
|
||||||
|
if (fieldNameIndex < 0 || fieldNameIndex >= namesAndValues.size()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return namesAndValues.get(fieldNameIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the value at {@code index} or null if that is out of range. */
|
||||||
|
public String getValue(int index) {
|
||||||
|
int valueIndex = index * 2 + 1;
|
||||||
|
if (valueIndex < 0 || valueIndex >= namesAndValues.size()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return namesAndValues.get(valueIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the last value corresponding to the specified field, or null. */
|
||||||
|
public String get(String fieldName) {
|
||||||
|
for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) {
|
||||||
|
if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
|
||||||
|
return namesAndValues.get(i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param fieldNames a case-insensitive set of HTTP header field names. */
|
||||||
|
public RawHeaders getAll(Set<String> fieldNames) {
|
||||||
|
RawHeaders result = new RawHeaders();
|
||||||
|
for (int i = 0; i < namesAndValues.size(); i += 2) {
|
||||||
|
String fieldName = namesAndValues.get(i);
|
||||||
|
if (fieldNames.contains(fieldName)) {
|
||||||
|
result.add(fieldName, namesAndValues.get(i + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns bytes of a request header for sending on an HTTP transport. */
|
||||||
|
public byte[] toBytes() throws UnsupportedEncodingException {
|
||||||
|
StringBuilder result = new StringBuilder(256);
|
||||||
|
result.append(requestLine).append("\r\n");
|
||||||
|
for (int i = 0; i < namesAndValues.size(); i += 2) {
|
||||||
|
result.append(namesAndValues.get(i))
|
||||||
|
.append(": ")
|
||||||
|
.append(namesAndValues.get(i + 1))
|
||||||
|
.append("\r\n");
|
||||||
|
}
|
||||||
|
result.append("\r\n");
|
||||||
|
return result.toString().getBytes("ISO-8859-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parses bytes of a response header from an HTTP transport. */
|
||||||
|
public static RawHeaders fromBytes(InputStream in) throws IOException {
|
||||||
|
RawHeaders headers;
|
||||||
|
do {
|
||||||
|
headers = new RawHeaders();
|
||||||
|
headers.setStatusLine(Util.readAsciiLine(in));
|
||||||
|
readHeaders(in, headers);
|
||||||
|
} while (headers.getResponseCode() == HttpEngine.HTTP_CONTINUE);
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reads headers or trailers into {@code out}. */
|
||||||
|
public static void readHeaders(InputStream in, RawHeaders out) throws IOException {
|
||||||
|
// parse the result headers until the first blank line
|
||||||
|
String line;
|
||||||
|
while ((line = Util.readAsciiLine(in)).length() != 0) {
|
||||||
|
out.addLine(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an immutable map containing each field to its list of values. The
|
||||||
|
* status line is mapped to null.
|
||||||
|
*/
|
||||||
|
public Map<String, List<String>> toMultimap(boolean response) {
|
||||||
|
Map<String, List<String>> result = new TreeMap<String, List<String>>(FIELD_NAME_COMPARATOR);
|
||||||
|
for (int i = 0; i < namesAndValues.size(); i += 2) {
|
||||||
|
String fieldName = namesAndValues.get(i);
|
||||||
|
String value = namesAndValues.get(i + 1);
|
||||||
|
|
||||||
|
List<String> allValues = new ArrayList<String>();
|
||||||
|
List<String> otherValues = result.get(fieldName);
|
||||||
|
if (otherValues != null) {
|
||||||
|
allValues.addAll(otherValues);
|
||||||
|
}
|
||||||
|
allValues.add(value);
|
||||||
|
result.put(fieldName, Collections.unmodifiableList(allValues));
|
||||||
|
}
|
||||||
|
if (response && statusLine != null) {
|
||||||
|
result.put(null, Collections.unmodifiableList(Collections.singletonList(statusLine)));
|
||||||
|
} else if (requestLine != null) {
|
||||||
|
result.put(null, Collections.unmodifiableList(Collections.singletonList(requestLine)));
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableMap(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance from the given map of fields to values. If
|
||||||
|
* present, the null field's last element will be used to set the status
|
||||||
|
* line.
|
||||||
|
*/
|
||||||
|
public static RawHeaders fromMultimap(Map<String, List<String>> map, boolean response)
|
||||||
|
throws IOException {
|
||||||
|
if (!response) throw new UnsupportedOperationException();
|
||||||
|
RawHeaders result = new RawHeaders();
|
||||||
|
for (Entry<String, List<String>> entry : map.entrySet()) {
|
||||||
|
String fieldName = entry.getKey();
|
||||||
|
List<String> values = entry.getValue();
|
||||||
|
if (fieldName != null) {
|
||||||
|
for (String value : values) {
|
||||||
|
result.addLenient(fieldName, value);
|
||||||
|
}
|
||||||
|
} else if (!values.isEmpty()) {
|
||||||
|
result.setStatusLine(values.get(values.size() - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of alternating names and values. Names are all lower case.
|
||||||
|
* No names are repeated. If any name has multiple values, they are
|
||||||
|
* concatenated using "\0" as a delimiter.
|
||||||
|
*/
|
||||||
|
public List<String> toNameValueBlock() {
|
||||||
|
Set<String> names = new HashSet<String>();
|
||||||
|
List<String> result = new ArrayList<String>();
|
||||||
|
for (int i = 0; i < namesAndValues.size(); i += 2) {
|
||||||
|
String name = namesAndValues.get(i).toLowerCase(Locale.US);
|
||||||
|
String value = namesAndValues.get(i + 1);
|
||||||
|
|
||||||
|
// Drop headers that are forbidden when layering HTTP over SPDY.
|
||||||
|
if (name.equals("connection")
|
||||||
|
|| name.equals("host")
|
||||||
|
|| name.equals("keep-alive")
|
||||||
|
|| name.equals("proxy-connection")
|
||||||
|
|| name.equals("transfer-encoding")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we haven't seen this name before, add the pair to the end of the list...
|
||||||
|
if (names.add(name)) {
|
||||||
|
result.add(name);
|
||||||
|
result.add(value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...otherwise concatenate the existing values and this value.
|
||||||
|
for (int j = 0; j < result.size(); j += 2) {
|
||||||
|
if (name.equals(result.get(j))) {
|
||||||
|
result.set(j + 1, result.get(j + 1) + "\0" + value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RawHeaders fromNameValueBlock(List<String> nameValueBlock) {
|
||||||
|
if (nameValueBlock.size() % 2 != 0) {
|
||||||
|
throw new IllegalArgumentException("Unexpected name value block: " + nameValueBlock);
|
||||||
|
}
|
||||||
|
RawHeaders result = new RawHeaders();
|
||||||
|
for (int i = 0; i < nameValueBlock.size(); i += 2) {
|
||||||
|
String name = nameValueBlock.get(i);
|
||||||
|
String values = nameValueBlock.get(i + 1);
|
||||||
|
for (int start = 0; start < values.length(); ) {
|
||||||
|
int end = values.indexOf('\0', start);
|
||||||
|
if (end == -1) {
|
||||||
|
end = values.length();
|
||||||
|
}
|
||||||
|
result.namesAndValues.add(name);
|
||||||
|
result.namesAndValues.add(values.substring(start, end));
|
||||||
|
start = end + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp.internal.http;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/** Parsed HTTP request headers. */
|
||||||
|
public final class RequestHeaders {
|
||||||
|
private final URI uri;
|
||||||
|
private final RawHeaders headers;
|
||||||
|
|
||||||
|
/** Don't use a cache to satisfy this request. */
|
||||||
|
private boolean noCache;
|
||||||
|
private int maxAgeSeconds = -1;
|
||||||
|
private int maxStaleSeconds = -1;
|
||||||
|
private int minFreshSeconds = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This field's name "only-if-cached" is misleading. It actually means "do
|
||||||
|
* not use the network". It is set by a client who only wants to make a
|
||||||
|
* request if it can be fully satisfied by the cache. Cached responses that
|
||||||
|
* would require validation (ie. conditional gets) are not permitted if this
|
||||||
|
* header is set.
|
||||||
|
*/
|
||||||
|
private boolean onlyIfCached;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the request contains an authorization field. Although this isn't
|
||||||
|
* necessarily a shared cache, it follows the spec's strict requirements for
|
||||||
|
* shared caches.
|
||||||
|
*/
|
||||||
|
private boolean hasAuthorization;
|
||||||
|
|
||||||
|
private int contentLength = -1;
|
||||||
|
private String transferEncoding;
|
||||||
|
private String userAgent;
|
||||||
|
private String host;
|
||||||
|
private String connection;
|
||||||
|
private String acceptEncoding;
|
||||||
|
private String contentType;
|
||||||
|
private String ifModifiedSince;
|
||||||
|
private String ifNoneMatch;
|
||||||
|
private String proxyAuthorization;
|
||||||
|
|
||||||
|
public RequestHeaders(URI uri, RawHeaders headers) {
|
||||||
|
this.uri = uri;
|
||||||
|
this.headers = headers;
|
||||||
|
|
||||||
|
HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() {
|
||||||
|
@Override public void handle(String directive, String parameter) {
|
||||||
|
if ("no-cache".equalsIgnoreCase(directive)) {
|
||||||
|
noCache = true;
|
||||||
|
} else if ("max-age".equalsIgnoreCase(directive)) {
|
||||||
|
maxAgeSeconds = HeaderParser.parseSeconds(parameter);
|
||||||
|
} else if ("max-stale".equalsIgnoreCase(directive)) {
|
||||||
|
maxStaleSeconds = HeaderParser.parseSeconds(parameter);
|
||||||
|
} else if ("min-fresh".equalsIgnoreCase(directive)) {
|
||||||
|
minFreshSeconds = HeaderParser.parseSeconds(parameter);
|
||||||
|
} else if ("only-if-cached".equalsIgnoreCase(directive)) {
|
||||||
|
onlyIfCached = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < headers.length(); i++) {
|
||||||
|
String fieldName = headers.getFieldName(i);
|
||||||
|
String value = headers.getValue(i);
|
||||||
|
if ("Cache-Control".equalsIgnoreCase(fieldName)) {
|
||||||
|
HeaderParser.parseCacheControl(value, handler);
|
||||||
|
} else if ("Pragma".equalsIgnoreCase(fieldName)) {
|
||||||
|
if ("no-cache".equalsIgnoreCase(value)) {
|
||||||
|
noCache = true;
|
||||||
|
}
|
||||||
|
} else if ("If-None-Match".equalsIgnoreCase(fieldName)) {
|
||||||
|
ifNoneMatch = value;
|
||||||
|
} else if ("If-Modified-Since".equalsIgnoreCase(fieldName)) {
|
||||||
|
ifModifiedSince = value;
|
||||||
|
} else if ("Authorization".equalsIgnoreCase(fieldName)) {
|
||||||
|
hasAuthorization = true;
|
||||||
|
} else if ("Content-Length".equalsIgnoreCase(fieldName)) {
|
||||||
|
try {
|
||||||
|
contentLength = Integer.parseInt(value);
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
}
|
||||||
|
} else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) {
|
||||||
|
transferEncoding = value;
|
||||||
|
} else if ("User-Agent".equalsIgnoreCase(fieldName)) {
|
||||||
|
userAgent = value;
|
||||||
|
} else if ("Host".equalsIgnoreCase(fieldName)) {
|
||||||
|
host = value;
|
||||||
|
} else if ("Connection".equalsIgnoreCase(fieldName)) {
|
||||||
|
connection = value;
|
||||||
|
} else if ("Accept-Encoding".equalsIgnoreCase(fieldName)) {
|
||||||
|
acceptEncoding = value;
|
||||||
|
} else if ("Content-Type".equalsIgnoreCase(fieldName)) {
|
||||||
|
contentType = value;
|
||||||
|
} else if ("Proxy-Authorization".equalsIgnoreCase(fieldName)) {
|
||||||
|
proxyAuthorization = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isChunked() {
|
||||||
|
return "chunked".equalsIgnoreCase(transferEncoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasConnectionClose() {
|
||||||
|
return "close".equalsIgnoreCase(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public URI getUri() {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RawHeaders getHeaders() {
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNoCache() {
|
||||||
|
return noCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxAgeSeconds() {
|
||||||
|
return maxAgeSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxStaleSeconds() {
|
||||||
|
return maxStaleSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMinFreshSeconds() {
|
||||||
|
return minFreshSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOnlyIfCached() {
|
||||||
|
return onlyIfCached;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasAuthorization() {
|
||||||
|
return hasAuthorization;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getContentLength() {
|
||||||
|
return contentLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTransferEncoding() {
|
||||||
|
return transferEncoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserAgent() {
|
||||||
|
return userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHost() {
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getConnection() {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAcceptEncoding() {
|
||||||
|
return acceptEncoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContentType() {
|
||||||
|
return contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIfModifiedSince() {
|
||||||
|
return ifModifiedSince;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIfNoneMatch() {
|
||||||
|
return ifNoneMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProxyAuthorization() {
|
||||||
|
return proxyAuthorization;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChunked() {
|
||||||
|
if (this.transferEncoding != null) {
|
||||||
|
headers.removeAll("Transfer-Encoding");
|
||||||
|
}
|
||||||
|
headers.add("Transfer-Encoding", "chunked");
|
||||||
|
this.transferEncoding = "chunked";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContentLength(int contentLength) {
|
||||||
|
if (this.contentLength != -1) {
|
||||||
|
headers.removeAll("Content-Length");
|
||||||
|
}
|
||||||
|
headers.add("Content-Length", Integer.toString(contentLength));
|
||||||
|
this.contentLength = contentLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserAgent(String userAgent) {
|
||||||
|
if (this.userAgent != null) {
|
||||||
|
headers.removeAll("User-Agent");
|
||||||
|
}
|
||||||
|
headers.add("User-Agent", userAgent);
|
||||||
|
this.userAgent = userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHost(String host) {
|
||||||
|
if (this.host != null) {
|
||||||
|
headers.removeAll("Host");
|
||||||
|
}
|
||||||
|
headers.add("Host", host);
|
||||||
|
this.host = host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConnection(String connection) {
|
||||||
|
if (this.connection != null) {
|
||||||
|
headers.removeAll("Connection");
|
||||||
|
}
|
||||||
|
headers.add("Connection", connection);
|
||||||
|
this.connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAcceptEncoding(String acceptEncoding) {
|
||||||
|
if (this.acceptEncoding != null) {
|
||||||
|
headers.removeAll("Accept-Encoding");
|
||||||
|
}
|
||||||
|
headers.add("Accept-Encoding", acceptEncoding);
|
||||||
|
this.acceptEncoding = acceptEncoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContentType(String contentType) {
|
||||||
|
if (this.contentType != null) {
|
||||||
|
headers.removeAll("Content-Type");
|
||||||
|
}
|
||||||
|
headers.add("Content-Type", contentType);
|
||||||
|
this.contentType = contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIfModifiedSince(Date date) {
|
||||||
|
if (ifModifiedSince != null) {
|
||||||
|
headers.removeAll("If-Modified-Since");
|
||||||
|
}
|
||||||
|
String formattedDate = HttpDate.format(date);
|
||||||
|
headers.add("If-Modified-Since", formattedDate);
|
||||||
|
ifModifiedSince = formattedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIfNoneMatch(String ifNoneMatch) {
|
||||||
|
if (this.ifNoneMatch != null) {
|
||||||
|
headers.removeAll("If-None-Match");
|
||||||
|
}
|
||||||
|
headers.add("If-None-Match", ifNoneMatch);
|
||||||
|
this.ifNoneMatch = ifNoneMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the request contains conditions that save the server from
|
||||||
|
* sending a response that the client has locally. When the caller adds
|
||||||
|
* conditions, this cache won't participate in the request.
|
||||||
|
*/
|
||||||
|
public boolean hasConditions() {
|
||||||
|
return ifModifiedSince != null || ifNoneMatch != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCookies(Map<String, List<String>> allCookieHeaders) {
|
||||||
|
for (Map.Entry<String, List<String>> entry : allCookieHeaders.entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
if ("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key)) {
|
||||||
|
headers.addAll(key, entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,497 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.squareup.okhttp.internal.http;
|
||||||
|
|
||||||
|
import com.squareup.okhttp.ResponseSource;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static com.squareup.okhttp.internal.Util.equal;
|
||||||
|
|
||||||
|
/** Parsed HTTP response headers. */
|
||||||
|
public final class ResponseHeaders {
|
||||||
|
|
||||||
|
/** HTTP header name for the local time when the request was sent. */
|
||||||
|
private static final String SENT_MILLIS = "X-Android-Sent-Millis";
|
||||||
|
|
||||||
|
/** HTTP header name for the local time when the response was received. */
|
||||||
|
private static final String RECEIVED_MILLIS = "X-Android-Received-Millis";
|
||||||
|
|
||||||
|
/** HTTP synthetic header with the response source. */
|
||||||
|
static final String RESPONSE_SOURCE = "X-Android-Response-Source";
|
||||||
|
|
||||||
|
private final URI uri;
|
||||||
|
private final RawHeaders headers;
|
||||||
|
|
||||||
|
/** The server's time when this response was served, if known. */
|
||||||
|
private Date servedDate;
|
||||||
|
|
||||||
|
/** The last modified date of the response, if known. */
|
||||||
|
private Date lastModified;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The expiration date of the response, if known. If both this field and the
|
||||||
|
* max age are set, the max age is preferred.
|
||||||
|
*/
|
||||||
|
private Date expires;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension header set by HttpURLConnectionImpl specifying the timestamp
|
||||||
|
* when the HTTP request was first initiated.
|
||||||
|
*/
|
||||||
|
private long sentRequestMillis;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension header set by HttpURLConnectionImpl specifying the timestamp
|
||||||
|
* when the HTTP response was first received.
|
||||||
|
*/
|
||||||
|
private long receivedResponseMillis;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In the response, this field's name "no-cache" is misleading. It doesn't
|
||||||
|
* prevent us from caching the response; it only means we have to validate
|
||||||
|
* the response with the origin server before returning it. We can do this
|
||||||
|
* with a conditional get.
|
||||||
|
*/
|
||||||
|
private boolean noCache;
|
||||||
|
|
||||||
|
/** If true, this response should not be cached. */
|
||||||
|
private boolean noStore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The duration past the response's served date that it can be served
|
||||||
|
* without validation.
|
||||||
|
*/
|
||||||
|
private int maxAgeSeconds = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The "s-maxage" directive is the max age for shared caches. Not to be
|
||||||
|
* confused with "max-age" for non-shared caches, As in Firefox and Chrome,
|
||||||
|
* this directive is not honored by this cache.
|
||||||
|
*/
|
||||||
|
private int sMaxAgeSeconds = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This request header field's name "only-if-cached" is misleading. It
|
||||||
|
* actually means "do not use the network". It is set by a client who only
|
||||||
|
* wants to make a request if it can be fully satisfied by the cache.
|
||||||
|
* Cached responses that would require validation (ie. conditional gets) are
|
||||||
|
* not permitted if this header is set.
|
||||||
|
*/
|
||||||
|
private boolean isPublic;
|
||||||
|
private boolean mustRevalidate;
|
||||||
|
private String etag;
|
||||||
|
private int ageSeconds = -1;
|
||||||
|
|
||||||
|
/** Case-insensitive set of field names. */
|
||||||
|
private Set<String> varyFields = Collections.emptySet();
|
||||||
|
|
||||||
|
private String contentEncoding;
|
||||||
|
private String transferEncoding;
|
||||||
|
private int contentLength = -1;
|
||||||
|
private String connection;
|
||||||
|
|
||||||
|
public ResponseHeaders(URI uri, RawHeaders headers) {
|
||||||
|
this.uri = uri;
|
||||||
|
this.headers = headers;
|
||||||
|
|
||||||
|
HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() {
|
||||||
|
@Override public void handle(String directive, String parameter) {
|
||||||
|
if ("no-cache".equalsIgnoreCase(directive)) {
|
||||||
|
noCache = true;
|
||||||
|
} else if ("no-store".equalsIgnoreCase(directive)) {
|
||||||
|
noStore = true;
|
||||||
|
} else if ("max-age".equalsIgnoreCase(directive)) {
|
||||||
|
maxAgeSeconds = HeaderParser.parseSeconds(parameter);
|
||||||
|
} else if ("s-maxage".equalsIgnoreCase(directive)) {
|
||||||
|
sMaxAgeSeconds = HeaderParser.parseSeconds(parameter);
|
||||||
|
} else if ("public".equalsIgnoreCase(directive)) {
|
||||||
|
isPublic = true;
|
||||||
|
} else if ("must-revalidate".equalsIgnoreCase(directive)) {
|
||||||
|
mustRevalidate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < headers.length(); i++) {
|
||||||
|
String fieldName = headers.getFieldName(i);
|
||||||
|
String value = headers.getValue(i);
|
||||||
|
if ("Cache-Control".equalsIgnoreCase(fieldName)) {
|
||||||
|
HeaderParser.parseCacheControl(value, handler);
|
||||||
|
} else if ("Date".equalsIgnoreCase(fieldName)) {
|
||||||
|
servedDate = HttpDate.parse(value);
|
||||||
|
} else if ("Expires".equalsIgnoreCase(fieldName)) {
|
||||||
|
expires = HttpDate.parse(value);
|
||||||
|
} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
|
||||||
|
lastModified = HttpDate.parse(value);
|
||||||
|
} else if ("ETag".equalsIgnoreCase(fieldName)) {
|
||||||
|
etag = value;
|
||||||
|
} else if ("Pragma".equalsIgnoreCase(fieldName)) {
|
||||||
|
if ("no-cache".equalsIgnoreCase(value)) {
|
||||||
|
noCache = true;
|
||||||
|
}
|
||||||
|
} else if ("Age".equalsIgnoreCase(fieldName)) {
|
||||||
|
ageSeconds = HeaderParser.parseSeconds(value);
|
||||||
|
} else if ("Vary".equalsIgnoreCase(fieldName)) {
|
||||||
|
// Replace the immutable empty set with something we can mutate.
|
||||||
|
if (varyFields.isEmpty()) {
|
||||||
|
varyFields = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
|
||||||
|
}
|
||||||
|
for (String varyField : value.split(",")) {
|
||||||
|
varyFields.add(varyField.trim());
|
||||||
|
}
|
||||||
|
} else if ("Content-Encoding".equalsIgnoreCase(fieldName)) {
|
||||||
|
contentEncoding = value;
|
||||||
|
} else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) {
|
||||||
|
transferEncoding = value;
|
||||||
|
} else if ("Content-Length".equalsIgnoreCase(fieldName)) {
|
||||||
|
try {
|
||||||
|
contentLength = Integer.parseInt(value);
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
}
|
||||||
|
} else if ("Connection".equalsIgnoreCase(fieldName)) {
|
||||||
|
connection = value;
|
||||||
|
} else if (SENT_MILLIS.equalsIgnoreCase(fieldName)) {
|
||||||
|
sentRequestMillis = Long.parseLong(value);
|
||||||
|
} else if (RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
|
||||||
|
receivedResponseMillis = Long.parseLong(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isContentEncodingGzip() {
|
||||||
|
return "gzip".equalsIgnoreCase(contentEncoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stripContentEncoding() {
|
||||||
|
contentEncoding = null;
|
||||||
|
headers.removeAll("Content-Encoding");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stripContentLength() {
|
||||||
|
contentLength = -1;
|
||||||
|
headers.removeAll("Content-Length");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isChunked() {
|
||||||
|
return "chunked".equalsIgnoreCase(transferEncoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasConnectionClose() {
|
||||||
|
return "close".equalsIgnoreCase(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public URI getUri() {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RawHeaders getHeaders() {
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getServedDate() {
|
||||||
|
return servedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getLastModified() {
|
||||||
|
return lastModified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getExpires() {
|
||||||
|
return expires;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNoCache() {
|
||||||
|
return noCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNoStore() {
|
||||||
|
return noStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxAgeSeconds() {
|
||||||
|
return maxAgeSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSMaxAgeSeconds() {
|
||||||
|
return sMaxAgeSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPublic() {
|
||||||
|
return isPublic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMustRevalidate() {
|
||||||
|
return mustRevalidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEtag() {
|
||||||
|
return etag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getVaryFields() {
|
||||||
|
return varyFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContentEncoding() {
|
||||||
|
return contentEncoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getContentLength() {
|
||||||
|
return contentLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getConnection() {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLocalTimestamps(long sentRequestMillis, long receivedResponseMillis) {
|
||||||
|
this.sentRequestMillis = sentRequestMillis;
|
||||||
|
headers.add(SENT_MILLIS, Long.toString(sentRequestMillis));
|
||||||
|
this.receivedResponseMillis = receivedResponseMillis;
|
||||||
|
headers.add(RECEIVED_MILLIS, Long.toString(receivedResponseMillis));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResponseSource(ResponseSource responseSource) {
|
||||||
|
headers.set(RESPONSE_SOURCE, responseSource.toString() + " " + headers.getResponseCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current age of the response, in milliseconds. The calculation
|
||||||
|
* is specified by RFC 2616, 13.2.3 Age Calculations.
|
||||||
|
*/
|
||||||
|
private long computeAge(long nowMillis) {
|
||||||
|
long apparentReceivedAge =
|
||||||
|
servedDate != null ? Math.max(0, receivedResponseMillis - servedDate.getTime()) : 0;
|
||||||
|
long receivedAge =
|
||||||
|
ageSeconds != -1 ? Math.max(apparentReceivedAge, TimeUnit.SECONDS.toMillis(ageSeconds))
|
||||||
|
: apparentReceivedAge;
|
||||||
|
long responseDuration = receivedResponseMillis - sentRequestMillis;
|
||||||
|
long residentDuration = nowMillis - receivedResponseMillis;
|
||||||
|
return receivedAge + responseDuration + residentDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of milliseconds that the response was fresh for,
|
||||||
|
* starting from the served date.
|
||||||
|
*/
|
||||||
|
private long computeFreshnessLifetime() {
|
||||||
|
if (maxAgeSeconds != -1) {
|
||||||
|
return TimeUnit.SECONDS.toMillis(maxAgeSeconds);
|
||||||
|
} else if (expires != null) {
|
||||||
|
long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis;
|
||||||
|
long delta = expires.getTime() - servedMillis;
|
||||||
|
return delta > 0 ? delta : 0;
|
||||||
|
} else if (lastModified != null && uri.getRawQuery() == null) {
|
||||||
|
// As recommended by the HTTP RFC and implemented in Firefox, the
|
||||||
|
// max age of a document should be defaulted to 10% of the
|
||||||
|
// document's age at the time it was served. Default expiration
|
||||||
|
// dates aren't used for URIs containing a query.
|
||||||
|
long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis;
|
||||||
|
long delta = servedMillis - lastModified.getTime();
|
||||||
|
return delta > 0 ? (delta / 10) : 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if computeFreshnessLifetime used a heuristic. If we used a
|
||||||
|
* heuristic to serve a cached response older than 24 hours, we are required
|
||||||
|
* to attach a warning.
|
||||||
|
*/
|
||||||
|
private boolean isFreshnessLifetimeHeuristic() {
|
||||||
|
return maxAgeSeconds == -1 && expires == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this response can be stored to later serve another
|
||||||
|
* request.
|
||||||
|
*/
|
||||||
|
public boolean isCacheable(RequestHeaders request) {
|
||||||
|
// Always go to network for uncacheable response codes (RFC 2616, 13.4),
|
||||||
|
// This implementation doesn't support caching partial content.
|
||||||
|
int responseCode = headers.getResponseCode();
|
||||||
|
if (responseCode != HttpURLConnection.HTTP_OK
|
||||||
|
&& responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE
|
||||||
|
&& responseCode != HttpURLConnection.HTTP_MULT_CHOICE
|
||||||
|
&& responseCode != HttpURLConnection.HTTP_MOVED_PERM
|
||||||
|
&& responseCode != HttpURLConnection.HTTP_GONE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responses to authorized requests aren't cacheable unless they include
|
||||||
|
// a 'public', 'must-revalidate' or 's-maxage' directive.
|
||||||
|
if (request.hasAuthorization() && !isPublic && !mustRevalidate && sMaxAgeSeconds == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noStore) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if a Vary header contains an asterisk. Such responses cannot
|
||||||
|
* be cached.
|
||||||
|
*/
|
||||||
|
public boolean hasVaryAll() {
|
||||||
|
return varyFields.contains("*");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if none of the Vary headers on this response have changed
|
||||||
|
* between {@code cachedRequest} and {@code newRequest}.
|
||||||
|
*/
|
||||||
|
public boolean varyMatches(Map<String, List<String>> cachedRequest,
|
||||||
|
Map<String, List<String>> newRequest) {
|
||||||
|
for (String field : varyFields) {
|
||||||
|
if (!equal(cachedRequest.get(field), newRequest.get(field))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the source to satisfy {@code request} given this cached response. */
|
||||||
|
public ResponseSource chooseResponseSource(long nowMillis, RequestHeaders request) {
|
||||||
|
// If this response shouldn't have been stored, it should never be used
|
||||||
|
// as a response source. This check should be redundant as long as the
|
||||||
|
// persistence store is well-behaved and the rules are constant.
|
||||||
|
if (!isCacheable(request)) {
|
||||||
|
return ResponseSource.NETWORK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.isNoCache() || request.hasConditions()) {
|
||||||
|
return ResponseSource.NETWORK;
|
||||||
|
}
|
||||||
|
|
||||||
|
long ageMillis = computeAge(nowMillis);
|
||||||
|
long freshMillis = computeFreshnessLifetime();
|
||||||
|
|
||||||
|
if (request.getMaxAgeSeconds() != -1) {
|
||||||
|
freshMillis = Math.min(freshMillis, TimeUnit.SECONDS.toMillis(request.getMaxAgeSeconds()));
|
||||||
|
}
|
||||||
|
|
||||||
|
long minFreshMillis = 0;
|
||||||
|
if (request.getMinFreshSeconds() != -1) {
|
||||||
|
minFreshMillis = TimeUnit.SECONDS.toMillis(request.getMinFreshSeconds());
|
||||||
|
}
|
||||||
|
|
||||||
|
long maxStaleMillis = 0;
|
||||||
|
if (!mustRevalidate && request.getMaxStaleSeconds() != -1) {
|
||||||
|
maxStaleMillis = TimeUnit.SECONDS.toMillis(request.getMaxStaleSeconds());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
|
||||||
|
if (ageMillis + minFreshMillis >= freshMillis) {
|
||||||
|
headers.add("Warning", "110 HttpURLConnection \"Response is stale\"");
|
||||||
|
}
|
||||||
|
long oneDayMillis = 24 * 60 * 60 * 1000L;
|
||||||
|
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
|
||||||
|
headers.add("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
|
||||||
|
}
|
||||||
|
return ResponseSource.CACHE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastModified != null) {
|
||||||
|
request.setIfModifiedSince(lastModified);
|
||||||
|
} else if (servedDate != null) {
|
||||||
|
request.setIfModifiedSince(servedDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (etag != null) {
|
||||||
|
request.setIfNoneMatch(etag);
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.hasConditions() ? ResponseSource.CONDITIONAL_CACHE : ResponseSource.NETWORK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this cached response should be used; false if the
|
||||||
|
* network response should be used.
|
||||||
|
*/
|
||||||
|
public boolean validate(ResponseHeaders networkResponse) {
|
||||||
|
if (networkResponse.headers.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The HTTP spec says that if the network's response is older than our
|
||||||
|
// cached response, we may return the cache's response. Like Chrome (but
|
||||||
|
// unlike Firefox), this client prefers to return the newer response.
|
||||||
|
if (lastModified != null
|
||||||
|
&& networkResponse.lastModified != null
|
||||||
|
&& networkResponse.lastModified.getTime() < lastModified.getTime()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines this cached header with a network header as defined by RFC 2616,
|
||||||
|
* 13.5.3.
|
||||||
|
*/
|
||||||
|
public ResponseHeaders combine(ResponseHeaders network) throws IOException {
|
||||||
|
RawHeaders result = new RawHeaders();
|
||||||
|
result.setStatusLine(headers.getStatusLine());
|
||||||
|
|
||||||
|
for (int i = 0; i < headers.length(); i++) {
|
||||||
|
String fieldName = headers.getFieldName(i);
|
||||||
|
String value = headers.getValue(i);
|
||||||
|
if ("Warning".equals(fieldName) && value.startsWith("1")) {
|
||||||
|
continue; // drop 100-level freshness warnings
|
||||||
|
}
|
||||||
|
if (!isEndToEnd(fieldName) || network.headers.get(fieldName) == null) {
|
||||||
|
result.add(fieldName, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < network.headers.length(); i++) {
|
||||||
|
String fieldName = network.headers.getFieldName(i);
|
||||||
|
if (isEndToEnd(fieldName)) {
|
||||||
|
result.add(fieldName, network.headers.getValue(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ResponseHeaders(uri, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if {@code fieldName} is an end-to-end HTTP header, as
|
||||||
|
* defined by RFC 2616, 13.5.1.
|
||||||
|
*/
|
||||||
|
private static boolean isEndToEnd(String fieldName) {
|
||||||
|
return !"Connection".equalsIgnoreCase(fieldName)
|
||||||
|
&& !"Keep-Alive".equalsIgnoreCase(fieldName)
|
||||||
|
&& !"Proxy-Authenticate".equalsIgnoreCase(fieldName)
|
||||||
|
&& !"Proxy-Authorization".equalsIgnoreCase(fieldName)
|
||||||
|
&& !"TE".equalsIgnoreCase(fieldName)
|
||||||
|
&& !"Trailers".equalsIgnoreCase(fieldName)
|
||||||
|
&& !"Transfer-Encoding".equalsIgnoreCase(fieldName)
|
||||||
|
&& !"Upgrade".equalsIgnoreCase(fieldName);
|
||||||
|
}
|
||||||
|
}
|
||||||