Compare commits

..

7 Commits

Author SHA1 Message Date
Joe Bowser
221b10b1e6 Set VERSION to 3.2.0 (via coho) 2013-11-18 13:18:48 -08:00
Joe Bowser
dd4e2a6a62 Update JS snapshot to version 3.2.0 (via coho) 2013-11-18 13:18:48 -08:00
Steven Gill
1febd02592 Merge branch '3.2.x' of https://git-wip-us.apache.org/repos/asf/cordova-android into 3.2.x 2013-11-11 16:38:05 -08:00
Steven Gill
499efecd7f CB-5349: fixed regression in update script 2013-11-11 16:36:24 -08:00
Marcel Kinard
fce9be7b0b CB-5301 add missing license headers
(cherry picked from commit 04f812c136)
2013-11-06 16:29:24 -05:00
Joe Bowser
fb3fb416b1 Set VERSION to 3.2.0-rc1 (via coho) 2013-11-04 09:50:38 -08:00
Joe Bowser
31195a1817 Update JS snapshot to version 3.2.0-rc1 (via coho) 2013-11-04 09:50:37 -08:00
288 changed files with 14372 additions and 16790 deletions

5
.gitignore vendored
View File

@@ -14,16 +14,11 @@ framework/assets/www/.DS_Store
framework/assets/www/cordova-*.js
framework/assets/www/phonegap-*.js
framework/libs
framework/javadoc-public
framework/javadoc-private
test/libs
example
./test
test/bin
test/assets/www/.tmp*
test/assets/www/cordova.js
test/cordova/plugins/org.apache.cordova.device/www/device.js
test/cordova/plugins/org.apache.cordova.device/src/android/Device.java
tmp/**
.metadata
tmp/**/*

14
NOTICE
View File

@@ -2,4 +2,16 @@ Apache Cordova
Copyright 2012 The Apache Software Foundation
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).

View File

@@ -7,9 +7,9 @@
# 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
@@ -23,27 +23,27 @@ Cordova Android
Cordova Android is an Android application library that allows for Cordova-based
projects to be built for the Android Platform. Cordova based applications are,
at the core, applications written with web technology: HTML, CSS and JavaScript.
at the core, applications written with web technology: HTML, CSS and JavaScript.
[Apache Cordova](http://cordova.io) is a project of The Apache Software Foundation (ASF).
[Apache Cordova](http://cordova.io) is a project at The Apache Software Foundation (ASF).
Requires
---
- 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)
Cordova Android Developer Tools
---
The Cordova developer tooling is split between general tooling and project level tooling.
The Cordova developer tooling is split between general tooling and project level tooling.
General Commands
./bin/create [path package activity] ... creates the ./example app or a cordova android project
./bin/create [path package activity] ... create the ./example app or a cordova android project
./bin/check_reqs ....................... checks that your environment is set up for cordova-android development
./bin/update [path] .................... updates an existing cordova-android project to the version of the framework
@@ -53,7 +53,7 @@ These commands live in a generated Cordova Android project. Any interactions wit
./cordova/clean ........................ cleans the project
./cordova/build ........................ calls `clean` then compiles the project
./cordova/log ........................ streams device or emulator logs to STDOUT
./cordova/log ........................ stream device or emulate logs to stdout
./cordova/run ........................ calls `build` then deploys to a connected Android device. If no Android device is detected, will launch an emulator and deploy to it.
./cordova/version ...................... returns the cordova-android version of the current project
@@ -69,13 +69,13 @@ Importing a Cordova Android Project into Eclipse
Building without the Tooling
---
Note: The Developer Tools handle this. This is only to be done if the tooling fails, or if
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-19
android update project -p . -t android-18
ant jar

View File

@@ -7,9 +7,9 @@
# 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
@@ -20,55 +20,6 @@
-->
## Release Notes for Cordova (Android) ##
### 3.4.0 (Feb 2014) ###
43 commits from 10 authors. Highlights include:
* Removing addJavascriptInterface support from all Android versions lower than 4.2 due to security vulnerability
* CB-5917 Add a loadUrlIntoView overload that doesn't recreate plugins.
* CB-5889 Make update script find project name instead of using "null" for CordovaLib
* CB-5889 Add a message in the update script about needing to import CordovaLib when using an IDE.
* CB-5793 Don't clean before build and change output directory to ant-build to avoid conflicts with Eclipse.
* CB-5803 Fix cordova/emulate on windows.
* CB-5801 exec->spawn in build to make sure compile errors are shown.
* CB-5799 Update version of OkHTTP to 1.3
* CB-4910 Update CLI project template to point to config.xml at the root now that it's not in www/ by default.
* CB-5504 Adding onDestroy to app plugin to deregister telephonyReceiver
* CB-5715 Add Eclipse .project file to create template. For CLI projects, it adds refs for root www/ & config.xml and hides platform versions
* CB-5447 Removed android:debuggable=“true” from project template.
* CB-5714 Fix of android build when too big output stops build with error due to buffer overflow.
* CB-5592 Set MIME type for openExternal when scheme is file:
### 3.3.0 (Dec 2013) ###
41 commits from 11 authors. Highlights include:
* CB-5481 Fix for Cordova trying to get config.xml from the wrong namespace
* CB-5487 Enable Remote Debugging when your Android app is debuggable.
* CB-5445 Adding onScrollChanged and the ScrollEvent object
* CB-5422 Don't require JAVA_HOME to be defined
* CB-5490 Add javadoc target to ant script
* CB-5471 Deprecated DroidGap class
* CB-5255 Prefer Google API targets over android-## targets when building.
* CB-5232 Change create script to use Cordova as a Library Project instead of a .jar
* CB-5302 Massive movement to get tests working again
* CB-4996 Fix paths with spaces while launching on emulator and device
* CB-5209 Cannot build Android app if project path contains spaces
### 3.2.0 (Nov 2013) ###
27 commits from 7 authors. Highlights include:
* CB-5193 Fix Android WebSQL sometime throwing SECURITY_ERR.
* CB-5191 Deprecate <url-filter>
* Updating shelljs to 0.2.6. Copy now preserves mode bits.
* CB-4872 Added android version scripts (android_sdk_version, etc)
* CB-5117 Output confirmation message if check_reqs passes.
* CB-5080 Find resources in a way that works with aapt's --rename-manifest-package
* CB-4527 Don't delete .bat files even when on non-windows platform
* CB-4892 Fix create script only escaping the first space instead of all spaces.
### 3.1.0 (Sept 2013) ###
55 commits from 9 authors. Highlights include:
@@ -81,7 +32,7 @@
* [CB-4764] Deprecated DirectoryManager.java (moved into plugins)
* [CB-4763] Deprecated FileHelper.java (moved into plugins), Move getMimeType() into CordovaResourceApi.
* [CB-4725] Add CordovaWebView.CORDOVA_VERSION constant
* Incrementing version check for Android 4.3 API Level 18
* Incremeting version check for Android 4.3 API Level 18
* [CB-3542] rewrote cli tooling scripts in node
* Allow CordovaChromeClient subclasses access to CordovaInterface and CordovaWebView members
* Refactor CordovaActivity.init so that subclasses can easily override factory methods for webview objects

View File

@@ -1 +1 @@
3.5.0-dev
3.2.0

View File

@@ -21,11 +21,8 @@
var check_reqs = require('./lib/check_reqs');
check_reqs.run().done(
function success() {
console.log('Looks like your environment fully supports cordova-android development!');
}, function fail(err) {
console.log(err);
process.exit(2);
}
);
check_reqs.run().done(null, function(err) {
console.log(err);
process.exit(2);
});

View File

@@ -20,17 +20,20 @@
*/
var path = require('path');
var create = require('./lib/create');
var args = require('./lib/simpleargs').getArgs(process.argv);
var args = process.argv;
if (args['--help'] || args._.length === 0) {
console.log('Usage: ' + path.relative(process.cwd(), path.join(__dirname, 'create')) + ' <path_to_new_project> <package_name> <project_name> [<template_path>] [--shared]');
// Support basic help commands
if(args.length < 3 || (args[2] == '--help' || args[2] == '/?' || args[2] == '-h' ||
args[2] == 'help' || args[2] == '-help' || args[2] == '/help')) {
console.log('Usage: ' + path.relative(process.cwd(), path.join(__dirname, 'create')) + ' <path_to_new_project> <package_name> <project_name>');
console.log(' <path_to_new_project>: Path to your new Cordova Android project');
console.log(' <package_name>: Package name, following reverse-domain style convention');
console.log(' <project_name>: Project name');
console.log(' <template_path>: Path to a custom application template to use');
console.log(' --shared will use the CordovaLib project directly instead of making a copy.');
process.exit(1);
} else {
create.createProject(args[2], args[3], args[4], args[5]).done(null, function(err) {
console.error(err);
process.exit(2);
});
}
create.createProject(args._[0], args._[1], args._[2], args._[3], args['--shared'], args['--cli']).done();

View File

@@ -33,10 +33,8 @@ module.exports.get_target = function() {
return target.split('=')[1].replace('\n', '').replace('\r', '').replace(' ', '');
} else if (fs.existsSync(path.join(ROOT, 'project.properties'))) {
// if no target found, we're probably in a project and project.properties is in ROOT.
// this is called on the project itself, and can support Google APIs AND Vanilla Android
var target = shell.grep(/target=android-[\d+]/, path.join(ROOT, 'project.properties')) ||
shell.grep(/target=Google Inc.:Google APIs:[\d+]/, path.join(ROOT, 'project.properties'));
return target.split('=')[1].replace('\n', '').replace('\r', '');
var target = shell.grep(/target=android-[\d+]/, path.join(ROOT, 'project.properties'));
return target.split('=')[1].replace('\n', '').replace('\r', '').replace(' ', '');
}
}
@@ -52,18 +50,16 @@ module.exports.check_ant = function() {
// Returns a promise.
module.exports.check_java = function() {
var d = Q.defer();
child_process.exec('java -version', function(err, stdout, stderr) {
if(err) {
var msg =
'Failed to run \'java -version\', make sure your java environment is set up\n' +
'including JDK and JRE.\n' +
'Your JAVA_HOME variable is ' + process.env.JAVA_HOME + '\n';
d.reject(new Error(msg + err));
}
else d.resolve();
});
return d.promise;
if(process.env.JAVA_HOME) {
var d = Q.defer();
child_process.exec('java -version', function(err, stdout, stderr) {
if(err) d.reject(new Error('ERROR : executing command \'java\', make sure you java environment is set up. Including your JDK and JRE.' + err));
else d.resolve();
});
return d.promise;
} else {
return Q.reject(new Error('ERROR : Make sure JAVA_HOME is set, as well as paths to your JDK and JRE for java.'));
}
}
// Returns a promise.
@@ -91,6 +87,8 @@ module.exports.check_android = function() {
// Returns a promise.
module.exports.run = function() {
return Q.all([this.check_ant(), this.check_java(), this.check_android()]);
return Q.all([this.check_ant(), this.check_java(), this.check_android()]).then(function() {
console.log('Looks like your environment fully supports cordova-android development!');
});
}

View File

@@ -29,13 +29,14 @@ var shell = require('shelljs'),
// Returns a promise.
function exec(command, opt_cwd) {
var d = Q.defer();
console.log('Running: ' + command);
child_process.exec(command, { cwd: opt_cwd }, function(err, stdout, stderr) {
stdout && console.log(stdout);
stderr && console.error(stderr);
if (err) d.reject(err);
else d.resolve(stdout);
});
try {
child_process.exec(command, { cwd: opt_cwd }, function(err, stdout, stderr) {
if (err) d.reject(err);
else d.resolve(stdout);
});
} catch(e) {
return Q.reject('Command error on execution: ' + command + '\n' + e);
}
return d.promise;
}
@@ -46,50 +47,32 @@ function setShellFatal(value, func) {
shell.config.fatal = oldVal;
}
function getFrameworkDir(projectPath, shared) {
return shared ? path.join(ROOT, 'framework') : path.join(projectPath, 'CordovaLib');
// Returns a promise.
function ensureJarIsBuilt(version, target_api) {
var isDevVersion = /-dev$/.test(version);
if (isDevVersion || !fs.existsSync(path.join(ROOT, 'framework', 'cordova-' + version + '.jar')) && fs.existsSync(path.join(ROOT, 'framework'))) {
var valid_target = check_reqs.get_target();
console.log('Building cordova-' + version + '.jar');
// update the cordova-android framework for the desired target
return exec('android --silent update lib-project --target "' + target_api + '" --path "' + path.join(ROOT, 'framework') + '"')
.then(function() {
// compile cordova.js and cordova.jar
return exec('ant jar', path.join(ROOT, 'framework'));
});
}
return Q();
}
function copyJsAndLibrary(projectPath, shared, projectName) {
var nestedCordovaLibPath = getFrameworkDir(projectPath, false);
function copyJsAndJar(projectPath, version) {
shell.cp('-f', path.join(ROOT, 'framework', 'assets', 'www', 'cordova.js'), path.join(projectPath, 'assets', 'www', 'cordova.js'));
// Don't fail if there are no old jars.
setShellFatal(false, function() {
shell.ls(path.join(projectPath, 'libs', 'cordova-*.jar')).forEach(function(oldJar) {
console.log("Deleting " + oldJar);
shell.rm('-f', oldJar);
shell.rm('-f', path.join(oldJar));
});
// Delete old library project if it existed.
if (shared) {
shell.rm('-rf', nestedCordovaLibPath);
} else {
// Delete only the src, since eclipse can't handle its .project file being deleted.
shell.rm('-rf', path.join(nestedCordovaLibPath, 'src'));
}
});
if (!shared) {
shell.mkdir('-p', nestedCordovaLibPath);
shell.cp('-f', path.join(ROOT, 'framework', 'AndroidManifest.xml'), nestedCordovaLibPath);
shell.cp('-f', path.join(ROOT, 'framework', 'project.properties'), nestedCordovaLibPath);
shell.cp('-r', path.join(ROOT, 'framework', 'src'), nestedCordovaLibPath);
// Create an eclipse project file and set the name of it to something unique.
// Without this, you can't import multiple CordovaLib projects into the same workspace.
var eclipseProjectFilePath = path.join(nestedCordovaLibPath, '.project');
if (!fs.existsSync(eclipseProjectFilePath)) {
var data = '<?xml version="1.0" encoding="UTF-8"?><projectDescription><name>' + projectName + '-' + 'CordovaLib</name></projectDescription>';
fs.writeFileSync(eclipseProjectFilePath, data, 'utf8');
}
}
}
function runAndroidUpdate(projectPath, target_api, shared) {
var targetFrameworkDir = getFrameworkDir(projectPath, shared);
return exec('android update project --subprojects --path "' + projectPath + '" --target ' + target_api + ' --library "' + path.relative(projectPath, targetFrameworkDir) + '"');
}
function copyAntRules(projectPath) {
var srcDir = path.join(ROOT, 'bin', 'templates', 'project');
shell.cp('-f', path.join(srcDir, 'custom_rules.xml'), projectPath);
shell.cp('-f', path.join(ROOT, 'framework', 'cordova-' + version + '.jar'), path.join(projectPath, 'libs', 'cordova-' + version + '.jar'));
}
function copyScripts(projectPath) {
@@ -121,7 +104,7 @@ function copyScripts(projectPath) {
* Returns a promise.
*/
exports.createProject = function(project_path, package_name, project_name, project_template_dir, use_shared_project, use_cli_template) {
exports.createProject = function(project_path, package_name, project_name, project_template_dir) {
var VERSION = fs.readFileSync(path.join(ROOT, 'VERSION'), 'utf-8').trim();
// Set default values for path, package and name
@@ -149,10 +132,6 @@ exports.createProject = function(project_path, package_name, project_name, proje
return Q.reject('Package name must look like: com.company.Name');
}
if (project_name === 'CordovaActivity') {
return Q.reject('Project name cannot be CordovaActivity');
}
// Check that requirements are met and proper targets are installed
return check_reqs.run()
.then(function() {
@@ -163,37 +142,27 @@ exports.createProject = function(project_path, package_name, project_name, proje
console.log('\tName: ' + project_name);
console.log('\tAndroid target: ' + target_api);
// build from source. distro should have these files
return ensureJarIsBuilt(VERSION, target_api);
}).then(function() {
console.log('Copying template files...');
setShellFatal(true, function() {
// copy project template
shell.cp('-r', path.join(project_template_dir, 'assets'), project_path);
shell.cp('-r', path.join(project_template_dir, 'res'), project_path);
shell.cp('-r', path.join(ROOT, 'framework', 'res', 'xml'), path.join(project_path, 'res'));
// Manually create directories that would be empty within the template (since git doesn't track directories).
shell.mkdir(path.join(project_path, 'libs'));
// Add in the proper eclipse project file.
if (use_cli_template) {
var note = 'To show `assets/www` or `res/xml/config.xml`, go to:\n' +
' Project -> Properties -> Resource -> Resource Filters\n' +
'And delete the exclusion filter.\n';
shell.cp(path.join(project_template_dir, 'eclipse-project-CLI'), path.join(project_path, '.project'));
fs.writeFileSync(path.join(project_path, 'assets', '_where-is-www.txt'), note);
} else {
shell.cp(path.join(project_template_dir, 'eclipse-project'), path.join(project_path, '.project'));
}
// copy cordova.js, cordova.jar
copyJsAndLibrary(project_path, use_shared_project, safe_activity_name);
// copy cordova.js, cordova.jar and res/xml
shell.cp('-r', path.join(ROOT, 'framework', 'res', 'xml'), path.join(project_path, 'res'));
copyJsAndJar(project_path, VERSION);
// interpolate the activity name and package
shell.mkdir('-p', activity_dir);
shell.cp('-f', path.join(project_template_dir, 'Activity.java'), activity_path);
shell.sed('-i', /__ACTIVITY__/, safe_activity_name, activity_path);
shell.sed('-i', /__NAME__/, project_name, path.join(project_path, 'res', 'values', 'strings.xml'));
shell.sed('-i', /__NAME__/, project_name, path.join(project_path, '.project'));
shell.sed('-i', /__ID__/, package_name, activity_path);
shell.cp('-f', path.join(project_template_dir, 'AndroidManifest.xml'), manifest_path);
@@ -201,48 +170,27 @@ exports.createProject = function(project_path, package_name, project_name, proje
shell.sed('-i', /__PACKAGE__/, package_name, manifest_path);
shell.sed('-i', /__APILEVEL__/, target_api.split('-')[1], manifest_path);
copyScripts(project_path);
copyAntRules(project_path);
});
// Link it to local android install.
return runAndroidUpdate(project_path, target_api, use_shared_project);
console.log('Running "android update project"');
return exec('android --silent update project --target "'+target_api+'" --path "'+ project_path+'"');
}).then(function() {
console.log('Project successfully created.');
});
}
// Attribute removed in Cordova 4.4 (CB-5447).
function removeDebuggableFromManifest(projectPath) {
var manifestPath = path.join(projectPath, 'AndroidManifest.xml');
shell.sed('-i', /\s*android:debuggable="true"/, '', manifestPath);
}
function extractProjectNameFromManifest(projectPath) {
var manifestPath = path.join(projectPath, 'AndroidManifest.xml');
var manifestData = fs.readFileSync(manifestPath, 'utf8');
var m = /<activity[\s\S]*?android:name\s*=\s*"(.*?)"/i.exec(manifestData);
if (!m) {
throw new Error('Could not find activity name in ' + manifestPath);
}
return m[1];
}
// Returns a promise.
exports.updateProject = function(projectPath) {
var version = fs.readFileSync(path.join(ROOT, 'VERSION'), 'utf-8').trim();
// Check that requirements are met and proper targets are installed
return check_reqs.run()
.then(function() {
var projectName = extractProjectNameFromManifest(projectPath);
var target_api = check_reqs.get_target();
copyJsAndLibrary(projectPath, false, projectName);
return ensureJarIsBuilt(version, target_api);
}).then(function() {
copyJsAndJar(projectPath, version);
copyScripts(projectPath);
copyAntRules(projectPath);
removeDebuggableFromManifest(projectPath);
return runAndroidUpdate(projectPath, target_api, false)
.then(function() {
console.log('Android project is now at version ' + version);
console.log('If you updated from a pre-3.2.0 version and use an IDE, we now require that you import the "CordovaLib" library project.');
});
console.log('Android project is now at version ' + version);
});
};

View File

@@ -1,32 +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.
*/
exports.getArgs = function(argv) {
var ret = {};
var posArgs = [];
for (var i = 2, arg; arg = argv[i] || i < argv.length; ++i) {
if (/^--/.exec(arg)) {
ret[arg] = true;
} else {
posArgs.push(arg);
}
}
ret._ = posArgs;
return ret;
};

33
bin/package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "cordova-android",
"description": "Cordova tooling for the android platform.",
"version": "0.0.0",
"homepage": "http://github.com/apache/cordova-android",
"repository": {
"type": "git",
"url": "https://git-wip-us.apache.org/repos/asf/cordova-android.git"
},
"keywords": [
"cli",
"cordova",
"tooling"
],
"engineStrict": "true",
"engines": {
"node": ">=0.10.0"
},
"dependencies": {
"shelljs" : "0.2.6",
"q": "~0.9"
},
"devDependencies": {
},
"optionalDependencies": {
},
"author": {
"name": "Benn Mapes",
"email": "bennmapes@gmail.com"
},
"contributors": [
]
}

View File

@@ -5,9 +5,9 @@
:: 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
@@ -18,7 +18,7 @@
@ECHO OFF
SET script_path="%~dp0build"
IF EXIST %script_path% (
node %script_path% %*
node "%script_path%" %*
) ELSE (
ECHO.
ECHO ERROR: Could not find 'build' script in 'cordova' folder, aborting...>&2

View File

@@ -5,9 +5,9 @@
:: 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
@@ -18,7 +18,7 @@
@ECHO OFF
SET script_path="%~dp0clean"
IF EXIST %script_path% (
node %script_path% %*
node "%script_path%" %*
) ELSE (
ECHO.
ECHO ERROR: Could not find 'clean' script in 'cordova' folder, aborting...>&2

View File

@@ -1,32 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<widget xmlns = "http://www.w3.org/ns/widgets"
id = "io.cordova.helloCordova"
version = "2.0.0">
<!-- Preferences for Android -->
<preference name="loglevel" value="DEBUG" />
<preference name="AndroidLaunchMode" value="singleTop" />
<!-- This is required for native Android hooks -->
<feature name="App">
<param name="android-package" value="org.apache.cordova.App" />
</feature>
</widget>

View File

@@ -20,25 +20,13 @@
*/
var shell = require('shelljs'),
spawn = require('./spawn'),
exec = require('./exec'),
Q = require('q'),
clean = require('./clean'),
path = require('path'),
fs = require('fs'),
ROOT = path.join(__dirname, '..', '..');
function hasCustomRules() {
return fs.existsSync(path.join(ROOT, 'custom_rules.xml'));
}
module.exports.getAntArgs = function(cmd) {
var args = [cmd, '-f', path.join(ROOT, 'build.xml')];
// custom_rules.xml is required for incremental builds.
if (hasCustomRules()) {
args.push('-Dout.dir=ant-build', '-Dgen.absolute.dir=ant-gen');
}
return args;
};
/*
* Builds the project with ant.
* Returns a promise.
@@ -46,12 +34,13 @@ module.exports.getAntArgs = function(cmd) {
module.exports.run = function(build_type) {
//default build type
build_type = typeof build_type !== 'undefined' ? build_type : "--debug";
var args = module.exports.getAntArgs('debug');
var cmd;
switch(build_type) {
case '--debug' :
cmd = 'ant debug -f ' + path.join(ROOT, 'build.xml');
break;
case '--release' :
args[0] = 'release';
cmd = 'ant release -f ' + path.join(ROOT, 'build.xml');
break;
case '--nobuild' :
console.log('Skipping build...');
@@ -59,14 +48,13 @@ module.exports.run = function(build_type) {
default :
return Q.reject('Build option \'' + build_type + '\' not recognized.');
}
// Without our custom_rules.xml, we need to clean before building.
var ret = Q();
if (!hasCustomRules()) {
ret = require('./clean').run();
if(cmd) {
return clean.run() // TODO: Can we stop cleaning every time and let ant build incrementally?
.then(function() {
return exec(cmd);
});
}
return ret.then(function() {
return spawn('ant', args);
});
return Q();
}
/*
@@ -74,37 +62,23 @@ module.exports.run = function(build_type) {
* the script will error out. (should we error or just return undefined?)
*/
module.exports.get_apk = function() {
var binDir = '';
if(!hasCustomRules()) {
binDir = path.join(ROOT, 'bin');
} else {
binDir = path.join(ROOT, 'ant-build');
}
if (fs.existsSync(binDir)) {
var candidates = fs.readdirSync(binDir).filter(function(p) {
// Need to choose between release and debug .apk.
return path.extname(p) == '.apk';
}).map(function(p) {
p = path.join(binDir, p);
return { p: p, t: fs.statSync(p).mtime };
}).sort(function(a,b) {
return a.t > b.t ? -1 :
a.t < b.t ? 1 : 0;
});
if (candidates.length === 0) {
console.error('ERROR : No .apk found in ' + binDir + ' directory');
process.exit(2);
if(fs.existsSync(path.join(ROOT, 'bin'))) {
var bin_files = fs.readdirSync(path.join(ROOT, 'bin'));
for (file in bin_files) {
if(path.extname(bin_files[file]) == '.apk') {
return path.join(ROOT, 'bin', bin_files[file]);
}
}
console.log('Using apk: ' + candidates[0].p);
return candidates[0].p;
console.error('ERROR : No .apk found in \'bin\' folder');
process.exit(2);
} else {
console.error('ERROR : unable to find project ' + binDir + ' directory, could not locate .apk');
console.error('ERROR : unable to find project bin folder, could not locate .apk');
process.exit(2);
}
}
module.exports.help = function() {
console.log('Usage: ' + path.relative(process.cwd(), path.join(ROOT, 'cordova', 'build')) + ' [build_type]');
console.log('Usage: ' + path.relative(process.cwd(), path.join(ROOT, 'corodva', 'build')) + ' [build_type]');
console.log('Build Types : ');
console.log(' \'--debug\': Default build, will build project in using ant debug');
console.log(' \'--release\': will build project using ant release');

View File

@@ -19,17 +19,16 @@
under the License.
*/
var build = require('./build'),
spawn = require('./spawn'),
path = require('path');
var exec = require('./exec'),
path = require('path'),
ROOT = path.join(__dirname, '..', '..');
/*
* Cleans the project using ant
* Returns a promise.
*/
module.exports.run = function() {
var args = build.getAntArgs('clean');
return spawn('ant', args);
return exec('ant clean -f ' + path.join(ROOT, 'build.xml'));
}
module.exports.help = function() {

View File

@@ -64,7 +64,7 @@ module.exports.install = function(target) {
var apk_path = build.get_apk();
launchName = appinfo.getActivityName();
console.log('Installing app on device...');
var cmd = 'adb -s ' + target + ' install -r "' + apk_path + '"';
var cmd = 'adb -s ' + target + ' install -r ' + apk_path;
return exec(cmd);
}).then(function(output) {
if (output.match(/Failure/)) return Q.reject('ERROR: Failed to install apk to device: ' + output);
@@ -79,7 +79,7 @@ module.exports.install = function(target) {
var cmd = 'adb -s ' + target + ' shell am start -W -a android.intent.action.MAIN -n ' + launchName;
return exec(cmd);
}).then(function() {
console.log('LAUNCH SUCCESS');
console.log('LANCH SUCCESS');
}, function(err) {
return Q.reject('ERROR: Failed to launch application on device: ' + err);
});

View File

@@ -143,8 +143,8 @@ module.exports.list_targets = function() {
/*
* Starts an emulator with the given ID,
* and returns the started ID of that emulator.
* If no ID is given it will used the first image available,
* if no image is available it will error out (maybe create one?).
* If no ID is given it will used the first image availible,
* if no image is availible it will error out (maybe create one?).
*
* Returns a promise.
*/
@@ -178,8 +178,14 @@ module.exports.start = function(emulator_ID) {
return Q(emulator_ID);
}
}).then(function() {
var cmd = 'emulator';
var args = ['-avd', emulator_ID];
var cmd, args;
if(process.platform == 'win32' || process.platform == 'win64') {
cmd = '%comspec%';
args = ['/c', 'start', 'cmd', '/c', 'emulator', '-avd', emulator_ID];
} else {
cmd = 'emulator';
args = ['-avd', emulator_ID];
}
var proc = child_process.spawn(cmd, args, { stdio: 'inherit', detached: true });
proc.unref(); // Don't wait for it to finish, since the emulator will probably keep running for a long time.
}).then(function() {
@@ -268,7 +274,7 @@ module.exports.create_image = function(name, target) {
.then(function() {
// TODO: This seems like another error case, even though it always happens.
console.error('ERROR : Unable to create an avd emulator, no targets found.');
console.error('Please insure you have targets available by running the "android" command');
console.error('Please insure you have targets availible by runing the "android" command');
return Q.reject();
}, function(error) {
console.error('ERROR : Failed to create emulator image : ');
@@ -299,7 +305,7 @@ module.exports.install = function(target) {
console.log('Installing app on emulator...');
var apk_path = build.get_apk();
return exec('adb -s ' + target + ' install -r "' + apk_path + '"');
return exec('adb -s ' + target + ' install -r ' + apk_path);
}).then(function(output) {
if (output.match(/Failure/)) {
return Q.reject('Failed to install apk to emulator: ' + output);

View File

@@ -27,8 +27,10 @@ var child_process = require('child_process'),
// rejects with an error message and the stderr.
module.exports = function(cmd, opt_cwd) {
var d = Q.defer();
console.log('exec: ' + cmd);
try {
child_process.exec(cmd, {cwd: opt_cwd, maxBuffer: 1024000}, function(err, stdout, stderr) {
child_process.exec(cmd, {cwd: opt_cwd}, function(err, stdout, stderr) {
console.log([cmd, err, stdout, stderr]);
if (err) d.reject('Error executing "' + cmd + '": ' + stderr);
else d.resolve(stdout);
});

View File

@@ -51,7 +51,7 @@ module.exports.run = function() {
}
module.exports.help = function() {
console.log('Usage: ' + path.relative(process.cwd(), path.join(ROOT, 'cordova', 'log')));
console.log('Usage: ' + path.relative(process.cwd(), path.join(ROOT, 'corodva', 'log')));
console.log('Gives the logcat output on the command line.');
process.exit(0);
}

View File

@@ -26,7 +26,7 @@ var path = require('path'),
Q = require('q');
/*
* Runs the application on a device if available.
* Runs the application on a device if availible.
* If not device is found, it will use a started emulator.
* If no started emulators are found it will attempt to start an avd.
* If no avds are found it will error out.
@@ -91,7 +91,7 @@ var path = require('path'),
}
});
} else {
// no target given, deploy to device if available, otherwise use the emulator.
// no target given, deploy to device if availible, otherwise use the emulator.
return device.list()
.then(function(device_list) {
if (device_list.length > 0) {

View File

@@ -1,49 +0,0 @@
#!/usr/bin/env node
/*
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 child_process = require('child_process'),
Q = require('q');
var isWindows = process.platform.slice(0, 3) == 'win';
// Takes a command and optional current working directory.
module.exports = function(cmd, args, opt_cwd) {
var d = Q.defer();
try {
// Work around spawn not being able to find .bat files.
if (isWindows) {
args.unshift('/s', '/c', cmd);
cmd = 'cmd';
}
var child = child_process.spawn(cmd, args, {cwd: opt_cwd, stdio: 'inherit'});
child.on('exit', function(code) {
if (code) {
d.reject('Error code ' + code + ' for command: ' + cmd + ' with args: ' + args);
} else {
d.resolve();
}
});
} catch(e) {
console.error('error caught: ' + e);
d.reject(e);
}
return d.promise;
}

View File

@@ -5,9 +5,9 @@
:: 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
@@ -18,7 +18,7 @@
@ECHO OFF
SET script_path="%~dp0log"
IF EXIST %script_path% (
node %script_path% %*
node "%script_path%" %*
) ELSE (
ECHO.
ECHO ERROR: Could not find 'log' script in 'cordova' folder, aborting...>&2

View File

@@ -5,9 +5,9 @@
:: 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
@@ -18,7 +18,7 @@
@ECHO OFF
SET script_path="%~dp0run"
IF EXIST %script_path% (
node %script_path% %*
node "%script_path%" %*
) ELSE (
ECHO.
ECHO ERROR: Could not find 'run' script in 'cordova' folder, aborting...>&2

View File

@@ -20,6 +20,6 @@
*/
// Coho updates this line:
var VERSION = "3.5.0-dev";
var VERSION = "3.2.0";
console.log(VERSION);

View File

@@ -5,9 +5,9 @@
:: 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
@@ -18,7 +18,7 @@
@ECHO OFF
SET script_path="%~dp0version"
IF EXIST %script_path% (
node %script_path% %*
node "%script_path%" %*
) ELSE (
ECHO.
ECHO ERROR: Could not find 'version' script in 'cordova' folder, aborting...>&2

View File

@@ -31,7 +31,7 @@ public class __ACTIVITY__ extends CordovaActivity
super.init();
// Set by <content src="index.html" /> in config.xml
super.loadUrl(Config.getStartUrl());
//super.loadUrl("file:///android_asset/www/index.html");
//super.loadUrl("file:///android_asset/www/index.html")
}
}

View File

@@ -29,12 +29,11 @@
/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<application android:icon="@drawable/icon" android:label="@string/app_name"
android:hardwareAccelerated="true">
<activity android:name="__ACTIVITY__" android:label="@string/app_name" android:launchMode="singleTop"
android:hardwareAccelerated="true"
android:debuggable="true">
<activity android:name="__ACTIVITY__" android:label="@string/app_name"
android:theme="@android:style/Theme.Black.NoTitleBar"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale">
<intent-filter>

View File

@@ -31,7 +31,7 @@ var app = {
// deviceready Event Handler
//
// The scope of 'this' is the event. In order to call the 'receivedEvent'
// function, we must explicitly call 'app.receivedEvent(...);'
// function, we must explicity call 'app.receivedEvent(...);'
onDeviceReady: function() {
app.receivedEvent('deviceready');
},

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project>
<target name="-pre-compile">
<!-- Fix library references due to bug in build.xml: See: https://groups.google.com/forum/#!topic/android-developers/0ivH-YqCjzg -->
<pathconvert property="fixedJarsPath" refid="project.all.jars.path">
<filtermapper>
<replacestring from="/bin/" to="/ant-build/"/>
<replacestring from="\bin\" to="\ant-build\"/>
</filtermapper>
</pathconvert>
<path id="project.all.jars.path">
<pathelement path="${fixedJarsPath}"/>
</path>
<echo message="Set jars path to: ${toString:project.all.jars.path}"/>
</target>
<target name="-post-build">
<move file="ant-build/AndroidManifest.xml" tofile="ant-build/AndroidManifest.cordova.xml" failonerror="false" overwrite="true" />
<move file="CordovaLib/ant-build/AndroidManifest.xml" tofile="CordovaLib/ant-build/AndroidManifest.cordova.xml" failonerror="false" overwrite="true" />
</target>
</project>

View File

@@ -1,45 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>__NAME__</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
<filteredResources>
<filter>
<id>1388696068187</id>
<name></name>
<type>10</type>
<matcher>
<id>org.eclipse.ui.ide.multiFilter</id>
<arguments>1.0-name-matches-false-true-CordovaLib|platform_www|cordova</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>

View File

@@ -1,71 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>__NAME__</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
<linkedResources>
<link>
<name>config.xml</name>
<type>1</type>
<locationURI>$%7BPARENT-2-PROJECT_LOC%7D/config.xml</locationURI>
</link>
<link>
<name>www</name>
<type>2</type>
<locationURI>$%7BPARENT-2-PROJECT_LOC%7D/www</locationURI>
</link>
<link>
<name>merges</name>
<type>2</type>
<locationURI>$%7BPARENT-2-PROJECT_LOC%7D/merges</locationURI>
</link>
</linkedResources>
<filteredResources>
<filter>
<id>1390880034107</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.ui.ide.multiFilter</id>
<arguments>1.0-projectRelativePath-matches-false-true-^(build.xml|ant-gen|ant-build|custom_rules.xml|CordovaLib|platform_www|cordova)</arguments>
</matcher>
</filter>
<filter>
<id>1390880034108</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.ui.ide.multiFilter</id>
<arguments>1.0-projectRelativePath-matches-false-true-^(assets/www|res/xml/config.xml)</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 493 KiB

View File

@@ -19,13 +19,18 @@
under the License.
*/
var path = require('path');
var args = process.argv;
var create = require('./lib/create');
var args = require('./lib/simpleargs').getArgs(process.argv);
if (args['--help'] || args._.length === 0) {
console.log('Usage: ' + path.relative(process.cwd(), path.join(__dirname, 'update')) + ' <path_to_project> [--shared]');
console.log(' --shared will use the CordovaLib project directly instead of making a copy.');
// Support basic help commands
if(args.length < 3 || (args[2] == '--help' || args[2] == '/?' || args[2] == '-h' ||
args[2] == 'help' || args[2] == '-help' || args[2] == '/help')) {
console.log('Usage: ' + path.relative(process.cwd(), path.join(__dirname, 'update')) + ' <path_to_project>');
process.exit(1);
} else {
create.updateProject(args[2]).done(null, function(err) {
console.error(err);
process.exit(2);
});
}
create.updateProject(args._[0], args['--shared']).done();

View File

@@ -1,9 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>

View File

@@ -1,4 +0,0 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
org.eclipse.jdt.core.compiler.compliance=1.6
org.eclipse.jdt.core.compiler.source=1.6

View File

@@ -17,7 +17,43 @@
specific language governing permissions and limitations
under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:windowSoftInputMode="adjustPan"
package="org.apache.cordova" android:versionName="1.0" android:versionCode="1">
<supports-screens
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true"
android:resizeable="true"
android:anyDensity="true"
/>
<!-- android:xlargeScreens="true" screen supported only after Android-9 -->
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.RECORD_VIDEO"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<application android:icon="@drawable/icon" android:label="@string/app_name"
android:debuggable="true">
<activity android:name="org.apache.cordova.DroidGap" android:label="@string/app_name"
android:configChanges="orientation|keyboardHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<uses-sdk android:minSdkVersion="8" />
</manifest>

View File

@@ -1,5 +1,5 @@
// Platform: android
// 3.5.0-dev-81f9a00
// 3.2.0
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
@@ -19,8 +19,8 @@
under the License.
*/
;(function() {
var CORDOVA_JS_BUILD_LABEL = '3.5.0-dev-81f9a00';
// file: src/scripts/require.js
var CORDOVA_JS_BUILD_LABEL = '3.2.0';
// file: lib/scripts/require.js
/*jshint -W079 */
/*jshint -W020 */
@@ -34,7 +34,7 @@ var require,
requireStack = [],
// Map of module ID -> index into requireStack of modules currently being built.
inProgressModules = {},
SEPARATOR = ".";
SEPERATOR = ".";
@@ -44,7 +44,7 @@ var require,
var resultantId = id;
//Its a relative path, so lop off the last portion and add the id (minus "./")
if (id.charAt(0) === ".") {
resultantId = module.id.slice(0, module.id.lastIndexOf(SEPARATOR)) + SEPARATOR + id.slice(2);
resultantId = module.id.slice(0, module.id.lastIndexOf(SEPERATOR)) + SEPERATOR + id.slice(2);
}
return require(resultantId);
};
@@ -98,7 +98,7 @@ if (typeof module === "object" && typeof require === "function") {
module.exports.define = define;
}
// file: src/cordova.js
// file: lib/cordova.js
define("cordova", function(require, exports, module) {
@@ -316,7 +316,7 @@ module.exports = cordova;
});
// file: src/android/android/nativeapiprovider.js
// file: lib/android/android/nativeapiprovider.js
define("cordova/android/nativeapiprovider", function(require, exports, module) {
/**
@@ -339,7 +339,7 @@ module.exports = {
});
// file: src/android/android/promptbasednativeapi.js
// file: lib/android/android/promptbasednativeapi.js
define("cordova/android/promptbasednativeapi", function(require, exports, module) {
/**
@@ -361,7 +361,7 @@ module.exports = {
});
// file: src/common/argscheck.js
// file: lib/common/argscheck.js
define("cordova/argscheck", function(require, exports, module) {
var exec = require('cordova/exec');
@@ -427,7 +427,7 @@ moduleExports.enableChecks = true;
});
// file: src/common/base64.js
// file: lib/common/base64.js
define("cordova/base64", function(require, exports, module) {
var base64 = exports;
@@ -437,16 +437,6 @@ base64.fromArrayBuffer = function(arrayBuffer) {
return uint8ToBase64(array);
};
base64.toArrayBuffer = function(str) {
var decodedStr = typeof atob != 'undefined' ? atob(str) : new Buffer(str,'base64').toString('binary');
var arrayBuffer = new ArrayBuffer(decodedStr.length);
var array = new Uint8Array(arrayBuffer);
for (var i=0, len=decodedStr.length; i < len; i++) {
array[i] = decodedStr.charCodeAt(i);
}
return arrayBuffer;
};
//------------------------------------------------------------------------------
/* This code is based on the performance tests at http://jsperf.com/b64tests
@@ -493,7 +483,7 @@ function uint8ToBase64(rawData) {
});
// file: src/common/builder.js
// file: lib/common/builder.js
define("cordova/builder", function(require, exports, module) {
var utils = require('cordova/utils');
@@ -562,7 +552,7 @@ function include(parent, objects, clobber, merge) {
include(result, obj.children, clobber, merge);
}
} catch(e) {
utils.alert('Exception building Cordova JS globals: ' + e + ' for key "' + key + '"');
utils.alert('Exception building cordova JS globals: ' + e + ' for key "' + key + '"');
}
});
}
@@ -606,7 +596,7 @@ exports.replaceHookForTesting = function() {};
});
// file: src/common/channel.js
// file: lib/common/channel.js
define("cordova/channel", function(require, exports, module) {
var utils = require('cordova/utils'),
@@ -847,7 +837,7 @@ module.exports = channel;
});
// file: src/android/exec.js
// file: lib/android/exec.js
define("cordova/exec", function(require, exports, module) {
/**
@@ -929,7 +919,7 @@ function androidExec(success, fail, service, action, args) {
androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);
return;
} else {
androidExec.processMessages(messages, true);
androidExec.processMessages(messages);
}
}
}
@@ -973,6 +963,7 @@ androidExec.nativeToJsModes = nativeToJsModes;
androidExec.setJsToNativeBridgeMode = function(mode) {
if (mode == jsToNativeModes.JS_OBJECT && !window._cordovaNative) {
console.log('Falling back on PROMPT mode since _cordovaNative is missing. Expected for Android 3.2 and lower only.');
mode = jsToNativeModes.PROMPT;
}
nativeApiProvider.setPreferPrompt(mode == jsToNativeModes.PROMPT);
@@ -1037,69 +1028,53 @@ function processMessage(message) {
}
cordova.callbackFromNative(callbackId, success, status, [payload], keepCallback);
} else {
console.log("processMessage failed: invalid message: " + JSON.stringify(message));
console.log("processMessage failed: invalid message:" + message);
}
} catch (e) {
console.log("processMessage failed: Message: " + message);
console.log("processMessage failed: Error: " + e);
console.log("processMessage failed: Stack: " + e.stack);
console.log("processMessage failed: Message: " + message);
}
}
var isProcessing = false;
// This is called from the NativeToJsMessageQueue.java.
androidExec.processMessages = function(messages, opt_useTimeout) {
androidExec.processMessages = function(messages) {
if (messages) {
messagesFromNative.push(messages);
}
// Check for the reentrant case.
if (isProcessing) {
return;
}
if (opt_useTimeout) {
window.setTimeout(androidExec.processMessages, 0);
return;
}
isProcessing = true;
try {
// TODO: add setImmediate polyfill and process only one message at a time.
// Check for the reentrant case, and enqueue the message if that's the case.
if (messagesFromNative.length > 1) {
return;
}
while (messagesFromNative.length) {
var msg = popMessageFromQueue();
// Don't unshift until the end so that reentrancy can be detected.
messages = messagesFromNative[0];
// The Java side can send a * message to indicate that it
// still has messages waiting to be retrieved.
if (msg == '*' && messagesFromNative.length === 0) {
setTimeout(pollOnce, 0);
if (messages == '*') {
messagesFromNative.shift();
window.setTimeout(pollOnce, 0);
return;
}
processMessage(msg);
var spaceIdx = messages.indexOf(' ');
var msgLen = +messages.slice(0, spaceIdx);
var message = messages.substr(spaceIdx + 1, msgLen);
messages = messages.slice(spaceIdx + msgLen + 1);
processMessage(message);
if (messages) {
messagesFromNative[0] = messages;
} else {
messagesFromNative.shift();
}
}
} finally {
isProcessing = false;
}
};
function popMessageFromQueue() {
var messageBatch = messagesFromNative.shift();
if (messageBatch == '*') {
return '*';
}
var spaceIdx = messageBatch.indexOf(' ');
var msgLen = +messageBatch.slice(0, spaceIdx);
var message = messageBatch.substr(spaceIdx + 1, msgLen);
messageBatch = messageBatch.slice(spaceIdx + msgLen + 1);
if (messageBatch) {
messagesFromNative.unshift(messageBatch);
}
return message;
}
module.exports = androidExec;
});
// file: src/common/exec/proxy.js
// file: lib/common/exec/proxy.js
define("cordova/exec/proxy", function(require, exports, module) {
@@ -1129,7 +1104,7 @@ module.exports = {
};
});
// file: src/common/init.js
// file: lib/common/init.js
define("cordova/init", function(require, exports, module) {
var channel = require('cordova/channel');
@@ -1216,13 +1191,9 @@ modulemapper.clobbers('cordova/exec', 'Cordova.exec');
// Call the platform-specific initialization.
platform.bootstrap && platform.bootstrap();
// Wrap in a setTimeout to support the use-case of having plugin JS appended to cordova.js.
// The delay allows the attached modules to be defined before the plugin loader looks for them.
setTimeout(function() {
pluginloader.load(function() {
channel.onPluginsReady.fire();
});
}, 0);
pluginloader.load(function() {
channel.onPluginsReady.fire();
});
/**
* Create all cordova objects once native side is ready.
@@ -1247,7 +1218,7 @@ channel.join(function() {
});
// file: src/common/modulemapper.js
// file: lib/common/modulemapper.js
define("cordova/modulemapper", function(require, exports, module) {
var builder = require('cordova/builder'),
@@ -1348,7 +1319,7 @@ exports.reset();
});
// file: src/android/platform.js
// file: lib/android/platform.js
define("cordova/platform", function(require, exports, module) {
module.exports = {
@@ -1389,7 +1360,7 @@ module.exports = {
});
// file: src/android/plugin/android/app.js
// file: lib/android/plugin/android/app.js
define("cordova/plugin/android/app", function(require, exports, module) {
var exec = require('cordova/exec');
@@ -1466,57 +1437,48 @@ module.exports = {
});
// file: src/common/pluginloader.js
// file: lib/common/pluginloader.js
define("cordova/pluginloader", function(require, exports, module) {
var modulemapper = require('cordova/modulemapper');
var urlutil = require('cordova/urlutil');
// Helper function to inject a <script> tag.
// Exported for testing.
exports.injectScript = function(url, onload, onerror) {
function injectScript(url, onload, onerror) {
var script = document.createElement("script");
// onload fires even when script fails loads with an error.
script.onload = onload;
// onerror fires for malformed URLs.
script.onerror = onerror;
script.onerror = onerror || onload;
script.src = url;
document.head.appendChild(script);
};
function injectIfNecessary(id, url, onload, onerror) {
onerror = onerror || onload;
if (id in define.moduleMap) {
onload();
} else {
exports.injectScript(url, function() {
if (id in define.moduleMap) {
onload();
} else {
onerror();
}
}, onerror);
}
}
function onScriptLoadingComplete(moduleList, finishPluginLoading) {
// Loop through all the plugins and then through their clobbers and merges.
for (var i = 0, module; module = moduleList[i]; i++) {
if (module.clobbers && module.clobbers.length) {
for (var j = 0; j < module.clobbers.length; j++) {
modulemapper.clobbers(module.id, module.clobbers[j]);
}
}
if (module) {
try {
if (module.clobbers && module.clobbers.length) {
for (var j = 0; j < module.clobbers.length; j++) {
modulemapper.clobbers(module.id, module.clobbers[j]);
}
}
if (module.merges && module.merges.length) {
for (var k = 0; k < module.merges.length; k++) {
modulemapper.merges(module.id, module.merges[k]);
}
}
if (module.merges && module.merges.length) {
for (var k = 0; k < module.merges.length; k++) {
modulemapper.merges(module.id, module.merges[k]);
}
}
// Finally, if runs is truthy we want to simply require() the module.
if (module.runs) {
modulemapper.runs(module.id);
// Finally, if runs is truthy we want to simply require() the module.
// This can be skipped if it had any merges or clobbers, though,
// since the mapper will already have required the module.
if (module.runs && !(module.clobbers && module.clobbers.length) && !(module.merges && module.merges.length)) {
modulemapper.runs(module.id);
}
}
catch(err) {
// error with module, most likely clobbers, should we continue?
}
}
}
@@ -1542,16 +1504,29 @@ function handlePluginsObject(path, moduleList, finishPluginLoading) {
}
for (var i = 0; i < moduleList.length; i++) {
injectIfNecessary(moduleList[i].id, path + moduleList[i].file, scriptLoadedCallback);
injectScript(path + moduleList[i].file, scriptLoadedCallback);
}
}
function injectPluginScript(pathPrefix, finishPluginLoading) {
injectScript(pathPrefix + 'cordova_plugins.js', function(){
try {
var moduleList = require("cordova/plugin_list");
handlePluginsObject(pathPrefix, moduleList, finishPluginLoading);
} catch (e) {
// Error loading cordova_plugins.js, file not found or something
// this is an acceptable error, pre-3.0.0, so we just move on.
finishPluginLoading();
}
}, finishPluginLoading); // also, add script load error handler for file not found
}
function findCordovaPath() {
var path = null;
var scripts = document.getElementsByTagName('script');
var term = 'cordova.js';
for (var n = scripts.length-1; n>-1; n--) {
var src = scripts[n].src.replace(/\?.*$/, ''); // Strip any query param (CB-6007).
var src = scripts[n].src;
if (src.indexOf(term) == (src.length - term.length)) {
path = src.substring(0, src.length - term.length);
break;
@@ -1569,33 +1544,30 @@ exports.load = function(callback) {
console.log('Could not find cordova.js script tag. Plugin loading may fail.');
pathPrefix = '';
}
injectIfNecessary('cordova/plugin_list', pathPrefix + 'cordova_plugins.js', function() {
var moduleList = require("cordova/plugin_list");
handlePluginsObject(pathPrefix, moduleList, callback);
}, callback);
injectPluginScript(pathPrefix, callback);
};
});
// file: src/common/urlutil.js
// file: lib/common/urlutil.js
define("cordova/urlutil", function(require, exports, module) {
var urlutil = exports;
var anchorEl = document.createElement('a');
/**
* For already absolute URLs, returns what is passed in.
* For relative URLs, converts them to absolute ones.
*/
exports.makeAbsolute = function makeAbsolute(url) {
var anchorEl = document.createElement('a');
urlutil.makeAbsolute = function(url) {
anchorEl.href = url;
return anchorEl.href;
};
});
// file: src/common/utils.js
// file: lib/common/utils.js
define("cordova/utils", function(require, exports, module) {
var utils = exports;
@@ -1766,7 +1738,7 @@ function UUIDcreatePart(length) {
});
window.cordova = require('cordova');
// file: src/scripts/bootstrap.js
// file: lib/scripts/bootstrap.js
require('cordova/init');

View File

@@ -95,13 +95,6 @@
<target name="-post-compile">
</target>
-->
<target name="-pre-clean">
<!-- delete generated javadoc -->
<delete dir="javadoc-public" failonerror="false" />
<delete dir="javadoc-private" failonerror="false" />
<!-- delete generated jar -->
<delete file="cordova-${version}.jar" failonerror="false" />
</target>
<!-- Import the actual build file.
@@ -127,31 +120,7 @@
that includes all JavaScript code.
-->
<target name="jar" depends="-compile">
<jar
basedir="bin/classes"
excludes="org/apache/cordova/R.class,org/apache/cordova/R$*.class"
jarfile="cordova-${version}.jar" />
</target>
<target name="javadoc">
<delete dir="javadoc-public" failonerror="false" />
<javadoc
access="public"
destdir="javadoc-public"
classpath="${sdk.dir}/platforms/${target}/android.jar">
<packageset dir="src">
<include name="org/apache/cordova/**" />
</packageset>
</javadoc>
<delete dir="javadoc-private" failonerror="false" />
<javadoc
access="private"
destdir="javadoc-private"
classpath="${sdk.dir}/platforms/${target}/android.jar">
<packageset dir="src">
<include name="org/apache/cordova/**" />
</packageset>
</javadoc>
<jar jarfile="cordova-${version}.jar" basedir="bin/classes" excludes="org/apache/cordova/R.class,org/apache/cordova/R$*.class"/>
</target>
<!-- tests for Java files -->

View File

@@ -10,7 +10,7 @@
# Indicates whether an apk should be generated for each density.
split.density=false
# Project target.
target=android-19
target=android-18
apk-configurations=
renderscript.opt.level=O0
android.library=true

View File

@@ -36,7 +36,6 @@
<content src="index.html" />
<preference name="loglevel" value="DEBUG" />
<!--
<preference name="splashscreen" value="resourceName" />
<preference name="backgroundColor" value="0xFFF" />

33
framework/src/com/squareup/okhttp/Address.java Executable file → Normal file
View File

@@ -15,10 +15,8 @@
*/
package com.squareup.okhttp;
import com.squareup.okhttp.internal.Util;
import java.net.Proxy;
import java.net.UnknownHostException;
import java.util.List;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
@@ -40,23 +38,16 @@ public final class Address {
final int uriPort;
final SSLSocketFactory sslSocketFactory;
final HostnameVerifier hostnameVerifier;
final OkAuthenticator authenticator;
final List<String> transports;
public Address(String uriHost, int uriPort, SSLSocketFactory sslSocketFactory,
HostnameVerifier hostnameVerifier, OkAuthenticator authenticator, Proxy proxy,
List<String> transports) throws UnknownHostException {
HostnameVerifier hostnameVerifier, Proxy proxy) throws UnknownHostException {
if (uriHost == null) throw new NullPointerException("uriHost == null");
if (uriPort <= 0) throw new IllegalArgumentException("uriPort <= 0: " + uriPort);
if (authenticator == null) throw new IllegalArgumentException("authenticator == null");
if (transports == null) throw new IllegalArgumentException("transports == null");
this.proxy = proxy;
this.uriHost = uriHost;
this.uriPort = uriPort;
this.sslSocketFactory = sslSocketFactory;
this.hostnameVerifier = hostnameVerifier;
this.authenticator = authenticator;
this.transports = Util.immutableList(transports);
}
/** Returns the hostname of the origin server. */
@@ -88,22 +79,6 @@ public final class Address {
return hostnameVerifier;
}
/**
* Returns the client's authenticator. This method never returns null.
*/
public OkAuthenticator getAuthenticator() {
return authenticator;
}
/**
* Returns the client's transports. This method always returns a non-null list
* that contains "http/1.1", possibly among other transports.
*/
public List<String> getTransports() {
return transports;
}
/**
* Returns this address's explicitly-specified HTTP proxy, or null to
* delegate to the HTTP client's proxy selector.
@@ -119,9 +94,7 @@ public final class Address {
&& this.uriHost.equals(that.uriHost)
&& this.uriPort == that.uriPort
&& equal(this.sslSocketFactory, that.sslSocketFactory)
&& equal(this.hostnameVerifier, that.hostnameVerifier)
&& equal(this.authenticator, that.authenticator)
&& equal(this.transports, that.transports);
&& equal(this.hostnameVerifier, that.hostnameVerifier);
}
return false;
}
@@ -132,9 +105,7 @@ public final class Address {
result = 31 * result + uriPort;
result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0);
result = 31 * result + (hostnameVerifier != null ? hostnameVerifier.hashCode() : 0);
result = 31 * result + (authenticator != null ? authenticator.hashCode() : 0);
result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
result = 31 * result + transports.hashCode();
return result;
}
}

78
framework/src/com/squareup/okhttp/Connection.java Executable file → Normal file
View File

@@ -31,7 +31,6 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.net.Proxy;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.Arrays;
import javax.net.ssl.SSLSocket;
@@ -93,20 +92,24 @@ public final class Connection implements Closeable {
public void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest)
throws IOException {
if (connected) throw new IllegalStateException("already connected");
if (connected) {
throw new IllegalStateException("already connected");
}
connected = true;
socket = (route.proxy.type() != Proxy.Type.HTTP) ? new Socket(route.proxy) : new Socket();
Platform.get().connectSocket(socket, route.inetSocketAddress, connectTimeout);
socket.connect(route.inetSocketAddress, connectTimeout);
socket.setSoTimeout(readTimeout);
in = socket.getInputStream();
out = socket.getOutputStream();
if (route.address.sslSocketFactory != null) {
upgradeToTls(tunnelRequest);
} else {
streamWrapper();
}
connected = true;
// Use MTU-sized buffers to send fewer packets.
int mtu = Platform.get().getMtu(socket);
in = new BufferedInputStream(in, mtu);
out = new BufferedOutputStream(out, mtu);
}
/**
@@ -131,8 +134,7 @@ public final class Connection implements Closeable {
platform.supportTlsIntolerantServer(sslSocket);
}
boolean useNpn = route.modernTls && route.address.transports.contains("spdy/3");
if (useNpn) {
if (route.modernTls) {
platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS);
}
@@ -146,15 +148,14 @@ public final class Connection implements Closeable {
out = sslSocket.getOutputStream();
in = sslSocket.getInputStream();
streamWrapper();
byte[] selectedProtocol;
if (useNpn && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
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();
spdyConnection.sendConnectionHeader();
} else if (!Arrays.equals(selectedProtocol, HTTP_11)) {
throw new IOException(
"Unexpected NPN transport " + new String(selectedProtocol, "ISO-8859-1"));
@@ -189,39 +190,6 @@ public final class Connection implements Closeable {
return !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown();
}
/**
* Returns true if we are confident that we can read data from this
* connection. This is more expensive and more accurate than {@link
* #isAlive()}; callers should check {@link #isAlive()} first.
*/
public boolean isReadable() {
if (!(in instanceof BufferedInputStream)) {
return true; // Optimistic.
}
if (isSpdy()) {
return true; // Optimistic. We can't test SPDY because its streams are in use.
}
BufferedInputStream bufferedInputStream = (BufferedInputStream) in;
try {
int readTimeout = socket.getSoTimeout();
try {
socket.setSoTimeout(1);
bufferedInputStream.mark(1);
if (bufferedInputStream.read() == -1) {
return false; // Stream is exhausted; socket is closed.
}
bufferedInputStream.reset();
return true;
} finally {
socket.setSoTimeout(readTimeout);
}
} catch (SocketTimeoutException ignored) {
return true; // Read timed out; socket is good.
} catch (IOException e) {
return false; // Couldn't read; socket is closed.
}
}
public void resetIdleStartTime() {
if (spdyConnection != null) {
throw new IllegalStateException("spdyConnection != null");
@@ -239,7 +207,7 @@ public final class Connection implements Closeable {
* {@code keepAliveDurationNs}.
*/
public boolean isExpired(long keepAliveDurationNs) {
return getIdleStartTimeNs() < System.nanoTime() - keepAliveDurationNs;
return isIdle() && System.nanoTime() - getIdleStartTimeNs() > keepAliveDurationNs;
}
/**
@@ -252,8 +220,7 @@ public final class Connection implements Closeable {
/** Returns the transport appropriate for this connection. */
public Object newTransport(HttpEngine httpEngine) throws IOException {
return (spdyConnection != null)
? new SpdyTransport(httpEngine, spdyConnection)
return (spdyConnection != null) ? new SpdyTransport(httpEngine, spdyConnection)
: new HttpTransport(httpEngine, out, in);
}
@@ -291,11 +258,6 @@ public final class Connection implements Closeable {
return route.address.sslSocketFactory != null && route.proxy.type() == Proxy.Type.HTTP;
}
public void updateReadTimeout(int newTimeout) throws IOException {
if (!connected) throw new IllegalStateException("updateReadTimeout - not connected");
socket.setSoTimeout(newTimeout);
}
/**
* To make an HTTPS connection over an HTTP proxy, send an unencrypted
* CONNECT request to create the proxy connection. This may need to be
@@ -313,9 +275,8 @@ public final class Connection implements Closeable {
case HTTP_PROXY_AUTH:
requestHeaders = new RawHeaders(requestHeaders);
URL url = new URL("https", tunnelRequest.host, tunnelRequest.port, "/");
boolean credentialsFound = HttpAuthenticator.processAuthHeader(
route.address.authenticator, HTTP_PROXY_AUTH, responseHeaders, requestHeaders,
route.proxy, url);
boolean credentialsFound = HttpAuthenticator.processAuthHeader(HTTP_PROXY_AUTH,
responseHeaders, requestHeaders, route.proxy, url);
if (credentialsFound) {
continue;
} else {
@@ -327,9 +288,4 @@ public final class Connection implements Closeable {
}
}
}
private void streamWrapper() throws IOException {
in = new BufferedInputStream(in, 4096);
out = new BufferedOutputStream(out, 256);
}
}

9
framework/src/com/squareup/okhttp/ConnectionPool.java Executable file → Normal file
View File

@@ -80,9 +80,8 @@ public class ConnectionPool {
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>(),
Util.daemonThreadFactory("OkHttp ConnectionPool"));
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);
@@ -216,6 +215,8 @@ public class ConnectionPool {
* <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;
}
@@ -238,8 +239,6 @@ public class ConnectionPool {
connections.addFirst(connection);
connection.resetIdleStartTime();
}
executorService.submit(connectionsCleanupCallable);
}
/**

View File

@@ -1,86 +0,0 @@
/*
* 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 com.squareup.okhttp.internal.http.ResponseHeaders;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
final class Dispatcher {
// TODO: thread pool size should be configurable; possibly configurable per host.
private final ThreadPoolExecutor executorService = new ThreadPoolExecutor(
8, 8, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
private final Map<Object, List<Job>> enqueuedJobs = new LinkedHashMap<Object, List<Job>>();
public synchronized void enqueue(
OkHttpClient client, Request request, Response.Receiver responseReceiver) {
Job job = new Job(this, client, request, responseReceiver);
List<Job> jobsForTag = enqueuedJobs.get(request.tag());
if (jobsForTag == null) {
jobsForTag = new ArrayList<Job>(2);
enqueuedJobs.put(request.tag(), jobsForTag);
}
jobsForTag.add(job);
executorService.execute(job);
}
public synchronized void cancel(Object tag) {
List<Job> jobs = enqueuedJobs.remove(tag);
if (jobs == null) return;
for (Job job : jobs) {
executorService.remove(job);
}
}
synchronized void finished(Job job) {
List<Job> jobs = enqueuedJobs.get(job.tag());
if (jobs != null) jobs.remove(job);
}
static class RealResponseBody extends Response.Body {
private final ResponseHeaders responseHeaders;
private final InputStream in;
RealResponseBody(ResponseHeaders responseHeaders, InputStream in) {
this.responseHeaders = responseHeaders;
this.in = in;
}
@Override public boolean ready() throws IOException {
return true;
}
@Override public MediaType contentType() {
String contentType = responseHeaders.getContentType();
return contentType != null ? MediaType.parse(contentType) : null;
}
@Override public long contentLength() {
return responseHeaders.getContentLength();
}
@Override public InputStream byteStream() throws IOException {
return in;
}
}
}

View File

@@ -1,59 +0,0 @@
/*
* 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;
/**
* A failure attempting to retrieve an HTTP response.
*
* <h3>Warning: Experimental OkHttp 2.0 API</h3>
* This class is in beta. APIs are subject to change!
*/
/* OkHttp 2.0: public */ class Failure {
private final Request request;
private final Throwable exception;
private Failure(Builder builder) {
this.request = builder.request;
this.exception = builder.exception;
}
public Request request() {
return request;
}
public Throwable exception() {
return exception;
}
public static class Builder {
private Request request;
private Throwable exception;
public Builder request(Request request) {
this.request = request;
return this;
}
public Builder exception(Throwable exception) {
this.exception = exception;
return this;
}
public Failure build() {
return new Failure(this);
}
}
}

119
framework/src/com/squareup/okhttp/HttpResponseCache.java Executable file → Normal file
View File

@@ -22,8 +22,8 @@ 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.HttpsEngine;
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;
@@ -35,6 +35,7 @@ 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;
@@ -43,6 +44,8 @@ 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;
@@ -52,8 +55,8 @@ 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 javax.net.ssl.SSLSocket;
import static com.squareup.okhttp.internal.Util.US_ASCII;
import static com.squareup.okhttp.internal.Util.UTF_8;
@@ -116,6 +119,9 @@ import static com.squareup.okhttp.internal.Util.UTF_8;
* }</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;
@@ -147,10 +153,6 @@ public final class HttpResponseCache extends ResponseCache {
return HttpResponseCache.this.put(uri, connection);
}
@Override public void maybeRemove(String requestMethod, URI uri) throws IOException {
HttpResponseCache.this.maybeRemove(requestMethod, uri);
}
@Override public void update(
CacheResponse conditionalCacheHit, HttpURLConnection connection) throws IOException {
HttpResponseCache.this.update(conditionalCacheHit, connection);
@@ -170,7 +172,26 @@ public final class HttpResponseCache extends ResponseCache {
}
private String uriToKey(URI uri) {
return Util.hash(uri.toString());
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,
@@ -205,11 +226,17 @@ public final class HttpResponseCache extends ResponseCache {
HttpURLConnection httpConnection = (HttpURLConnection) urlConnection;
String requestMethod = httpConnection.getRequestMethod();
String key = uriToKey(uri);
if (maybeRemove(requestMethod, 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;
}
if (!requestMethod.equals("GET")) {
} 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.
@@ -232,7 +259,7 @@ public final class HttpResponseCache extends ResponseCache {
Entry entry = new Entry(uri, varyHeaders, httpConnection);
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(uriToKey(uri));
editor = cache.edit(key);
if (editor == null) {
return null;
}
@@ -244,23 +271,6 @@ public final class HttpResponseCache extends ResponseCache {
}
}
/**
* Returns true if the supplied {@code requestMethod} potentially invalidates an entry in the
* cache.
*/
private boolean maybeRemove(String requestMethod, URI uri) {
if (requestMethod.equals("POST") || requestMethod.equals("PUT") || requestMethod.equals(
"DELETE")) {
try {
cache.remove(uriToKey(uri));
} catch (IOException ignored) {
// The cache cannot be written.
}
return true;
}
return false;
}
private void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
throws IOException {
HttpEngine httpEngine = getHttpEngine(httpConnection);
@@ -321,30 +331,6 @@ public final class HttpResponseCache extends ResponseCache {
return writeSuccessCount;
}
public long getSize() {
return cache.size();
}
public long getMaxSize() {
return cache.getMaxSize();
}
public void flush() throws IOException {
cache.flush();
}
public void close() throws IOException {
cache.close();
}
public File getDirectory() {
return cache.getDirectory();
}
public boolean isClosed() {
return cache.isClosed();
}
private synchronized void trackResponse(ResponseSource source) {
requestCount++;
@@ -397,7 +383,8 @@ public final class HttpResponseCache extends ResponseCache {
editor.commit();
}
@Override public void write(byte[] buffer, int offset, int length) throws IOException {
@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);
@@ -526,16 +513,16 @@ public final class HttpResponseCache extends ResponseCache {
this.requestMethod = httpConnection.getRequestMethod();
this.responseHeaders = RawHeaders.fromMultimap(httpConnection.getHeaderFields(), true);
SSLSocket sslSocket = getSslSocket(httpConnection);
if (sslSocket != null) {
cipherSuite = sslSocket.getSession().getCipherSuite();
if (isHttps()) {
HttpsURLConnection httpsConnection = (HttpsURLConnection) httpConnection;
cipherSuite = httpsConnection.getCipherSuite();
Certificate[] peerCertificatesNonFinal = null;
try {
peerCertificatesNonFinal = sslSocket.getSession().getPeerCertificates();
peerCertificatesNonFinal = httpsConnection.getServerCertificates();
} catch (SSLPeerUnverifiedException ignored) {
}
peerCertificates = peerCertificatesNonFinal;
localCertificates = sslSocket.getSession().getLocalCertificates();
localCertificates = httpsConnection.getLocalCertificates();
} else {
cipherSuite = null;
peerCertificates = null;
@@ -543,22 +530,6 @@ public final class HttpResponseCache extends ResponseCache {
}
}
/**
* Returns the SSL socket used by {@code httpConnection} for HTTPS, nor null
* if the connection isn't using HTTPS. Since we permit redirects across
* protocols (HTTP to HTTPS or vice versa), the implementation type of the
* connection doesn't necessarily match the implementation type of its HTTP
* engine.
*/
private SSLSocket getSslSocket(HttpURLConnection httpConnection) {
HttpEngine engine = httpConnection instanceof HttpsURLConnectionImpl
? ((HttpsURLConnectionImpl) httpConnection).getHttpEngine()
: ((HttpURLConnectionImpl) httpConnection).getHttpEngine();
return engine instanceof HttpsEngine
? ((HttpsEngine) engine).getSslSocket()
: null;
}
public void writeTo(DiskLruCache.Editor editor) throws IOException {
OutputStream out = editor.newOutputStream(ENTRY_METADATA);
Writer writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));

View File

@@ -1,232 +0,0 @@
/*
* 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 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.HttpsEngine;
import com.squareup.okhttp.internal.http.Policy;
import com.squareup.okhttp.internal.http.RawHeaders;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.URL;
import static com.squareup.okhttp.internal.Util.getEffectivePort;
import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_MOVED_PERM;
import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_MOVED_TEMP;
import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_MULT_CHOICE;
import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_PROXY_AUTH;
import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_SEE_OTHER;
import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_TEMP_REDIRECT;
import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_UNAUTHORIZED;
final class Job implements Runnable, Policy {
private final Dispatcher dispatcher;
private final OkHttpClient client;
private final Response.Receiver responseReceiver;
/** The request; possibly a consequence of redirects or auth headers. */
private Request request;
public Job(Dispatcher dispatcher, OkHttpClient client, Request request,
Response.Receiver responseReceiver) {
this.dispatcher = dispatcher;
this.client = client;
this.request = request;
this.responseReceiver = responseReceiver;
}
@Override public int getChunkLength() {
return request.body().contentLength() == -1 ? HttpTransport.DEFAULT_CHUNK_LENGTH : -1;
}
@Override public long getFixedContentLength() {
return request.body().contentLength();
}
@Override public boolean getUseCaches() {
return false; // TODO.
}
@Override public HttpURLConnection getHttpConnectionToCache() {
return null;
}
@Override public URL getURL() {
return request.url();
}
@Override public long getIfModifiedSince() {
return 0; // For HttpURLConnection only. We let the cache drive this.
}
@Override public boolean usingProxy() {
return false; // We let the connection decide this.
}
@Override public void setSelectedProxy(Proxy proxy) {
// Do nothing.
}
Object tag() {
return request.tag();
}
@Override public void run() {
try {
Response response = execute();
responseReceiver.onResponse(response);
} catch (IOException e) {
responseReceiver.onFailure(new Failure.Builder()
.request(request)
.exception(e)
.build());
} finally {
// TODO: close the response body
// TODO: release the HTTP engine (potentially multiple!)
dispatcher.finished(this);
}
}
private Response execute() throws IOException {
Connection connection = null;
Response redirectedBy = null;
while (true) {
HttpEngine engine = newEngine(connection);
Request.Body body = request.body();
if (body != null) {
MediaType contentType = body.contentType();
if (contentType == null) throw new IllegalStateException("contentType == null");
if (engine.getRequestHeaders().getContentType() == null) {
engine.getRequestHeaders().setContentType(contentType.toString());
}
}
engine.sendRequest();
if (body != null) {
body.writeTo(engine.getRequestBody());
}
engine.readResponse();
int responseCode = engine.getResponseCode();
Dispatcher.RealResponseBody responseBody = new Dispatcher.RealResponseBody(
engine.getResponseHeaders(), engine.getResponseBody());
Response response = new Response.Builder(request, responseCode)
.rawHeaders(engine.getResponseHeaders().getHeaders())
.body(responseBody)
.redirectedBy(redirectedBy)
.build();
Request redirect = processResponse(engine, response);
if (redirect == null) {
engine.automaticallyReleaseConnectionToPool();
return response;
}
// TODO: fail if too many redirects
// TODO: fail if not following redirects
// TODO: release engine
connection = sameConnection(request, redirect) ? engine.getConnection() : null;
redirectedBy = response;
request = redirect;
}
}
HttpEngine newEngine(Connection connection) throws IOException {
String protocol = request.url().getProtocol();
RawHeaders requestHeaders = request.rawHeaders();
if (protocol.equals("http")) {
return new HttpEngine(client, this, request.method(), requestHeaders, connection, null);
} else if (protocol.equals("https")) {
return new HttpsEngine(client, this, request.method(), requestHeaders, connection, null);
} else {
throw new AssertionError();
}
}
/**
* Figures out the HTTP request to make in response to receiving {@code
* response}. This will either add authentication headers or follow
* redirects. If a follow-up is either unnecessary or not applicable, this
* returns null.
*/
private Request processResponse(HttpEngine engine, Response response) throws IOException {
Request request = response.request();
Proxy selectedProxy = engine.getConnection() != null
? engine.getConnection().getRoute().getProxy()
: client.getProxy();
int responseCode = response.code();
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:
RawHeaders successorRequestHeaders = request.rawHeaders();
boolean credentialsFound = HttpAuthenticator.processAuthHeader(client.getAuthenticator(),
response.code(), response.rawHeaders(), successorRequestHeaders, selectedProxy,
this.request.url());
return credentialsFound
? request.newBuilder().rawHeaders(successorRequestHeaders).build()
: null;
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:
case HTTP_MOVED_TEMP:
case HTTP_SEE_OTHER:
case HTTP_TEMP_REDIRECT:
String method = request.method();
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 null;
}
String location = response.header("Location");
if (location == null) {
return null;
}
URL url = new URL(request.url(), location);
if (!url.getProtocol().equals("https") && !url.getProtocol().equals("http")) {
return null; // Don't follow redirects to unsupported protocols.
}
return this.request.newBuilder().url(url).build();
default:
return null;
}
}
private boolean sameConnection(Request a, Request b) {
return a.url().getHost().equals(b.url().getHost())
&& getEffectivePort(a.url()) == getEffectivePort(b.url())
&& a.url().getProtocol().equals(b.url().getProtocol());
}
}

View File

@@ -1,120 +0,0 @@
/*
* 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.nio.charset.Charset;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* An <a href="http://tools.ietf.org/html/rfc2045">RFC 2045</a> Media Type,
* appropriate to describe the content type of an HTTP request or response body.
*/
public final class MediaType {
private static final String TOKEN = "([a-zA-Z0-9-!#$%&'*+.^_`{|}~]+)";
private static final String QUOTED = "\"([^\"]*)\"";
private static final Pattern TYPE_SUBTYPE = Pattern.compile(TOKEN + "/" + TOKEN);
private static final Pattern PARAMETER = Pattern.compile(
";\\s*" + TOKEN + "=(?:" + TOKEN + "|" + QUOTED + ")");
private final String mediaType;
private final String type;
private final String subtype;
private final String charset;
private MediaType(String mediaType, String type, String subtype, String charset) {
this.mediaType = mediaType;
this.type = type;
this.subtype = subtype;
this.charset = charset;
}
/**
* Returns a media type for {@code string}, or null if {@code string} is not a
* well-formed media type.
*/
public static MediaType parse(String string) {
Matcher typeSubtype = TYPE_SUBTYPE.matcher(string);
if (!typeSubtype.lookingAt()) return null;
String type = typeSubtype.group(1).toLowerCase(Locale.US);
String subtype = typeSubtype.group(2).toLowerCase(Locale.US);
String charset = null;
Matcher parameter = PARAMETER.matcher(string);
for (int s = typeSubtype.end(); s < string.length(); s = parameter.end()) {
parameter.region(s, string.length());
if (!parameter.lookingAt()) return null; // This is not a well-formed media type.
String name = parameter.group(1);
if (name == null || !name.equalsIgnoreCase("charset")) continue;
if (charset != null) throw new IllegalArgumentException("Multiple charsets: " + string);
charset = parameter.group(2) != null
? parameter.group(2) // Value is a token.
: parameter.group(3); // Value is a quoted string.
}
return new MediaType(string, type, subtype, charset);
}
/**
* Returns the high-level media type, such as "text", "image", "audio",
* "video", or "application".
*/
public String type() {
return type;
}
/**
* Returns a specific media subtype, such as "plain" or "png", "mpeg",
* "mp4" or "xml".
*/
public String subtype() {
return subtype;
}
/**
* Returns the charset of this media type, or null if this media type doesn't
* specify a charset.
*/
public Charset charset() {
return charset != null ? Charset.forName(charset) : null;
}
/**
* Returns the charset of this media type, or {@code defaultValue} if this
* media type doesn't specify a charset.
*/
public Charset charset(Charset defaultValue) {
return charset != null ? Charset.forName(charset) : defaultValue;
}
/**
* Returns the encoded media type, like "text/plain; charset=utf-8",
* appropriate for use in a Content-Type header.
*/
@Override public String toString() {
return mediaType;
}
@Override public boolean equals(Object o) {
return o instanceof MediaType && ((MediaType) o).mediaType.equals(mediaType);
}
@Override public int hashCode() {
return mediaType.hashCode();
}
}

View File

@@ -1,123 +0,0 @@
/*
* 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 com.squareup.okhttp.internal.Base64;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.Proxy;
import java.net.URL;
import java.util.List;
/**
* Responds to authentication challenges from the remote web or proxy server by
* returning credentials.
*/
public interface OkAuthenticator {
/**
* Returns a credential that satisfies the authentication challenge made by
* {@code url}. Returns null if the challenge cannot be satisfied. This method
* is called in response to an HTTP 401 unauthorized status code sent by the
* origin server.
*
* @param challenges parsed "WWW-Authenticate" challenge headers from the HTTP
* response.
*/
Credential authenticate(Proxy proxy, URL url, List<Challenge> challenges) throws IOException;
/**
* Returns a credential that satisfies the authentication challenge made by
* {@code proxy}. Returns null if the challenge cannot be satisfied. This
* method is called in response to an HTTP 401 unauthorized status code sent
* by the proxy server.
*
* @param challenges parsed "Proxy-Authenticate" challenge headers from the
* HTTP response.
*/
Credential authenticateProxy(Proxy proxy, URL url, List<Challenge> challenges) throws IOException;
/** An RFC 2617 challenge. */
public final class Challenge {
private final String scheme;
private final String realm;
public Challenge(String scheme, String realm) {
this.scheme = scheme;
this.realm = realm;
}
/** Returns the authentication scheme, like {@code Basic}. */
public String getScheme() {
return scheme;
}
/** Returns the protection space. */
public String getRealm() {
return 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();
}
@Override public String toString() {
return scheme + " realm=\"" + realm + "\"";
}
}
/** An RFC 2617 credential. */
public final class Credential {
private final String headerValue;
private Credential(String headerValue) {
this.headerValue = headerValue;
}
/** Returns an auth credential for the Basic scheme. */
public static Credential basic(String userName, String password) {
try {
String usernameAndPassword = userName + ":" + password;
byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1");
String encoded = Base64.encode(bytes);
return new Credential("Basic " + encoded);
} catch (UnsupportedEncodingException e) {
throw new AssertionError();
}
}
public String getHeaderValue() {
return headerValue;
}
@Override public boolean equals(Object o) {
return o instanceof Credential && ((Credential) o).headerValue.equals(headerValue);
}
@Override public int hashCode() {
return headerValue.hashCode();
}
@Override public String toString() {
return headerValue;
}
}
}

228
framework/src/com/squareup/okhttp/OkHttpClient.java Executable file → Normal file
View File

@@ -15,105 +15,34 @@
*/
package com.squareup.okhttp;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.http.HttpAuthenticator;
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 com.squareup.okhttp.internal.tls.OkHostnameVerifier;
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.net.URLConnection;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
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 implements URLStreamHandlerFactory {
private static final List<String> DEFAULT_TRANSPORTS
= Util.immutableList(Arrays.asList("spdy/3", "http/1.1"));
private final RouteDatabase routeDatabase;
private final Dispatcher dispatcher;
public final class OkHttpClient {
private Proxy proxy;
private List<String> transports;
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 OkAuthenticator authenticator;
private ConnectionPool connectionPool;
private boolean followProtocolRedirects = true;
private int connectTimeout;
private int readTimeout;
public OkHttpClient() {
routeDatabase = new RouteDatabase();
dispatcher = new Dispatcher();
}
private OkHttpClient(OkHttpClient copyFrom) {
routeDatabase = copyFrom.routeDatabase;
dispatcher = copyFrom.dispatcher;
}
/**
* Sets the default connect timeout for new connections. A value of 0 means no timeout.
*
* @see URLConnection#setConnectTimeout(int)
*/
public void setConnectTimeout(long timeout, TimeUnit unit) {
if (timeout < 0) {
throw new IllegalArgumentException("timeout < 0");
}
if (unit == null) {
throw new IllegalArgumentException("unit == null");
}
long millis = unit.toMillis(timeout);
if (millis > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Timeout too large.");
}
connectTimeout = (int) millis;
}
/** Default connect timeout (in milliseconds). */
public int getConnectTimeout() {
return connectTimeout;
}
/**
* Sets the default read timeout for new connections. A value of 0 means no timeout.
*
* @see URLConnection#setReadTimeout(int)
*/
public void setReadTimeout(long timeout, TimeUnit unit) {
if (timeout < 0) {
throw new IllegalArgumentException("timeout < 0");
}
if (unit == null) {
throw new IllegalArgumentException("unit == null");
}
long millis = unit.toMillis(timeout);
if (millis > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Timeout too large.");
}
readTimeout = (int) millis;
}
/** Default read timeout (in milliseconds). */
public int getReadTimeout() {
return readTimeout;
}
/**
* Sets the HTTP proxy that will be used by connections created by this
@@ -179,7 +108,7 @@ public final class OkHttpClient implements URLStreamHandlerFactory {
return responseCache;
}
public OkResponseCache getOkResponseCache() {
private OkResponseCache okResponseCache() {
if (responseCache instanceof HttpResponseCache) {
return ((HttpResponseCache) responseCache).okResponseCache;
} else if (responseCache != null) {
@@ -195,7 +124,7 @@ public final class OkHttpClient implements URLStreamHandlerFactory {
* <p>If unset, the {@link HttpsURLConnection#getDefaultSSLSocketFactory()
* system-wide default} SSL socket factory will be used.
*/
public OkHttpClient setSslSocketFactory(SSLSocketFactory sslSocketFactory) {
public OkHttpClient setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
this.sslSocketFactory = sslSocketFactory;
return this;
}
@@ -220,22 +149,6 @@ public final class OkHttpClient implements URLStreamHandlerFactory {
return hostnameVerifier;
}
/**
* Sets the authenticator used to respond to challenges from the remote web
* server or proxy server.
*
* <p>If unset, the {@link java.net.Authenticator#setDefault system-wide default}
* authenticator will be used.
*/
public OkHttpClient setAuthenticator(OkAuthenticator authenticator) {
this.authenticator = authenticator;
return this;
}
public OkAuthenticator getAuthenticator() {
return authenticator;
}
/**
* Sets the connection pool used to recycle HTTP and HTTPS connections.
*
@@ -267,86 +180,16 @@ public final class OkHttpClient implements URLStreamHandlerFactory {
return followProtocolRedirects;
}
public RouteDatabase getRoutesDatabase() {
return routeDatabase;
}
/**
* Configure the transports used by this client to communicate with remote
* servers. By default this client will prefer the most efficient transport
* available, falling back to more ubiquitous transports. Applications should
* only call this method to avoid specific compatibility problems, such as web
* servers that behave incorrectly when SPDY is enabled.
*
* <p>The following transports are currently supported:
* <ul>
* <li><a href="http://www.w3.org/Protocols/rfc2616/rfc2616.html">http/1.1</a>
* <li><a href="http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3">spdy/3</a>
* </ul>
*
* <p><strong>This is an evolving set.</strong> Future releases may drop
* support for transitional transports (like spdy/3), in favor of their
* successors (spdy/4 or http/2.0). The http/1.1 transport will never be
* dropped.
*
* <p>If multiple protocols are specified, <a
* href="https://technotes.googlecode.com/git/nextprotoneg.html">NPN</a> will
* be used to negotiate a transport. Future releases may use another mechanism
* (such as <a href="http://tools.ietf.org/html/draft-friedl-tls-applayerprotoneg-02">ALPN</a>)
* to negotiate a transport.
*
* @param transports the transports to use, in order of preference. The list
* must contain "http/1.1". It must not contain null.
*/
public OkHttpClient setTransports(List<String> transports) {
transports = Util.immutableList(transports);
if (!transports.contains("http/1.1")) {
throw new IllegalArgumentException("transports doesn't contain http/1.1: " + transports);
}
if (transports.contains(null)) {
throw new IllegalArgumentException("transports must not contain null");
}
if (transports.contains("")) {
throw new IllegalArgumentException("transports contains an empty string");
}
this.transports = transports;
return this;
}
public List<String> getTransports() {
return transports;
}
/**
* Schedules {@code request} to be executed.
*/
/* OkHttp 2.0: public */ void enqueue(Request request, Response.Receiver responseReceiver) {
// Create the HttpURLConnection immediately so the enqueued job gets the current settings of
// this client. Otherwise changes to this client (socket factory, redirect policy, etc.) may
// incorrectly be reflected in the request when it is dispatched later.
dispatcher.enqueue(copyWithDefaults(), request, responseReceiver);
}
/**
* Cancels all scheduled tasks tagged with {@code tag}. Requests that are already
* in flight might not be canceled.
*/
/* OkHttp 2.0: public */ void cancel(Object tag) {
dispatcher.cancel(tag);
}
public HttpURLConnection open(URL url) {
return open(url, proxy);
}
HttpURLConnection open(URL url, Proxy proxy) {
String protocol = url.getProtocol();
OkHttpClient copy = copyWithDefaults();
copy.proxy = proxy;
if (protocol.equals("http")) return new HttpURLConnectionImpl(url, copy);
if (protocol.equals("https")) return new HttpsURLConnectionImpl(url, copy);
throw new IllegalArgumentException("Unexpected protocol: " + protocol);
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);
}
}
/**
@@ -354,8 +197,9 @@ public final class OkHttpClient implements URLStreamHandlerFactory {
* each field that hasn't been explicitly configured.
*/
private OkHttpClient copyWithDefaults() {
OkHttpClient result = new OkHttpClient(this);
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();
@@ -364,45 +208,9 @@ public final class OkHttpClient implements URLStreamHandlerFactory {
: HttpsURLConnection.getDefaultSSLSocketFactory();
result.hostnameVerifier = hostnameVerifier != null
? hostnameVerifier
: OkHostnameVerifier.INSTANCE;
result.authenticator = authenticator != null
? authenticator
: HttpAuthenticator.SYSTEM_DEFAULT;
: HttpsURLConnection.getDefaultHostnameVerifier();
result.connectionPool = connectionPool != null ? connectionPool : ConnectionPool.getDefault();
result.followProtocolRedirects = followProtocolRedirects;
result.transports = transports != null ? transports : DEFAULT_TRANSPORTS;
result.connectTimeout = connectTimeout;
result.readTimeout = readTimeout;
return result;
}
/**
* Creates a URLStreamHandler as a {@link URL#setURLStreamHandlerFactory}.
*
* <p>This code configures OkHttp to handle all HTTP and HTTPS connections
* created with {@link URL#openConnection()}: <pre> {@code
*
* OkHttpClient okHttpClient = new OkHttpClient();
* URL.setURLStreamHandlerFactory(okHttpClient);
* }</pre>
*/
public URLStreamHandler createURLStreamHandler(final String protocol) {
if (!protocol.equals("http") && !protocol.equals("https")) return null;
return new URLStreamHandler() {
@Override protected URLConnection openConnection(URL url) {
return open(url);
}
@Override protected URLConnection openConnection(URL url, Proxy proxy) {
return open(url, proxy);
}
@Override protected int getDefaultPort() {
if (protocol.equals("http")) return 80;
if (protocol.equals("https")) return 443;
throw new AssertionError();
}
};
}
}

36
framework/src/com/squareup/okhttp/OkResponseCache.java Executable file → Normal file
View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2013 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.
@@ -16,41 +16,23 @@
package com.squareup.okhttp;
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.
*
* <h3>Warning: Experimental OkHttp 2.0 API</h3>
* This class is in beta. APIs are subject to change!
* 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 {
CacheResponse get(URI uri, String requestMethod, Map<String, List<String>> requestHeaders)
throws IOException;
CacheRequest put(URI uri, URLConnection urlConnection) throws IOException;
/** Remove any cache entries for the supplied {@code uri} if the request method invalidates. */
void maybeRemove(String requestMethod, URI uri) 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 HTTP response being satisfied by {@code source}. */
void trackResponse(ResponseSource source);
/** 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);
/** Updates stored HTTP headers using a hit on a conditional GET. */
void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
throws IOException;
}

View File

@@ -1,284 +0,0 @@
/*
* 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 com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.http.RawHeaders;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Set;
/**
* An HTTP request. Instances of this class are immutable if their {@link #body}
* is null or itself immutable.
*
* <h3>Warning: Experimental OkHttp 2.0 API</h3>
* This class is in beta. APIs are subject to change!
*/
/* OkHttp 2.0: public */ final class Request {
private final URL url;
private final String method;
private final RawHeaders headers;
private final Body body;
private final Object tag;
private Request(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = new RawHeaders(builder.headers);
this.body = builder.body;
this.tag = builder.tag != null ? builder.tag : this;
}
public URL url() {
return url;
}
public String urlString() {
return url.toString();
}
public String method() {
return method;
}
public String header(String name) {
return headers.get(name);
}
public List<String> headers(String name) {
return headers.values(name);
}
public Set<String> headerNames() {
return headers.names();
}
RawHeaders rawHeaders() {
return new RawHeaders(headers);
}
public int headerCount() {
return headers.length();
}
public String headerName(int index) {
return headers.getFieldName(index);
}
public String headerValue(int index) {
return headers.getValue(index);
}
public Body body() {
return body;
}
public Object tag() {
return tag;
}
Builder newBuilder() {
return new Builder(url)
.method(method, body)
.rawHeaders(headers)
.tag(tag);
}
public abstract static class Body {
/** Returns the Content-Type header for this body. */
public abstract MediaType contentType();
/**
* Returns the number of bytes that will be written to {@code out} in a call
* to {@link #writeTo}, or -1 if that count is unknown.
*/
public long contentLength() {
return -1;
}
/** Writes the content of this request to {@code out}. */
public abstract void writeTo(OutputStream out) throws IOException;
/**
* Returns a new request body that transmits {@code content}. If {@code
* contentType} lacks a charset, this will use UTF-8.
*/
public static Body create(MediaType contentType, String content) {
contentType = contentType.charset() != null
? contentType
: MediaType.parse(contentType + "; charset=utf-8");
try {
byte[] bytes = content.getBytes(contentType.charset().name());
return create(contentType, bytes);
} catch (UnsupportedEncodingException e) {
throw new AssertionError();
}
}
/** Returns a new request body that transmits {@code content}. */
public static Body create(final MediaType contentType, final byte[] content) {
if (contentType == null) throw new NullPointerException("contentType == null");
if (content == null) throw new NullPointerException("content == null");
return new Body() {
@Override public MediaType contentType() {
return contentType;
}
@Override public long contentLength() {
return content.length;
}
@Override public void writeTo(OutputStream out) throws IOException {
out.write(content);
}
};
}
/** Returns a new request body that transmits the content of {@code file}. */
public static Body create(final MediaType contentType, final File file) {
if (contentType == null) throw new NullPointerException("contentType == null");
if (file == null) throw new NullPointerException("content == null");
return new Body() {
@Override public MediaType contentType() {
return contentType;
}
@Override public long contentLength() {
return file.length();
}
@Override public void writeTo(OutputStream out) throws IOException {
long length = contentLength();
if (length == 0) return;
InputStream in = null;
try {
in = new FileInputStream(file);
byte[] buffer = new byte[(int) Math.min(8192, length)];
for (int c; (c = in.read(buffer)) != -1; ) {
out.write(buffer, 0, c);
}
} finally {
Util.closeQuietly(in);
}
}
};
}
}
public static class Builder {
private URL url;
private String method = "GET";
private RawHeaders headers = new RawHeaders();
private Body body;
private Object tag;
public Builder(String url) {
url(url);
}
public Builder(URL url) {
url(url);
}
public Builder url(String url) {
try {
this.url = new URL(url);
return this;
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Malformed URL: " + url);
}
}
public Builder url(URL url) {
if (url == null) throw new IllegalStateException("url == null");
this.url = url;
return this;
}
/**
* Sets the header named {@code name} to {@code value}. If this request
* already has any headers with that name, they are all replaced.
*/
public Builder header(String name, String value) {
headers.set(name, value);
return this;
}
/**
* Adds a header with {@code name} and {@code value}. Prefer this method for
* multiply-valued headers like "Cookie".
*/
public Builder addHeader(String name, String value) {
headers.add(name, value);
return this;
}
Builder rawHeaders(RawHeaders rawHeaders) {
headers = new RawHeaders(rawHeaders);
return this;
}
public Builder get() {
return method("GET", null);
}
public Builder head() {
return method("HEAD", null);
}
public Builder post(Body body) {
return method("POST", body);
}
public Builder put(Body body) {
return method("PUT", body);
}
public Builder method(String method, Body body) {
if (method == null || method.length() == 0) {
throw new IllegalArgumentException("method == null || method.length() == 0");
}
this.method = method;
this.body = body;
return this;
}
/**
* Attaches {@code tag} to the request. It can be used later to cancel the
* request. If the tag is unspecified or null, the request is canceled by
* using the request itself as the tag.
*/
public Builder tag(Object tag) {
this.tag = tag;
return this;
}
public Request build() {
return new Request(this);
}
}
}

View File

@@ -1,290 +0,0 @@
/*
* 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 com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.http.RawHeaders;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Set;
import static com.squareup.okhttp.internal.Util.UTF_8;
/**
* An HTTP response. Instances of this class are not immutable: the response
* body is a one-shot value that may be consumed only once. All other properties
* are immutable.
*
* <h3>Warning: Experimental OkHttp 2.0 API</h3>
* This class is in beta. APIs are subject to change!
*/
/* OkHttp 2.0: public */ final class Response {
private final Request request;
private final int code;
private final RawHeaders headers;
private final Body body;
private final Response redirectedBy;
private Response(Builder builder) {
this.request = builder.request;
this.code = builder.code;
this.headers = new RawHeaders(builder.headers);
this.body = builder.body;
this.redirectedBy = builder.redirectedBy;
}
/**
* The wire-level request that initiated this HTTP response. This is usually
* <strong>not</strong> the same request instance provided to the HTTP client:
* <ul>
* <li>It may be transformed by the HTTP client. For example, the client
* may have added its own {@code Content-Encoding} header to enable
* response compression.
* <li>It may be the request generated in response to an HTTP redirect.
* In this case the request URL may be different than the initial
* request URL.
* </ul>
*/
public Request request() {
return request;
}
public int code() {
return code;
}
public String header(String name) {
return header(name, null);
}
public String header(String name, String defaultValue) {
String result = headers.get(name);
return result != null ? result : defaultValue;
}
public List<String> headers(String name) {
return headers.values(name);
}
public Set<String> headerNames() {
return headers.names();
}
public int headerCount() {
return headers.length();
}
public String headerName(int index) {
return headers.getFieldName(index);
}
RawHeaders rawHeaders() {
return new RawHeaders(headers);
}
public String headerValue(int index) {
return headers.getValue(index);
}
public Body body() {
return body;
}
/**
* Returns the response for the HTTP redirect that triggered this response, or
* null if this response wasn't triggered by an automatic redirect. The body
* of the returned response should not be read because it has already been
* consumed by the redirecting client.
*/
public Response redirectedBy() {
return redirectedBy;
}
public abstract static class Body {
/** Multiple calls to {@link #charStream()} must return the same instance. */
private Reader reader;
/**
* Returns true if further data from this response body should be read at
* this time. For asynchronous transports like SPDY and HTTP/2.0, this will
* return false once all locally-available body bytes have been read.
*
* <p>Clients with many concurrent downloads can use this method to reduce
* the number of idle threads blocking on reads. See {@link
* Receiver#onResponse} for details.
*/
// <h3>Body.ready() vs. InputStream.available()</h3>
// TODO: Can we fix response bodies to implement InputStream.available well?
// The deflater implementation is broken by default but we could do better.
public abstract boolean ready() throws IOException;
public abstract MediaType contentType();
/**
* Returns the number of bytes in that will returned by {@link #bytes}, or
* {@link #byteStream}, or -1 if unknown.
*/
public abstract long contentLength();
public abstract InputStream byteStream() throws IOException;
public final byte[] bytes() throws IOException {
long contentLength = contentLength();
if (contentLength > Integer.MAX_VALUE) {
throw new IOException("Cannot buffer entire body for content length: " + contentLength);
}
if (contentLength != -1) {
byte[] content = new byte[(int) contentLength];
InputStream in = byteStream();
Util.readFully(in, content);
if (in.read() != -1) throw new IOException("Content-Length and stream length disagree");
return content;
} else {
ByteArrayOutputStream out = new ByteArrayOutputStream();
Util.copy(byteStream(), out);
return out.toByteArray();
}
}
/**
* Returns the response as a character stream decoded with the charset
* of the Content-Type header. If that header is either absent or lacks a
* charset, this will attempt to decode the response body as UTF-8.
*/
public final Reader charStream() throws IOException {
if (reader == null) {
reader = new InputStreamReader(byteStream(), charset());
}
return reader;
}
/**
* Returns the response as a string decoded with the charset of the
* Content-Type header. If that header is either absent or lacks a charset,
* this will attempt to decode the response body as UTF-8.
*/
public final String string() throws IOException {
return new String(bytes(), charset().name());
}
private Charset charset() {
MediaType contentType = contentType();
return contentType != null ? contentType.charset(UTF_8) : UTF_8;
}
}
public interface Receiver {
/**
* Called when the request could not be executed due to a connectivity
* problem or timeout. Because networks can fail during an exchange, it is
* possible that the remote server accepted the request before the failure.
*/
void onFailure(Failure failure);
/**
* Called when the HTTP response was successfully returned by the remote
* server. The receiver may proceed to read the response body with the
* response's {@link #body} method.
*
* <p>Note that transport-layer success (receiving a HTTP response code,
* headers and body) does not necessarily indicate application-layer
* success: {@code response} may still indicate an unhappy HTTP response
* code like 404 or 500.
*
* <h3>Non-blocking responses</h3>
*
* <p>Receivers do not need to block while waiting for the response body to
* download. Instead, they can get called back as data arrives. Use {@link
* Body#ready} to check if bytes should be read immediately. While there is
* data ready, read it. If there isn't, return false: receivers will be
* called back with {@code onResponse()} as additional data is downloaded.
*
* <p>Return true to indicate that the receiver has finished handling the
* response body. If the response body has unread data, it will be
* discarded.
*
* <p>When the response body has been fully consumed the returned value is
* undefined.
*
* <p>The current implementation of {@link Body#ready} always returns true
* when the underlying transport is HTTP/1. This results in blocking on that
* transport. For effective non-blocking your server must support SPDY or
* HTTP/2.
*/
boolean onResponse(Response response) throws IOException;
}
public static class Builder {
private final Request request;
private final int code;
private RawHeaders headers = new RawHeaders();
private Body body;
private Response redirectedBy;
public Builder(Request request, int code) {
if (request == null) throw new IllegalArgumentException("request == null");
if (code <= 0) throw new IllegalArgumentException("code <= 0");
this.request = request;
this.code = code;
}
/**
* Sets the header named {@code name} to {@code value}. If this request
* already has any headers with that name, they are all replaced.
*/
public Builder header(String name, String value) {
headers.set(name, value);
return this;
}
/**
* Adds a header with {@code name} and {@code value}. Prefer this method for
* multiply-valued headers like "Set-Cookie".
*/
public Builder addHeader(String name, String value) {
headers.add(name, value);
return this;
}
Builder rawHeaders(RawHeaders rawHeaders) {
headers = new RawHeaders(rawHeaders);
return this;
}
public Builder body(Body body) {
this.body = body;
return this;
}
public Builder redirectedBy(Response redirectedBy) {
this.redirectedBy = redirectedBy;
return this;
}
public Response build() {
if (request == null) throw new IllegalStateException("Response has no request.");
if (code == -1) throw new IllegalStateException("Response has no code.");
return new Response(this);
}
}
}

0
framework/src/com/squareup/okhttp/ResponseSource.java Executable file → Normal file
View File

6
framework/src/com/squareup/okhttp/Route.java Executable file → Normal file
View File

@@ -59,13 +59,13 @@ public class Route {
return inetSocketAddress;
}
/** Returns true if this route uses modern TLS. */
/** Returns true if this route uses modern tls. */
public boolean isModernTls() {
return modernTls;
}
/** Returns a copy of this route with flipped TLS mode. */
Route flipTlsMode() {
/** Returns a copy of this route with flipped tls mode. */
public Route flipTlsMode() {
return new Route(address, proxy, inetSocketAddress, !modernTls);
}

View File

@@ -1,57 +0,0 @@
/*
* 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.io.IOException;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.net.ssl.SSLHandshakeException;
/**
* A blacklist of failed routes to avoid when creating a new connection to a
* target address. This is used so that OkHttp can learn from its mistakes: if
* there was a failure attempting to connect to a specific IP address, proxy
* server or TLS mode, that failure is remembered and alternate routes are
* preferred.
*/
public final class RouteDatabase {
private final Set<Route> failedRoutes = new LinkedHashSet<Route>();
/** Records a failure connecting to {@code failedRoute}. */
public synchronized void failed(Route failedRoute, IOException failure) {
failedRoutes.add(failedRoute);
if (!(failure instanceof SSLHandshakeException)) {
// If the problem was not related to SSL then it will also fail with
// a different TLS mode therefore we can be proactive about it.
failedRoutes.add(failedRoute.flipTlsMode());
}
}
/** Records success connecting to {@code failedRoute}. */
public synchronized void connected(Route route) {
failedRoutes.remove(route);
}
/** Returns true if {@code route} has failed recently and should be avoided. */
public synchronized boolean shouldPostpone(Route route) {
return failedRoutes.contains(route);
}
public synchronized int failedRoutesCount() {
return failedRoutes.size();
}
}

0
framework/src/com/squareup/okhttp/TunnelRequest.java Executable file → Normal file
View File

View File

0
framework/src/com/squareup/okhttp/internal/Base64.java Executable file → Normal file
View File

View File

0
framework/src/com/squareup/okhttp/internal/Dns.java Executable file → Normal file
View File

View File

@@ -20,10 +20,10 @@ package com.squareup.okhttp.internal;
* Runnable implementation which always sets its thread name.
*/
public abstract class NamedRunnable implements Runnable {
private final String name;
private String name;
public NamedRunnable(String format, Object... args) {
this.name = String.format(format, args);
public NamedRunnable(String name) {
this.name = name;
}
@Override public final void run() {

117
framework/src/com/squareup/okhttp/internal/Platform.java Executable file → Normal file
View File

@@ -16,6 +16,7 @@
*/
package com.squareup.okhttp.internal;
import com.squareup.okhttp.OkHttpClient;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
@@ -24,7 +25,7 @@ import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.Socket;
import java.net.SocketException;
import java.net.URI;
@@ -56,11 +57,6 @@ public class Platform {
return PLATFORM;
}
/** Prefix used on custom headers. */
public String getPrefix() {
return "OkHttp";
}
public void logW(String warning) {
System.out.println(warning);
}
@@ -103,11 +99,6 @@ public class Platform {
public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
}
public void connectSocket(Socket socket, InetSocketAddress address,
int connectTimeout) throws IOException {
socket.connect(address, connectTimeout);
}
/**
* Returns a deflater output stream that supports SYNC_FLUSH for SPDY name
* value blocks. This throws an {@link UnsupportedOperationException} on
@@ -134,21 +125,33 @@ public class Platform {
}
}
/**
* 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 {
try {
openSslSocketClass = Class.forName("com.android.org.conscrypt.OpenSSLSocketImpl");
} catch (ClassNotFoundException ignored) {
// Older platform before being unbundled.
openSslSocketClass = Class.forName(
"org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
}
openSslSocketClass = Class.forName("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
setUseSessionTickets = openSslSocketClass.getMethod("setUseSessionTickets", boolean.class);
setHostname = openSslSocketClass.getMethod("setHostname", String.class);
@@ -156,10 +159,10 @@ public class Platform {
try {
Method setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class);
Method getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol");
return new Android41(openSslSocketClass, setUseSessionTickets, setHostname,
return new Android41(getMtu, openSslSocketClass, setUseSessionTickets, setHostname,
setNpnProtocols, getNpnSelectedProtocol);
} catch (NoSuchMethodException ignored) {
return new Android23(openSslSocketClass, setUseSessionTickets, setHostname);
return new Android23(getMtu, openSslSocketClass, setUseSessionTickets, setHostname);
}
} catch (ClassNotFoundException ignored) {
// This isn't an Android runtime.
@@ -176,43 +179,55 @@ public class Platform {
Class<?> serverProviderClass = Class.forName(npnClassName + "$ServerProvider");
Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass);
Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class);
return new JdkWithJettyNpnPlatform(
putMethod, getMethod, clientProviderClass, serverProviderClass);
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 new Platform();
return getMtu != null ? new Java5(getMtu) : new Platform();
}
/** Android version 2.3 and newer support TLS session tickets and server name indication (SNI). */
private static class Android23 extends 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(
Class<?> openSslSocketClass, Method setUseSessionTickets, 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 connectSocket(Socket socket, InetSocketAddress address,
int connectTimeout) throws IOException {
try {
socket.connect(address, connectTimeout);
} catch (SecurityException se) {
// Before android 4.3, socket.connect could throw a SecurityException
// if opening a socket resulted in an EACCES error.
IOException ioException = new IOException("Exception in connect");
ioException.initCause(se);
throw ioException;
}
}
@Override public void enableTlsExtensions(SSLSocket socket, String uriHost) {
super.enableTlsExtensions(socket, uriHost);
if (openSslSocketClass.isInstance(socket)) {
@@ -234,9 +249,9 @@ public class Platform {
private final Method setNpnProtocols;
private final Method getNpnSelectedProtocol;
private Android41(Class<?> openSslSocketClass, Method setUseSessionTickets, Method setHostname,
Method setNpnProtocols, Method getNpnSelectedProtocol) {
super(openSslSocketClass, setUseSessionTickets, setHostname);
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;
}
@@ -268,15 +283,19 @@ public class Platform {
}
}
/** OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class path. */
private static class JdkWithJettyNpnPlatform extends Platform {
/**
* 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 putMethod, Method getMethod, Class<?> clientProviderClass,
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;
@@ -309,7 +328,7 @@ public class Platform {
JettyNpnProvider provider =
(JettyNpnProvider) Proxy.getInvocationHandler(getMethod.invoke(null, socket));
if (!provider.unsupported && provider.selected == null) {
Logger logger = Logger.getLogger("com.squareup.okhttp.OkHttpClient");
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;

View File

@@ -146,7 +146,8 @@ public class StrictLineReader implements Closeable {
// Let's anticipate up to 80 characters on top of those already read.
ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
@Override public String toString() {
@Override
public String toString() {
int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
try {
return new String(buf, 0, length, charset.name());

67
framework/src/com/squareup/okhttp/internal/Util.java Executable file → Normal file
View File

@@ -24,19 +24,11 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.ServerSocket;
import java.net.URI;
import java.net.URL;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicReference;
/** Junk drawer of utility methods. */
@@ -54,9 +46,6 @@ public final class Util {
public static final Charset UTF_8 = Charset.forName("UTF-8");
private static AtomicReference<byte[]> skipBuffer = new AtomicReference<byte[]>();
private static final char[] DIGITS =
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
private Util() {
}
@@ -137,21 +126,6 @@ public final class Util {
}
}
/**
* Closes {@code serverSocket}, ignoring any checked exceptions. Does nothing if
* {@code serverSocket} is null.
*/
public static void closeQuietly(ServerSocket serverSocket) {
if (serverSocket != null) {
try {
serverSocket.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.
@@ -284,8 +258,6 @@ public final class Util {
* buffer.
*/
public static long skipByReading(InputStream in, long byteCount) throws IOException {
if (byteCount == 0) return 0L;
// acquire the shared skip buffer.
byte[] buffer = skipBuffer.getAndSet(null);
if (buffer == null) {
@@ -352,43 +324,4 @@ public final class Util {
}
return result.toString();
}
/** Returns a 32 character string containing a hash of {@code s}. */
public static String hash(String s) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] md5bytes = messageDigest.digest(s.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);
}
/** Returns an immutable copy of {@code list}. */
public static <T> List<T> immutableList(List<T> list) {
return Collections.unmodifiableList(new ArrayList<T>(list));
}
public static ThreadFactory daemonThreadFactory(final String name) {
return new ThreadFactory() {
@Override public Thread newThread(Runnable runnable) {
Thread result = new Thread(runnable, name);
result.setDaemon(true);
return result;
}
};
}
}

View File

@@ -79,11 +79,11 @@ abstract class AbstractHttpInputStream extends InputStream {
* 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() throws IOException {
protected final void endOfInput(boolean streamCancelled) throws IOException {
if (cacheRequest != null) {
cacheBody.close();
}
httpEngine.release(false);
httpEngine.release(streamCancelled);
}
/**

View File

@@ -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");
}
}
}

View File

@@ -27,11 +27,11 @@ final class HeaderParser {
int pos = 0;
while (pos < value.length()) {
int tokenStart = pos;
pos = skipUntil(value, pos, "=,;");
pos = skipUntil(value, pos, "=,");
String directive = value.substring(tokenStart, pos).trim();
if (pos == value.length() || value.charAt(pos) == ',' || value.charAt(pos) == ';') {
pos++; // consume ',' or ';' (if necessary)
if (pos == value.length() || value.charAt(pos) == ',') {
pos++; // consume ',' (if necessary)
handler.handle(directive, null);
continue;
}
@@ -52,7 +52,7 @@ final class HeaderParser {
// unquoted string
} else {
int parameterStart = pos;
pos = skipUntil(value, pos, ",;");
pos = skipUntil(value, pos, ",");
parameter = value.substring(parameterStart, pos).trim();
}

View File

@@ -16,8 +16,7 @@
*/
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.OkAuthenticator;
import com.squareup.okhttp.OkAuthenticator.Challenge;
import com.squareup.okhttp.internal.Base64;
import java.io.IOException;
import java.net.Authenticator;
import java.net.InetAddress;
@@ -28,57 +27,11 @@ import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import static com.squareup.okhttp.OkAuthenticator.Credential;
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 {
/** Uses the global authenticator to get the password. */
public static final OkAuthenticator SYSTEM_DEFAULT = new OkAuthenticator() {
@Override public Credential authenticate(
Proxy proxy, URL url, List<Challenge> challenges) throws IOException {
for (Challenge challenge : challenges) {
if (!"Basic".equalsIgnoreCase(challenge.getScheme())) {
continue;
}
PasswordAuthentication auth = Authenticator.requestPasswordAuthentication(url.getHost(),
getConnectToInetAddress(proxy, url), url.getPort(), url.getProtocol(),
challenge.getRealm(), challenge.getScheme(), url, Authenticator.RequestorType.SERVER);
if (auth != null) {
return Credential.basic(auth.getUserName(), new String(auth.getPassword()));
}
}
return null;
}
@Override public Credential authenticateProxy(
Proxy proxy, URL url, List<Challenge> challenges) throws IOException {
for (Challenge challenge : challenges) {
if (!"Basic".equalsIgnoreCase(challenge.getScheme())) {
continue;
}
InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
PasswordAuthentication auth = Authenticator.requestPasswordAuthentication(
proxyAddress.getHostName(), getConnectToInetAddress(proxy, url), proxyAddress.getPort(),
url.getProtocol(), challenge.getRealm(), challenge.getScheme(), url,
Authenticator.RequestorType.PROXY);
if (auth != null) {
return Credential.basic(auth.getUserName(), new String(auth.getPassword()));
}
}
return null;
}
private InetAddress getConnectToInetAddress(Proxy proxy, URL url) throws IOException {
return (proxy != null && proxy.type() != Proxy.Type.DIRECT)
? ((InetSocketAddress) proxy.address()).getAddress()
: InetAddress.getByName(url.getHost());
}
};
private HttpAuthenticator() {
}
@@ -88,35 +41,70 @@ public final class HttpAuthenticator {
* @return true if credentials have been added to successorRequestHeaders
* and another request should be attempted.
*/
public static boolean processAuthHeader(OkAuthenticator authenticator, int responseCode,
RawHeaders responseHeaders, RawHeaders successorRequestHeaders, Proxy proxy, URL url)
throws IOException {
String responseField;
String requestField;
if (responseCode == HTTP_UNAUTHORIZED) {
responseField = "WWW-Authenticate";
requestField = "Authorization";
} else if (responseCode == HTTP_PROXY_AUTH) {
responseField = "Proxy-Authenticate";
requestField = "Proxy-Authorization";
} else {
throw new IllegalArgumentException(); // TODO: ProtocolException?
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();
}
List<Challenge> challenges = parseChallenges(responseHeaders, responseField);
if (challenges.isEmpty()) {
return false; // Could not find a challenge so end the request cycle.
}
Credential credential = responseHeaders.getResponseCode() == HTTP_PROXY_AUTH
? authenticator.authenticateProxy(proxy, url, challenges)
: authenticator.authenticate(proxy, url, challenges);
if (credential == null) {
return false; // Could not satisfy the challenge so end the request cycle.
// 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.
successorRequestHeaders.set(requestField, credential.getHeaderValue());
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.
@@ -146,7 +134,7 @@ public final class HttpAuthenticator {
// 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(true, pos, "realm=\"", 0, "realm=\"".length())) {
if (!value.regionMatches(pos, "realm=\"", 0, "realm=\"".length())) {
break; // Unexpected challenge parameter; give up!
}
@@ -163,4 +151,25 @@ public final class HttpAuthenticator {
}
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();
}
}
}

View File

@@ -36,13 +36,14 @@ final class HttpDate {
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("GMT"));
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_FORMAT_STRINGS = new String[] {
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",
@@ -53,26 +54,19 @@ final class HttpDate {
/* RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com */
"EEE MMM d yyyy HH:mm:ss z", };
private static final DateFormat[] BROWSER_COMPATIBLE_DATE_FORMATS =
new DateFormat[BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS.length];
/** Returns the date for {@code value}. Returns null if the value couldn't be parsed. */
/**
* 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 ignored) {
} catch (ParseException ignore) {
}
synchronized (BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS) {
for (int i = 0, count = BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS.length; i < count; i++) {
DateFormat format = BROWSER_COMPATIBLE_DATE_FORMATS[i];
if (format == null) {
format = new SimpleDateFormat(BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS[i], Locale.US);
BROWSER_COMPATIBLE_DATE_FORMATS[i] = format;
}
try {
return format.parse(value);
} catch (ParseException ignored) {
}
for (String formatString : BROWSER_COMPATIBLE_DATE_FORMATS) {
try {
return new SimpleDateFormat(formatString, Locale.US).parse(value);
} catch (ParseException ignore) {
}
}
return null;

View File

@@ -19,8 +19,6 @@ package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Address;
import com.squareup.okhttp.Connection;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.OkResponseCache;
import com.squareup.okhttp.ResponseSource;
import com.squareup.okhttp.TunnelRequest;
import com.squareup.okhttp.internal.Dns;
@@ -33,7 +31,6 @@ import java.io.OutputStream;
import java.net.CacheRequest;
import java.net.CacheResponse;
import java.net.CookieHandler;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
@@ -88,8 +85,7 @@ public class HttpEngine {
};
public static final int HTTP_CONTINUE = 100;
protected final Policy policy;
protected final OkHttpClient client;
protected final HttpURLConnectionImpl policy;
protected final String method;
@@ -110,9 +106,6 @@ public class HttpEngine {
/** The time when the request headers were written, or -1 if they haven't been written yet. */
long sentRequestMillis = -1;
/** Whether the connection has been established. */
boolean connected;
/**
* True if this client added an "Accept-Encoding: gzip" header field and is
* therefore responsible for also decompressing the transfer stream.
@@ -144,15 +137,14 @@ public class HttpEngine {
/**
* @param requestHeaders the client's supplied request headers. This class
* creates a private copy that it can mutate.
* 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.
* 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(OkHttpClient client, Policy policy, String method, RawHeaders requestHeaders,
public HttpEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
Connection connection, RetryableOutputStream requestBodyOut) throws IOException {
this.client = client;
this.policy = policy;
this.method = method;
this.connection = connection;
@@ -183,9 +175,8 @@ public class HttpEngine {
prepareRawRequestHeaders();
initResponseSource();
OkResponseCache responseCache = client.getOkResponseCache();
if (responseCache != null) {
responseCache.trackResponse(responseSource);
if (policy.responseCache != null) {
policy.responseCache.trackResponse(responseSource);
}
// The raw response source may require the network, but the request
@@ -205,7 +196,8 @@ public class HttpEngine {
if (responseSource.requiresConnection()) {
sendSocketRequest();
} else if (connection != null) {
client.getConnectionPool().recycle(connection);
policy.connectionPool.recycle(connection);
policy.getFailedRoutes().remove(connection.getRoute());
connection = null;
}
}
@@ -216,14 +208,15 @@ public class HttpEngine {
*/
private void initResponseSource() throws IOException {
responseSource = ResponseSource.NETWORK;
if (!policy.getUseCaches()) return;
if (!policy.getUseCaches() || policy.responseCache == null) {
return;
}
OkResponseCache responseCache = client.getOkResponseCache();
if (responseCache == null) return;
CacheResponse candidate = responseCache.get(
uri, method, requestHeaders.getHeaders().toMultimap(false));
if (candidate == 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();
@@ -281,24 +274,22 @@ public class HttpEngine {
SSLSocketFactory sslSocketFactory = null;
HostnameVerifier hostnameVerifier = null;
if (uri.getScheme().equalsIgnoreCase("https")) {
sslSocketFactory = client.getSslSocketFactory();
hostnameVerifier = client.getHostnameVerifier();
sslSocketFactory = policy.sslSocketFactory;
hostnameVerifier = policy.hostnameVerifier;
}
Address address = new Address(uriHost, getEffectivePort(uri), sslSocketFactory,
hostnameVerifier, client.getAuthenticator(), client.getProxy(), client.getTransports());
routeSelector = new RouteSelector(address, uri, client.getProxySelector(),
client.getConnectionPool(), Dns.DEFAULT, client.getRoutesDatabase());
hostnameVerifier, policy.requestedProxy);
routeSelector = new RouteSelector(address, uri, policy.proxySelector, policy.connectionPool,
Dns.DEFAULT, policy.getFailedRoutes());
}
connection = routeSelector.next(method);
connection = routeSelector.next();
if (!connection.isConnected()) {
connection.connect(client.getConnectTimeout(), client.getReadTimeout(), getTunnelConfig());
client.getConnectionPool().maybeShare(connection);
client.getRoutesDatabase().connected(connection.getRoute());
} else if (!connection.isSpdy()) {
connection.updateReadTimeout(client.getReadTimeout());
connection.connect(policy.getConnectTimeout(), policy.getReadTimeout(), getTunnelConfig());
policy.connectionPool.maybeShare(connection);
policy.getFailedRoutes().remove(connection.getRoute());
}
connected(connection);
if (connection.getRoute().getProxy() != client.getProxy()) {
if (connection.getRoute().getProxy() != policy.requestedProxy) {
// Update the request line if the proxy changed; it may need a host name.
requestHeaders.getHeaders().setRequestLine(getRequestLine());
}
@@ -309,8 +300,6 @@ public class HttpEngine {
* pool. Subclasses use this hook to get a reference to the TLS data.
*/
protected void connected(Connection connection) {
policy.setSelectedProxy(connection.getRoute().getProxy());
connected = true;
}
/**
@@ -339,7 +328,7 @@ public class HttpEngine {
}
boolean hasRequestBody() {
return method.equals("POST") || method.equals("PUT") || method.equals("PATCH");
return method.equals("POST") || method.equals("PUT");
}
/** Returns the request body or null if this request doesn't have a body. */
@@ -398,20 +387,17 @@ public class HttpEngine {
private void maybeCache() throws IOException {
// Are we caching at all?
if (!policy.getUseCaches()) return;
OkResponseCache responseCache = client.getOkResponseCache();
if (responseCache == null) return;
HttpURLConnection connectionToCache = policy.getHttpConnectionToCache();
if (!policy.getUseCaches() || policy.responseCache == null) {
return;
}
// Should we cache this response for this request?
if (!responseHeaders.isCacheable(requestHeaders)) {
responseCache.maybeRemove(connectionToCache.getRequestMethod(), uri);
return;
}
// Offer this request to the cache.
cacheRequest = responseCache.put(uri, connectionToCache);
cacheRequest = policy.responseCache.put(uri, policy.getHttpConnectionToCache());
}
/**
@@ -423,7 +409,7 @@ public class HttpEngine {
public final void automaticallyReleaseConnectionToPool() {
automaticallyReleaseConnectionToPool = true;
if (connection != null && connectionReleased) {
client.getConnectionPool().recycle(connection);
policy.connectionPool.recycle(connection);
connection = null;
}
}
@@ -433,7 +419,7 @@ public class HttpEngine {
* closed. Also call {@link #automaticallyReleaseConnectionToPool} unless
* the connection will be used to follow a redirect.
*/
public final void release(boolean streamCanceled) {
public final void release(boolean streamCancelled) {
// If the response body comes from the cache, close it.
if (responseBodyIn == cachedResponseBody) {
Util.closeQuietly(responseBodyIn);
@@ -442,12 +428,12 @@ public class HttpEngine {
if (!connectionReleased && connection != null) {
connectionReleased = true;
if (transport == null
|| !transport.makeReusable(streamCanceled, requestBodyOut, responseTransferIn)) {
if (transport == null || !transport.makeReusable(streamCancelled, requestBodyOut,
responseTransferIn)) {
Util.closeQuietly(connection);
connection = null;
} else if (automaticallyReleaseConnectionToPool) {
client.getConnectionPool().recycle(connection);
policy.connectionPool.recycle(connection);
connection = null;
}
}
@@ -535,7 +521,7 @@ public class HttpEngine {
requestHeaders.setIfModifiedSince(new Date(ifModifiedSince));
}
CookieHandler cookieHandler = client.getCookieHandler();
CookieHandler cookieHandler = policy.cookieHandler;
if (cookieHandler != null) {
requestHeaders.addCookies(
cookieHandler.get(uri, requestHeaders.getHeaders().toMultimap(false)));
@@ -649,17 +635,9 @@ public class HttpEngine {
if (cachedResponseHeaders.validate(responseHeaders)) {
release(false);
ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders);
this.responseHeaders = combinedHeaders;
// Update the cache after applying the combined headers but before initializing the content
// stream, otherwise the Content-Encoding header (if present) will be stripped from the
// combined headers and not end up in the cache file if transparent gzip compression is
// turned on.
OkResponseCache responseCache = client.getOkResponseCache();
responseCache.trackConditionalCacheHit();
responseCache.update(cacheResponse, policy.getHttpConnectionToCache());
initContentStream(cachedResponseBody);
setResponse(combinedHeaders, cachedResponseBody);
policy.responseCache.trackConditionalCacheHit();
policy.responseCache.update(cacheResponse, policy.getHttpConnectionToCache());
return;
} else {
Util.closeQuietly(cachedResponseBody);
@@ -678,7 +656,7 @@ public class HttpEngine {
}
public void receiveHeaders(RawHeaders headers) throws IOException {
CookieHandler cookieHandler = client.getCookieHandler();
CookieHandler cookieHandler = policy.cookieHandler;
if (cookieHandler != null) {
cookieHandler.put(uri, headers.toMultimap(true));
}

View File

@@ -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();
}
}
}

View File

@@ -78,23 +78,18 @@ public final class HttpTransport implements Transport {
}
// Stream a request body of a known length.
long fixedContentLength = httpEngine.policy.getFixedContentLength();
int fixedContentLength = httpEngine.policy.getFixedContentLength();
if (fixedContentLength != -1) {
httpEngine.requestHeaders.setContentLength(fixedContentLength);
writeRequestHeaders();
return new FixedLengthOutputStream(requestOut, fixedContentLength);
}
long contentLength = httpEngine.requestHeaders.getContentLength();
if (contentLength > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Use setFixedLengthStreamingMode() or "
+ "setChunkedStreamingMode() for requests larger than 2 GiB.");
}
// Buffer a request body of a known length.
int contentLength = httpEngine.requestHeaders.getContentLength();
if (contentLength != -1) {
writeRequestHeaders();
return new RetryableOutputStream((int) contentLength);
return new RetryableOutputStream(contentLength);
}
// Buffer a request body of an unknown length. Don't write request
@@ -132,18 +127,15 @@ public final class HttpTransport implements Transport {
}
@Override public ResponseHeaders readResponseHeaders() throws IOException {
RawHeaders rawHeaders = RawHeaders.fromBytes(socketIn);
httpEngine.connection.setHttpMinorVersion(rawHeaders.getHttpMinorVersion());
httpEngine.receiveHeaders(rawHeaders);
ResponseHeaders headers = new ResponseHeaders(httpEngine.uri, rawHeaders);
headers.setTransport("http/1.1");
return headers;
RawHeaders headers = RawHeaders.fromBytes(socketIn);
httpEngine.connection.setHttpMinorVersion(headers.getHttpMinorVersion());
httpEngine.receiveHeaders(headers);
return new ResponseHeaders(httpEngine.uri, headers);
}
public boolean makeReusable(boolean streamCanceled, OutputStream requestBodyOut,
public boolean makeReusable(boolean streamCancelled, OutputStream requestBodyOut,
InputStream responseBodyIn) {
if (streamCanceled) {
if (streamCancelled) {
return false;
}
@@ -177,10 +169,6 @@ public final class HttpTransport implements Transport {
* 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.
*
* <p>A stream may be discarded to encourage response caching (a response
* cannot be cached unless it is consumed completely) or to enable connection
* reuse.
*/
private static boolean discardStream(HttpEngine httpEngine, InputStream responseBodyIn) {
Connection connection = httpEngine.connection;
@@ -224,9 +212,9 @@ public final class HttpTransport implements Transport {
/** An HTTP body with a fixed length known in advance. */
private static final class FixedLengthOutputStream extends AbstractOutputStream {
private final OutputStream socketOut;
private long bytesRemaining;
private int bytesRemaining;
private FixedLengthOutputStream(OutputStream socketOut, long bytesRemaining) {
private FixedLengthOutputStream(OutputStream socketOut, int bytesRemaining) {
this.socketOut = socketOut;
this.bytesRemaining = bytesRemaining;
}
@@ -370,14 +358,14 @@ public final class HttpTransport implements Transport {
/** An HTTP body with a fixed length specified in advance. */
private static class FixedLengthInputStream extends AbstractHttpInputStream {
private long bytesRemaining;
private int bytesRemaining;
public FixedLengthInputStream(InputStream is, CacheRequest cacheRequest, HttpEngine httpEngine,
long length) throws IOException {
int length) throws IOException {
super(is, httpEngine, cacheRequest);
bytesRemaining = length;
if (bytesRemaining == 0) {
endOfInput();
endOfInput(false);
}
}
@@ -387,7 +375,7 @@ public final class HttpTransport implements Transport {
if (bytesRemaining == 0) {
return -1;
}
int read = in.read(buffer, offset, (int) Math.min(count, bytesRemaining));
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");
@@ -395,14 +383,14 @@ public final class HttpTransport implements Transport {
bytesRemaining -= read;
cacheWrite(buffer, offset, read);
if (bytesRemaining == 0) {
endOfInput();
endOfInput(false);
}
return read;
}
@Override public int available() throws IOException {
checkNotClosed();
return bytesRemaining == 0 ? 0 : (int) Math.min(in.available(), bytesRemaining);
return bytesRemaining == 0 ? 0 : Math.min(in.available(), bytesRemaining);
}
@Override public void close() throws IOException {
@@ -472,7 +460,7 @@ public final class HttpTransport implements Transport {
RawHeaders rawResponseHeaders = httpEngine.responseHeaders.getHeaders();
RawHeaders.readHeaders(transport.socketIn, rawResponseHeaders);
httpEngine.receiveHeaders(rawResponseHeaders);
endOfInput();
endOfInput(false);
}
}

View File

@@ -18,28 +18,33 @@
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.internal.Platform;
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.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
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;
@@ -57,10 +62,10 @@ import static com.squareup.okhttp.internal.Util.getEffectivePort;
* 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 implements Policy {
public class HttpURLConnectionImpl extends HttpURLConnection {
/** Numeric status code, 307: Temporary Redirect. */
public static final int HTTP_TEMP_REDIRECT = 307;
static final int HTTP_TEMP_REDIRECT = 307;
/**
* How many redirects should we follow? Chrome follows 21; Firefox, curl,
@@ -68,19 +73,51 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
*/
private static final int MAX_REDIRECTS = 20;
final OkHttpClient client;
/**
* 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();
/** Like the superclass field of the same name, but a long and available on all platforms. */
private long fixedContentLength = -1;
private int redirectionCount;
private FaultRecoveringOutputStream faultRecoveringRequestBody;
protected IOException httpEngineFailure;
protected HttpEngine httpEngine;
private Proxy selectedProxy;
public HttpURLConnectionImpl(URL url, OkHttpClient client) {
public HttpURLConnectionImpl(URL url, OkHttpClient client, OkResponseCache responseCache,
Set<Route> failedRoutes) {
super(url);
this.client = client;
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 {
@@ -160,7 +197,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
try {
return getResponse().getResponseHeaders().getHeaders().toMultimap(true);
} catch (IOException e) {
return Collections.emptyMap();
return null;
}
}
@@ -204,14 +241,29 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
throw new ProtocolException("cannot write request body after response has been read");
}
return out;
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) client.getProxy().address();
InetSocketAddress proxyAddress = (InetSocketAddress) requestedProxy.address();
hostName = proxyAddress.getHostName();
hostPort = proxyAddress.getPort();
}
@@ -225,22 +277,6 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
return rawRequestHeaders.get(field);
}
@Override public void setConnectTimeout(int timeoutMillis) {
client.setConnectTimeout(timeoutMillis, TimeUnit.MILLISECONDS);
}
@Override public int getConnectTimeout() {
return client.getConnectTimeout();
}
@Override public void setReadTimeout(int timeoutMillis) {
client.setReadTimeout(timeoutMillis, TimeUnit.MILLISECONDS);
}
@Override public int getReadTimeout() {
return client.getReadTimeout();
}
private void initHttpEngine() throws IOException {
if (httpEngineFailure != null) {
throw httpEngineFailure;
@@ -254,8 +290,8 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
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") && !method.equals("PATCH")) {
// If the request method is neither POST nor PUT nor PATCH, then you're not writing
} 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");
}
}
@@ -266,16 +302,17 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
}
}
@Override public HttpURLConnection getHttpConnectionToCache() {
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(client, this, method, requestHeaders, connection, requestBody);
return new HttpEngine(this, method, requestHeaders, connection, requestBody);
} else if (url.getProtocol().equals("https")) {
return new HttpsEngine(client, this, method, requestHeaders, connection, requestBody);
return new HttpsURLConnectionImpl.HttpsEngine(
this, method, requestHeaders, connection, requestBody);
} else {
throw new AssertionError();
}
@@ -311,7 +348,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
// 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 = httpEngine.getResponseCode();
int responseCode = getResponseCode();
if (responseCode == HTTP_MULT_CHOICE
|| responseCode == HTTP_MOVED_PERM
|| responseCode == HTTP_MOVED_TEMP
@@ -321,7 +358,8 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
}
if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) {
throw new HttpRetryException("Cannot retry streamed HTTP body", responseCode);
throw new HttpRetryException("Cannot retry streamed HTTP body",
httpEngine.getResponseCode());
}
if (retry == Retry.DIFFERENT_CONNECTION) {
@@ -332,11 +370,6 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
httpEngine = newHttpEngine(retryMethod, rawRequestHeaders, httpEngine.getConnection(),
(RetryableOutputStream) requestBody);
if (requestBody == null) {
// Drop the Content-Length header when redirected from POST to GET.
httpEngine.getRequestHeaders().removeContentLength();
}
}
}
@@ -351,7 +384,6 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
if (readResponse) {
httpEngine.readResponse();
}
return true;
} catch (IOException e) {
if (handleFailure(e)) {
@@ -375,7 +407,8 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
OutputStream requestBody = httpEngine.getRequestBody();
boolean canRetryRequestBody = requestBody == null
|| requestBody instanceof RetryableOutputStream;
|| 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)
@@ -385,9 +418,15 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
}
httpEngine.release(true);
RetryableOutputStream retryableOutputStream = (RetryableOutputStream) requestBody;
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;
}
@@ -412,13 +451,13 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
/**
* Returns the retry action to take for the current response headers. The
* headers, proxy and target URL for this connection may be adjusted to
* 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()
: client.getProxy();
: requestedProxy;
final int responseCode = getResponseCode();
switch (responseCode) {
case HTTP_PROXY_AUTH:
@@ -427,9 +466,8 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
}
// fall-through
case HTTP_UNAUTHORIZED:
boolean credentialsFound = HttpAuthenticator.processAuthHeader(client.getAuthenticator(),
getResponseCode(), httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders,
selectedProxy, url);
boolean credentialsFound = HttpAuthenticator.processAuthHeader(getResponseCode(),
httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders, selectedProxy, url);
return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE;
case HTTP_MULT_CHOICE:
@@ -458,7 +496,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
return Retry.NONE; // Don't follow redirects to unsupported protocols.
}
boolean sameProtocol = previousUrl.getProtocol().equals(url.getProtocol());
if (!sameProtocol && !client.getFollowProtocolRedirects()) {
if (!sameProtocol && !followProtocolRedirects) {
return Retry.NONE; // This client doesn't follow redirects across protocols.
}
boolean sameHost = previousUrl.getHost().equals(url.getHost());
@@ -475,29 +513,17 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
}
/** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */
@Override public final long getFixedContentLength() {
final int getFixedContentLength() {
return fixedContentLength;
}
@Override public final int getChunkLength() {
/** @see java.net.HttpURLConnection#setChunkedStreamingMode(int) */
final int getChunkLength() {
return chunkLength;
}
@Override public final boolean usingProxy() {
if (selectedProxy != null) {
return isValidNonDirectProxy(selectedProxy);
}
// This behavior is a bit odd (but is probably justified by the
// oddness of the APIs involved). Before a connection is established,
// this method will return true only if this connection was explicitly
// opened with a Proxy. We don't attempt to query the ProxySelector
// at all.
return isValidNonDirectProxy(client.getProxy());
}
private static boolean isValidNonDirectProxy(Proxy proxy) {
return proxy != null && proxy.type() != Proxy.Type.DIRECT;
return (requestedProxy != null && requestedProxy.type() != Proxy.Type.DIRECT);
}
@Override public String getResponseMessage() throws IOException {
@@ -515,21 +541,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
if (field == null) {
throw new NullPointerException("field == null");
}
if (newValue == null) {
// Silently ignore null header values for backwards compatibility with older
// android versions as well as with other URLConnection implementations.
//
// Some implementations send a malformed HTTP header when faced with
// such requests, we respect the spec and ignore the header.
Platform.get().logW("Ignoring header " + field + " because its value was null.");
return;
}
if ("X-Android-Transports".equals(field)) {
setTransports(newValue, false /* append */);
} else {
rawRequestHeaders.set(field, newValue);
}
rawRequestHeaders.set(field, newValue);
}
@Override public final void addRequestProperty(String field, String value) {
@@ -539,52 +551,6 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
if (field == null) {
throw new NullPointerException("field == null");
}
if (value == null) {
// Silently ignore null header values for backwards compatibility with older
// android versions as well as with other URLConnection implementations.
//
// Some implementations send a malformed HTTP header when faced with
// such requests, we respect the spec and ignore the header.
Platform.get().logW("Ignoring header " + field + " because its value was null.");
return;
}
if ("X-Android-Transports".equals(field)) {
setTransports(value, true /* append */);
} else {
rawRequestHeaders.add(field, value);
}
}
/*
* Splits and validates a comma-separated string of transports.
* When append == false, we require that the transport list contains "http/1.1".
*/
private void setTransports(String transportsString, boolean append) {
List<String> transportsList = new ArrayList<String>();
if (append) {
transportsList.addAll(client.getTransports());
}
for (String transport : transportsString.split(",", -1)) {
transportsList.add(transport);
}
client.setTransports(transportsList);
}
@Override public void setFixedLengthStreamingMode(int contentLength) {
setFixedLengthStreamingMode((long) contentLength);
}
// @Override Don't override: this overload method doesn't exist prior to Java 1.7.
public void setFixedLengthStreamingMode(long contentLength) {
if (super.connected) throw new IllegalStateException("Already connected");
if (chunkLength > 0) throw new IllegalStateException("Already in chunked mode");
if (contentLength < 0) throw new IllegalArgumentException("contentLength < 0");
this.fixedContentLength = contentLength;
super.fixedContentLength = (int) Math.min(contentLength, Integer.MAX_VALUE);
}
@Override public final void setSelectedProxy(Proxy proxy) {
this.selectedProxy = proxy;
rawRequestHeaders.add(field, value);
}
}

View File

@@ -1,72 +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.
*/
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Connection;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.TunnelRequest;
import java.io.IOException;
import java.net.CacheResponse;
import java.net.SecureCacheResponse;
import java.net.URL;
import javax.net.ssl.SSLSocket;
import static com.squareup.okhttp.internal.Util.getEffectivePort;
public final class HttpsEngine extends HttpEngine {
/**
* Stash of HttpsEngine.connection.socket to implement requests like {@code
* HttpsURLConnection#getCipherSuite} even after the connection has been
* recycled.
*/
private SSLSocket sslSocket;
public HttpsEngine(OkHttpClient client, Policy policy, String method, RawHeaders requestHeaders,
Connection connection, RetryableOutputStream requestBody) throws IOException {
super(client, policy, method, requestHeaders, connection, requestBody);
this.sslSocket = connection != null ? (SSLSocket) connection.getSocket() : null;
}
@Override protected void connected(Connection connection) {
this.sslSocket = (SSLSocket) connection.getSocket();
super.connected(connection);
}
@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 path.
return false;
}
public SSLSocket getSslSocket() {
return sslSocket;
}
@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());
}
}

View File

@@ -16,11 +16,14 @@
*/
package com.squareup.okhttp.internal.http;
import android.annotation.SuppressLint;
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;
@@ -30,20 +33,24 @@ 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) {
public HttpsURLConnectionImpl(URL url, OkHttpClient client, OkResponseCache responseCache,
Set<Route> failedRoutes) {
super(url);
delegate = new HttpUrlConnectionDelegate(url, client);
delegate = new HttpUrlConnectionDelegate(url, client, responseCache, failedRoutes);
}
@Override public String getCipherSuite() {
@@ -113,247 +120,294 @@ public final class HttpsURLConnectionImpl extends HttpsURLConnection {
}
private SSLSocket getSslSocket() {
if (delegate.httpEngine == null || !delegate.httpEngine.connected) {
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).getSslSocket()
? ((HttpsEngine) delegate.httpEngine).sslSocket
: null; // Not HTTPS! Probably an https:// to http:// redirect.
}
@Override public void disconnect() {
@Override
public void disconnect() {
delegate.disconnect();
}
@Override public InputStream getErrorStream() {
@Override
public InputStream getErrorStream() {
return delegate.getErrorStream();
}
@Override public String getRequestMethod() {
@Override
public String getRequestMethod() {
return delegate.getRequestMethod();
}
@Override public int getResponseCode() throws IOException {
@Override
public int getResponseCode() throws IOException {
return delegate.getResponseCode();
}
@Override public String getResponseMessage() throws IOException {
@Override
public String getResponseMessage() throws IOException {
return delegate.getResponseMessage();
}
@Override public void setRequestMethod(String method) throws ProtocolException {
@Override
public void setRequestMethod(String method) throws ProtocolException {
delegate.setRequestMethod(method);
}
@Override public boolean usingProxy() {
@Override
public boolean usingProxy() {
return delegate.usingProxy();
}
@Override public boolean getInstanceFollowRedirects() {
@Override
public boolean getInstanceFollowRedirects() {
return delegate.getInstanceFollowRedirects();
}
@Override public void setInstanceFollowRedirects(boolean followRedirects) {
@Override
public void setInstanceFollowRedirects(boolean followRedirects) {
delegate.setInstanceFollowRedirects(followRedirects);
}
@Override public void connect() throws IOException {
@Override
public void connect() throws IOException {
connected = true;
delegate.connect();
}
@Override public boolean getAllowUserInteraction() {
@Override
public boolean getAllowUserInteraction() {
return delegate.getAllowUserInteraction();
}
@Override public Object getContent() throws IOException {
@Override
public Object getContent() throws IOException {
return delegate.getContent();
}
@SuppressWarnings("unchecked") // Spec does not generify
@Override public Object getContent(Class[] types) throws IOException {
@Override
public Object getContent(Class[] types) throws IOException {
return delegate.getContent(types);
}
@Override public String getContentEncoding() {
@Override
public String getContentEncoding() {
return delegate.getContentEncoding();
}
@Override public int getContentLength() {
@Override
public int getContentLength() {
return delegate.getContentLength();
}
@Override public String getContentType() {
@Override
public String getContentType() {
return delegate.getContentType();
}
@Override public long getDate() {
@Override
public long getDate() {
return delegate.getDate();
}
@Override public boolean getDefaultUseCaches() {
@Override
public boolean getDefaultUseCaches() {
return delegate.getDefaultUseCaches();
}
@Override public boolean getDoInput() {
@Override
public boolean getDoInput() {
return delegate.getDoInput();
}
@Override public boolean getDoOutput() {
@Override
public boolean getDoOutput() {
return delegate.getDoOutput();
}
@Override public long getExpiration() {
@Override
public long getExpiration() {
return delegate.getExpiration();
}
@Override public String getHeaderField(int pos) {
@Override
public String getHeaderField(int pos) {
return delegate.getHeaderField(pos);
}
@Override public Map<String, List<String>> getHeaderFields() {
@Override
public Map<String, List<String>> getHeaderFields() {
return delegate.getHeaderFields();
}
@Override public Map<String, List<String>> getRequestProperties() {
@Override
public Map<String, List<String>> getRequestProperties() {
return delegate.getRequestProperties();
}
@Override public void addRequestProperty(String field, String newValue) {
@Override
public void addRequestProperty(String field, String newValue) {
delegate.addRequestProperty(field, newValue);
}
@Override public String getHeaderField(String key) {
@Override
public String getHeaderField(String key) {
return delegate.getHeaderField(key);
}
@Override public long getHeaderFieldDate(String field, long defaultValue) {
@Override
public long getHeaderFieldDate(String field, long defaultValue) {
return delegate.getHeaderFieldDate(field, defaultValue);
}
@Override public int getHeaderFieldInt(String field, int defaultValue) {
@Override
public int getHeaderFieldInt(String field, int defaultValue) {
return delegate.getHeaderFieldInt(field, defaultValue);
}
@Override public String getHeaderFieldKey(int position) {
@Override
public String getHeaderFieldKey(int position) {
return delegate.getHeaderFieldKey(position);
}
@Override public long getIfModifiedSince() {
@Override
public long getIfModifiedSince() {
return delegate.getIfModifiedSince();
}
@Override public InputStream getInputStream() throws IOException {
@Override
public InputStream getInputStream() throws IOException {
return delegate.getInputStream();
}
@Override public long getLastModified() {
@Override
public long getLastModified() {
return delegate.getLastModified();
}
@Override public OutputStream getOutputStream() throws IOException {
@Override
public OutputStream getOutputStream() throws IOException {
return delegate.getOutputStream();
}
@Override public Permission getPermission() throws IOException {
@Override
public Permission getPermission() throws IOException {
return delegate.getPermission();
}
@Override public String getRequestProperty(String field) {
@Override
public String getRequestProperty(String field) {
return delegate.getRequestProperty(field);
}
@Override public URL getURL() {
@Override
public URL getURL() {
return delegate.getURL();
}
@Override public boolean getUseCaches() {
@Override
public boolean getUseCaches() {
return delegate.getUseCaches();
}
@Override public void setAllowUserInteraction(boolean newValue) {
@Override
public void setAllowUserInteraction(boolean newValue) {
delegate.setAllowUserInteraction(newValue);
}
@Override public void setDefaultUseCaches(boolean newValue) {
@Override
public void setDefaultUseCaches(boolean newValue) {
delegate.setDefaultUseCaches(newValue);
}
@Override public void setDoInput(boolean newValue) {
@Override
public void setDoInput(boolean newValue) {
delegate.setDoInput(newValue);
}
@Override public void setDoOutput(boolean newValue) {
@Override
public void setDoOutput(boolean newValue) {
delegate.setDoOutput(newValue);
}
@Override public void setIfModifiedSince(long newValue) {
@Override
public void setIfModifiedSince(long newValue) {
delegate.setIfModifiedSince(newValue);
}
@Override public void setRequestProperty(String field, String newValue) {
@Override
public void setRequestProperty(String field, String newValue) {
delegate.setRequestProperty(field, newValue);
}
@Override public void setUseCaches(boolean newValue) {
@Override
public void setUseCaches(boolean newValue) {
delegate.setUseCaches(newValue);
}
@Override public void setConnectTimeout(int timeoutMillis) {
@Override
public void setConnectTimeout(int timeoutMillis) {
delegate.setConnectTimeout(timeoutMillis);
}
@Override public int getConnectTimeout() {
@Override
public int getConnectTimeout() {
return delegate.getConnectTimeout();
}
@Override public void setReadTimeout(int timeoutMillis) {
@Override
public void setReadTimeout(int timeoutMillis) {
delegate.setReadTimeout(timeoutMillis);
}
@Override public int getReadTimeout() {
@Override
public int getReadTimeout() {
return delegate.getReadTimeout();
}
@Override public String toString() {
@Override
public String toString() {
return delegate.toString();
}
@Override public void setFixedLengthStreamingMode(int contentLength) {
@Override
public void setFixedLengthStreamingMode(int contentLength) {
delegate.setFixedLengthStreamingMode(contentLength);
}
@Override public void setChunkedStreamingMode(int chunkLength) {
@Override
public void setChunkedStreamingMode(int chunkLength) {
delegate.setChunkedStreamingMode(chunkLength);
}
@Override public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
delegate.client.setHostnameVerifier(hostnameVerifier);
delegate.hostnameVerifier = hostnameVerifier;
}
@Override public HostnameVerifier getHostnameVerifier() {
return delegate.client.getHostnameVerifier();
return delegate.hostnameVerifier;
}
@Override public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
delegate.client.setSslSocketFactory(sslSocketFactory);
delegate.sslSocketFactory = sslSocketFactory;
}
@Override public SSLSocketFactory getSSLSocketFactory() {
return delegate.client.getSslSocketFactory();
}
@SuppressLint("NewApi")
@Override public void setFixedLengthStreamingMode(long contentLength) {
delegate.setFixedLengthStreamingMode(contentLength);
return delegate.sslSocketFactory;
}
private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl {
private HttpUrlConnectionDelegate(URL url, OkHttpClient client) {
super(url, client);
private HttpUrlConnectionDelegate(URL url, OkHttpClient client, OkResponseCache responseCache,
Set<Route> failedRoutes) {
super(url, client, responseCache, failedRoutes);
}
@Override public HttpURLConnection getHttpConnectionToCache() {
@Override protected HttpURLConnection getHttpConnectionToCache() {
return HttpsURLConnectionImpl.this;
}
@@ -363,4 +417,45 @@ public final class HttpsURLConnectionImpl extends HttpsURLConnection {
: 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());
}
}
}

View File

@@ -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);
}

View File

@@ -15,7 +15,6 @@
*/
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.OkResponseCache;
import com.squareup.okhttp.ResponseSource;
import java.io.IOException;
import java.net.CacheRequest;
@@ -42,9 +41,6 @@ public final class OkResponseCacheAdapter implements OkResponseCache {
return responseCache.put(uri, urlConnection);
}
@Override public void maybeRemove(String requestMethod, URI uri) throws IOException {
}
@Override public void update(CacheResponse conditionalCacheHit, HttpURLConnection connection)
throws IOException {
}

View File

@@ -1,49 +0,0 @@
/*
* 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 java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
public interface Policy {
/** Returns true if HTTP response caches should be used. */
boolean getUseCaches();
/** Returns the HttpURLConnection instance to store in the cache. */
HttpURLConnection getHttpConnectionToCache();
/** Returns the current destination URL, possibly a redirect. */
URL getURL();
/** Returns the If-Modified-Since timestamp, or 0 if none is set. */
long getIfModifiedSince();
/** Returns true if a non-direct proxy is specified. */
boolean usingProxy();
/** @see java.net.HttpURLConnection#setChunkedStreamingMode(int) */
int getChunkLength();
/** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */
long getFixedContentLength();
/**
* Sets the current proxy that this connection is using.
* @see java.net.HttpURLConnection#usingProxy
*/
void setSelectedProxy(Proxy proxy);
}

View File

@@ -32,7 +32,6 @@ import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* The HTTP status and unparsed header fields of a single HTTP message. Values
@@ -123,6 +122,23 @@ public final class RawHeaders {
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"
@@ -164,17 +180,14 @@ public final class RawHeaders {
/**
* Add an HTTP header line containing a field name, a literal colon, and a
* value. This works around empty header names and header names that start
* with a colon (created by old broken SPDY versions of the response cache).
* value.
*/
public void addLine(String line) {
int index = line.indexOf(":", 1);
if (index != -1) {
addLenient(line.substring(0, index), line.substring(index + 1));
} else if (line.startsWith(":")) {
addLenient("", line.substring(1)); // Empty header name.
int index = line.indexOf(":");
if (index == -1) {
addLenient("", line);
} else {
addLenient("", line); // No header name.
addLenient(line.substring(0, index), line.substring(index + 1));
}
}
@@ -235,15 +248,6 @@ public final class RawHeaders {
return namesAndValues.get(fieldNameIndex);
}
/** Returns an immutable case-insensitive set of header names. */
public Set<String> names() {
TreeSet<String> result = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
for (int i = 0; i < length(); i++) {
result.add(getFieldName(i));
}
return Collections.unmodifiableSet(result);
}
/** Returns the value at {@code index} or null if that is out of range. */
public String getValue(int index) {
int valueIndex = index * 2 + 1;
@@ -263,20 +267,6 @@ public final class RawHeaders {
return null;
}
/** Returns an immutable list of the header values for {@code name}. */
public List<String> values(String name) {
List<String> result = null;
for (int i = 0; i < length(); i++) {
if (name.equalsIgnoreCase(getFieldName(i))) {
if (result == null) result = new ArrayList<String>(2);
result.add(getValue(i));
}
}
return result != null
? Collections.unmodifiableList(result)
: Collections.<String>emptyList();
}
/** @param fieldNames a case-insensitive set of HTTP header field names. */
public RawHeaders getAll(Set<String> fieldNames) {
RawHeaders result = new RawHeaders();
@@ -411,13 +401,10 @@ public final class RawHeaders {
return result;
}
/** Returns headers for a name value block containing a SPDY response. */
public static RawHeaders fromNameValueBlock(List<String> nameValueBlock) throws IOException {
public static RawHeaders fromNameValueBlock(List<String> nameValueBlock) {
if (nameValueBlock.size() % 2 != 0) {
throw new IllegalArgumentException("Unexpected name value block: " + nameValueBlock);
}
String status = null;
String version = null;
RawHeaders result = new RawHeaders();
for (int i = 0; i < nameValueBlock.size(); i += 2) {
String name = nameValueBlock.get(i);
@@ -427,21 +414,11 @@ public final class RawHeaders {
if (end == -1) {
end = values.length();
}
String value = values.substring(start, end);
if (":status".equals(name)) {
status = value;
} else if (":version".equals(name)) {
version = value;
} else {
result.namesAndValues.add(name);
result.namesAndValues.add(value);
}
result.namesAndValues.add(name);
result.namesAndValues.add(values.substring(start, end));
start = end + 1;
}
}
if (status == null) throw new ProtocolException("Expected ':status' header not present");
if (version == null) throw new ProtocolException("Expected ':version' header not present");
result.setStatusLine(version + " " + status);
return result;
}
}

View File

@@ -48,7 +48,7 @@ public final class RequestHeaders {
*/
private boolean hasAuthorization;
private long contentLength = -1;
private int contentLength = -1;
private String transferEncoding;
private String userAgent;
private String host;
@@ -157,7 +157,7 @@ public final class RequestHeaders {
return hasAuthorization;
}
public long getContentLength() {
public int getContentLength() {
return contentLength;
}
@@ -205,26 +205,14 @@ public final class RequestHeaders {
this.transferEncoding = "chunked";
}
public void setContentLength(long contentLength) {
public void setContentLength(int contentLength) {
if (this.contentLength != -1) {
headers.removeAll("Content-Length");
}
headers.add("Content-Length", Long.toString(contentLength));
headers.add("Content-Length", Integer.toString(contentLength));
this.contentLength = contentLength;
}
/**
* Remove the Content-Length headers. Call this when dropping the body on a
* request or response, such as when a redirect changes the method from POST
* to GET.
*/
public void removeContentLength() {
if (contentLength != -1) {
headers.removeAll("Content-Length");
contentLength = -1;
}
}
public void setUserAgent(String userAgent) {
if (this.userAgent != null) {
headers.removeAll("User-Agent");
@@ -294,24 +282,9 @@ public final class RequestHeaders {
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))
&& !entry.getValue().isEmpty()) {
headers.add(key, buildCookieHeader(entry.getValue()));
if ("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key)) {
headers.addAll(key, entry.getValue());
}
}
}
/**
* Send all cookies in one big header, as recommended by
* <a href="http://tools.ietf.org/html/rfc6265#section-4.2.1">RFC 6265</a>.
*/
private String buildCookieHeader(List<String> cookies) {
if (cookies.size() == 1) return cookies.get(0);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < cookies.size(); i++) {
if (i > 0) sb.append("; ");
sb.append(cookies.get(i));
}
return sb.toString();
}
}

View File

@@ -17,7 +17,6 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.ResponseSource;
import com.squareup.okhttp.internal.Platform;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
@@ -35,16 +34,13 @@ import static com.squareup.okhttp.internal.Util.equal;
public final class ResponseHeaders {
/** HTTP header name for the local time when the request was sent. */
private static final String SENT_MILLIS = Platform.get().getPrefix() + "-Sent-Millis";
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 = Platform.get().getPrefix() + "-Received-Millis";
private static final String RECEIVED_MILLIS = "X-Android-Received-Millis";
/** HTTP synthetic header with the response source. */
static final String RESPONSE_SOURCE = Platform.get().getPrefix() + "-Response-Source";
/** HTTP synthetic header with the selected transport (spdy/3, http/1.1, etc). */
static final String SELECTED_TRANSPORT = Platform.get().getPrefix() + "-Selected-Transport";
static final String RESPONSE_SOURCE = "X-Android-Response-Source";
private final URI uri;
private final RawHeaders headers;
@@ -114,9 +110,8 @@ public final class ResponseHeaders {
private String contentEncoding;
private String transferEncoding;
private long contentLength = -1;
private int contentLength = -1;
private String connection;
private String contentType;
public ResponseHeaders(URI uri, RawHeaders headers) {
this.uri = uri;
@@ -173,11 +168,9 @@ public final class ResponseHeaders {
transferEncoding = value;
} else if ("Content-Length".equalsIgnoreCase(fieldName)) {
try {
contentLength = Long.parseLong(value);
contentLength = Integer.parseInt(value);
} catch (NumberFormatException ignored) {
}
} else if ("Content-Type".equalsIgnoreCase(fieldName)) {
contentType = value;
} else if ("Connection".equalsIgnoreCase(fieldName)) {
connection = value;
} else if (SENT_MILLIS.equalsIgnoreCase(fieldName)) {
@@ -266,14 +259,10 @@ public final class ResponseHeaders {
return contentEncoding;
}
public long getContentLength() {
public int getContentLength() {
return contentLength;
}
public String getContentType() {
return contentType;
}
public String getConnection() {
return connection;
}
@@ -289,10 +278,6 @@ public final class ResponseHeaders {
headers.set(RESPONSE_SOURCE, responseSource.toString() + " " + headers.getResponseCode());
}
public void setTransport(String transport) {
headers.set(SELECTED_TRANSPORT, transport);
}
/**
* Returns the current age of the response, in milliseconds. The calculation
* is specified by RFC 2616, 13.2.3 Age Calculations.

View File

@@ -19,7 +19,6 @@ import com.squareup.okhttp.Address;
import com.squareup.okhttp.Connection;
import com.squareup.okhttp.ConnectionPool;
import com.squareup.okhttp.Route;
import com.squareup.okhttp.RouteDatabase;
import com.squareup.okhttp.internal.Dns;
import java.io.IOException;
import java.net.InetAddress;
@@ -33,6 +32,8 @@ import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.net.ssl.SSLHandshakeException;
import static com.squareup.okhttp.internal.Util.getEffectivePort;
@@ -54,7 +55,7 @@ public final class RouteSelector {
private final ProxySelector proxySelector;
private final ConnectionPool pool;
private final Dns dns;
private final RouteDatabase routeDatabase;
private final Set<Route> failedRoutes;
/* The most recently attempted route. */
private Proxy lastProxy;
@@ -77,13 +78,13 @@ public final class RouteSelector {
private final List<Route> postponedRoutes;
public RouteSelector(Address address, URI uri, ProxySelector proxySelector, ConnectionPool pool,
Dns dns, RouteDatabase routeDatabase) {
Dns dns, Set<Route> failedRoutes) {
this.address = address;
this.uri = uri;
this.proxySelector = proxySelector;
this.pool = pool;
this.dns = dns;
this.routeDatabase = routeDatabase;
this.failedRoutes = failedRoutes;
this.postponedRoutes = new LinkedList<Route>();
resetNextProxy(uri, address.getProxy());
@@ -102,11 +103,11 @@ public final class RouteSelector {
*
* @throws NoSuchElementException if there are no more routes to attempt.
*/
public Connection next(String method) throws IOException {
public Connection next() throws IOException {
// Always prefer pooled connections over new connections.
for (Connection pooled; (pooled = pool.get(address)) != null; ) {
if (method.equals("GET") || pooled.isReadable()) return pooled;
pooled.close();
Connection pooled = pool.get(address);
if (pooled != null) {
return pooled;
}
// Compute the next route to attempt.
@@ -127,11 +128,11 @@ public final class RouteSelector {
boolean modernTls = nextTlsMode() == TLS_MODE_MODERN;
Route route = new Route(address, lastProxy, lastInetSocketAddress, modernTls);
if (routeDatabase.shouldPostpone(route)) {
if (failedRoutes.contains(route)) {
postponedRoutes.add(route);
// We will only recurse in order to skip previously failed routes. They will be
// tried last.
return next(method);
return next();
}
return new Connection(route);
@@ -148,7 +149,12 @@ public final class RouteSelector {
proxySelector.connectFailed(uri, failedRoute.getProxy().address(), failure);
}
routeDatabase.failed(failedRoute, failure);
failedRoutes.add(failedRoute);
if (!(failure instanceof SSLHandshakeException)) {
// If the problem was not related to SSL then it will also fail with
// a different Tls mode therefore we can be proactive about it.
failedRoutes.add(failedRoute.flipTlsMode());
}
}
/** Resets {@link #nextProxy} to the first option. */

View File

@@ -16,7 +16,6 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.internal.spdy.ErrorCode;
import com.squareup.okhttp.internal.spdy.SpdyConnection;
import com.squareup.okhttp.internal.spdy.SpdyStream;
import java.io.IOException;
@@ -37,10 +36,6 @@ public final class SpdyTransport implements Transport {
}
@Override public OutputStream createRequestBody() throws IOException {
long fixedContentLength = httpEngine.policy.getFixedContentLength();
if (fixedContentLength != -1) {
httpEngine.requestHeaders.setContentLength(fixedContentLength);
}
// TODO: if we aren't streaming up to the server, we should buffer the whole request
writeRequestHeaders();
return stream.getOutputStream();
@@ -60,7 +55,7 @@ public final class SpdyTransport implements Transport {
boolean hasResponseBody = true;
stream = spdyConnection.newStream(requestHeaders.toNameValueBlock(), hasRequestBody,
hasResponseBody);
stream.setReadTimeout(httpEngine.client.getReadTimeout());
stream.setReadTimeout(httpEngine.policy.getReadTimeout());
}
@Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException {
@@ -74,26 +69,24 @@ public final class SpdyTransport implements Transport {
@Override public ResponseHeaders readResponseHeaders() throws IOException {
List<String> nameValueBlock = stream.getResponseHeaders();
RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock);
rawHeaders.computeResponseStatusLineFromSpdyHeaders();
httpEngine.receiveHeaders(rawHeaders);
ResponseHeaders headers = new ResponseHeaders(httpEngine.uri, rawHeaders);
headers.setTransport("spdy/3");
return headers;
return new ResponseHeaders(httpEngine.uri, rawHeaders);
}
@Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {
return new UnknownLengthHttpInputStream(stream.getInputStream(), cacheRequest, httpEngine);
}
@Override public boolean makeReusable(boolean streamCanceled, OutputStream requestBodyOut,
@Override public boolean makeReusable(boolean streamCancelled, OutputStream requestBodyOut,
InputStream responseBodyIn) {
if (streamCanceled) {
if (streamCancelled) {
if (stream != null) {
stream.closeLater(ErrorCode.CANCEL);
stream.closeLater(SpdyStream.RST_CANCEL);
return true;
} else {
// If stream is null, it either means that writeRequestHeaders wasn't called
// or that SpdyConnection#newStream threw an IOException. In both cases there's
// or that SpdyConnection#newStream threw an IOEXception. In both cases there's
// nothing to do here and this stream can't be reused.
return false;
}

View File

@@ -59,6 +59,6 @@ interface Transport {
InputStream getTransferStream(CacheRequest cacheRequest) throws IOException;
/** Returns true if the underlying connection can be recycled. */
boolean makeReusable(boolean streamCanceled, OutputStream requestBodyOut,
boolean makeReusable(boolean streamReusable, OutputStream requestBodyOut,
InputStream responseBodyIn);
}

View File

@@ -25,9 +25,9 @@ import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
final class UnknownLengthHttpInputStream extends AbstractHttpInputStream {
private boolean inputExhausted;
UnknownLengthHttpInputStream(InputStream in, CacheRequest cacheRequest, HttpEngine httpEngine)
UnknownLengthHttpInputStream(InputStream is, CacheRequest cacheRequest, HttpEngine httpEngine)
throws IOException {
super(in, httpEngine, cacheRequest);
super(is, httpEngine, cacheRequest);
}
@Override public int read(byte[] buffer, int offset, int count) throws IOException {
@@ -39,7 +39,7 @@ final class UnknownLengthHttpInputStream extends AbstractHttpInputStream {
int read = in.read(buffer, offset, count);
if (read == -1) {
inputExhausted = true;
endOfInput();
endOfInput(false);
return -1;
}
cacheWrite(buffer, offset, read);

View File

@@ -1,67 +0,0 @@
package com.squareup.okhttp.internal.spdy;
public enum ErrorCode {
/** Not an error! For SPDY stream resets, prefer null over NO_ERROR. */
NO_ERROR(0, -1, 0),
PROTOCOL_ERROR(1, 1, 1),
/** A subtype of PROTOCOL_ERROR used by SPDY. */
INVALID_STREAM(1, 2, -1),
/** A subtype of PROTOCOL_ERROR used by SPDY. */
UNSUPPORTED_VERSION(1, 4, -1),
/** A subtype of PROTOCOL_ERROR used by SPDY. */
STREAM_IN_USE(1, 8, -1),
/** A subtype of PROTOCOL_ERROR used by SPDY. */
STREAM_ALREADY_CLOSED(1, 9, -1),
INTERNAL_ERROR(2, 6, 2),
FLOW_CONTROL_ERROR(3, 7, -1),
STREAM_CLOSED(5, -1, -1),
FRAME_TOO_LARGE(6, 11, -1),
REFUSED_STREAM(7, 3, -1),
CANCEL(8, 5, -1),
COMPRESSION_ERROR(9, -1, -1),
INVALID_CREDENTIALS(-1, 10, -1);
public final int httpCode;
public final int spdyRstCode;
public final int spdyGoAwayCode;
private ErrorCode(int httpCode, int spdyRstCode, int spdyGoAwayCode) {
this.httpCode = httpCode;
this.spdyRstCode = spdyRstCode;
this.spdyGoAwayCode = spdyGoAwayCode;
}
public static ErrorCode fromSpdy3Rst(int code) {
for (ErrorCode errorCode : ErrorCode.values()) {
if (errorCode.spdyRstCode == code) return errorCode;
}
return null;
}
public static ErrorCode fromHttp2(int code) {
for (ErrorCode errorCode : ErrorCode.values()) {
if (errorCode.httpCode == code) return errorCode;
}
return null;
}
public static ErrorCode fromSpdyGoAway(int code) {
for (ErrorCode errorCode : ErrorCode.values()) {
if (errorCode.spdyGoAwayCode == code) return errorCode;
}
return null;
}
}

View File

@@ -1,55 +0,0 @@
/*
* 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.spdy;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/** Reads transport frames for SPDY/3 or HTTP/2.0. */
public interface FrameReader extends Closeable {
void readConnectionHeader() throws IOException;
boolean nextFrame(Handler handler) throws IOException;
public interface Handler {
void data(boolean inFinished, int streamId, InputStream in, int length) throws IOException;
/**
* Create or update incoming headers, creating the corresponding streams
* if necessary. Frames that trigger this are SPDY SYN_STREAM, HEADERS, and
* SYN_REPLY, and HTTP/2.0 HEADERS and PUSH_PROMISE.
*
* @param inFinished true if the sender will not send further frames.
* @param outFinished true if the receiver should not send further frames.
* @param streamId the stream owning these headers.
* @param associatedStreamId the stream that triggered the sender to create
* this stream.
* @param priority or -1 for no priority. For SPDY, priorities range from 0
* (highest) thru 7 (lowest). For HTTP/2.0, priorities range from 0
* (highest) thru 2**31-1 (lowest).
*/
void headers(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId,
int priority, List<String> nameValueBlock, HeadersMode headersMode);
void rstStream(int streamId, ErrorCode errorCode);
void settings(boolean clearPrevious, Settings settings);
void noop();
void ping(boolean reply, int payload1, int payload2);
void goAway(int lastGoodStreamId, ErrorCode errorCode);
void windowUpdate(int streamId, int deltaWindowSize, boolean endFlowControl);
void priority(int streamId, int priority);
}
}

View File

@@ -1,43 +0,0 @@
/*
* 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.spdy;
import java.io.Closeable;
import java.io.IOException;
import java.util.List;
/** Writes transport frames for SPDY/3 or HTTP/2.0. */
public interface FrameWriter extends Closeable {
/** HTTP/2.0 only. */
void connectionHeader() throws IOException;
/** SPDY/3 only. */
void flush() throws IOException;
void synStream(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId,
int priority, int slot, List<String> nameValueBlock) throws IOException;
void synReply(boolean outFinished, int streamId, List<String> nameValueBlock) throws IOException;
void headers(int streamId, List<String> nameValueBlock) throws IOException;
void rstStream(int streamId, ErrorCode errorCode) throws IOException;
void data(boolean outFinished, int streamId, byte[] data) throws IOException;
void data(boolean outFinished, int streamId, byte[] data, int offset, int byteCount)
throws IOException;
void settings(Settings settings) throws IOException;
void noop() throws IOException;
void ping(boolean reply, int payload1, int payload2) throws IOException;
void goAway(int lastGoodStreamId, ErrorCode errorCode) throws IOException;
void windowUpdate(int streamId, int deltaWindowSize) throws IOException;
}

View File

@@ -1,49 +0,0 @@
/*
* 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.spdy;
enum HeadersMode {
SPDY_SYN_STREAM,
SPDY_REPLY,
SPDY_HEADERS,
HTTP_20_HEADERS;
/** Returns true if it is an error these headers to create a new stream. */
public boolean failIfStreamAbsent() {
return this == SPDY_REPLY || this == SPDY_HEADERS;
}
/** Returns true if it is an error these headers to update an existing stream. */
public boolean failIfStreamPresent() {
return this == SPDY_SYN_STREAM;
}
/**
* Returns true if it is an error these headers to be the initial headers of a
* response.
*/
public boolean failIfHeadersAbsent() {
return this == SPDY_HEADERS;
}
/**
* Returns true if it is an error these headers to be update existing headers
* of a response.
*/
public boolean failIfHeadersPresent() {
return this == SPDY_REPLY;
}
}

Some files were not shown because too many files have changed in this diff Show More