html5 mobile boilerplate template. closes #119

This commit is contained in:
Thomas Reynolds 2011-09-20 09:17:46 -07:00
parent 80a3a9c82f
commit 4f09feeb2c
74 changed files with 17074 additions and 0 deletions

View file

@ -1,6 +1,7 @@
class Middleman::Templates::Html5 < Middleman::Templates::Base
class_option :css_dir, :default => "css"
class_option :js_dir, :default => "js"
class_option :images_dir, :default => "img"
def self.source_root
File.dirname(__FILE__)

View file

@ -0,0 +1,17 @@
class Middleman::Templates::Mobile < Middleman::Templates::Base
class_option :css_dir, :default => "css"
class_option :js_dir, :default => "js"
class_option :images_dir, :default => "img"
def self.source_root
File.dirname(__FILE__)
end
def build_scaffold
template "shared/config.tt", File.join(location, "config.rb")
directory "mobile/source", File.join(location, "source")
empty_directory File.join(location, "source")
end
end
Middleman::Templates.register(:mobile, Middleman::Templates::Mobile)

View file

@ -0,0 +1,38 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Page Not Found :(</title>
<style>
body { text-align: center;}
h1 { font-size: 50px; text-align: center }
span[frown] { transform: rotate(90deg); display:inline-block; color: #bbb; }
body { font: 20px Constantia, 'Hoefler Text', "Adobe Caslon Pro", Baskerville, Georgia, Times, serif; color: #999; text-shadow: 2px 2px 2px rgba(200, 200, 200, 0.5); }
::-moz-selection{ background:#FF5E99; color:#fff; }
::selection { background:#FF5E99; color:#fff; }
article {display:block; text-align: left; width: 500px; margin: 0 auto; }
a { color: rgb(36, 109, 56); text-decoration:none; }
a:hover { color: rgb(96, 73, 141) ; text-shadow: 2px 2px 2px rgba(36, 109, 56, 0.5); }
</style>
</head>
<body>
<article>
<h1>Not found <span frown>:(</span></h1>
<div>
<p>Sorry, but the page you were trying to view does not exist.</p>
<p>It looks like this was the result of either:</p>
<ul>
<li>a mistyped address</li>
<li>an out-of-date link</li>
</ul>
</div>
<script>
var GOOG_FIXURL_LANG = (navigator.language || '').slice(0,2),
GOOG_FIXURL_SITE = location.host;
</script>
<script src="http://linkhelp.clients.google.com/tbproxy/lh/wm/fixurl.js"></script>
</article>
</body>
</html>

View file

@ -0,0 +1,64 @@
#Mobile Boilerplate http://html5boilerplate.com
v1.0 (code named Secret Diary)
##Summary:
This is a set of features made specifically for mobile development, with following included:
###Markup:
Home screen icon (Android, iOS, Symbian)
CSS class target IE Mobile 7
Cross browser viewport optimization (Android, iOS, Mobile IE, Blackberry)
Optimized viewport scaling (Android, iOS, Mobile IE, Blackberry)
IE Mobile better Font rendering
Prevent scaling
iPhone full screen mode
###CSS:
Mobile helper class
Prevent text resize in ie/webkit browser
Prevent callout
HTML5 contenteditable attribute on mobile
S60 3.x and 5.0 devices which animated gif fix
Text overflow with ellipsis
Mobile optimized default CSS
###JavaScript:
Cross browser CSS media queries
Textarea autogrow
Hide webkit chrome
Insant button
Firebug lite debugger
Media queries for low end smartphone
###Server:
Added Blackberry MIME type
Added Nokia MIME type
Prevent Transcoding
Mobile site redirection
###General:
HTML5 offline caching for smartphone
Mobile sitemap
Mobile bookmark bubble
Browser Database Wrapper API
User Agent Detection
GA for low end mobile devices
Mobile build tool
##License:
###Major components:
css3-mediaqueries.js: Public Domain<br />
Bookmark bubble library: Apache License, Version 2.0<br />
Web Storage Portability Layer: Apache License, Version 2.0<br />
Modernizr: MIT/BSD license<br />
jQuery: MIT/GPL license<br />
HTML5Doctor CSS reset: Creative Commons 3.0 <br />
CSS Reset Reloaded: Public Domain
###Everything else:
The Unlicense (aka: public domain)

View file

@ -0,0 +1,25 @@
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<!-- Read this: www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html -->
<!-- Most restrictive policy: -->
<site-control permitted-cross-domain-policies="none"/>
<!-- Least restrictive policy: -->
<!--
<site-control permitted-cross-domain-policies="all"/>
<allow-access-from domain="*" to-ports="*" secure="false"/>
<allow-http-request-headers-from domain="*" headers="*" secure="false"/>
-->
<!--
If you host a crossdomain.xml file with allow-access-from domain=“*”
and dont understand all of the points described here, you probably
have a nasty security vulnerability. ~ simon willison
-->
</cross-domain-policy>

View file

@ -0,0 +1,236 @@
/**
* HTML5 Boilerplate
*
* style.css contains a reset, font normalization and some base styles.
*
* Credit is left where credit is due.
* Much inspiration was taken from these projects:
* - yui.yahooapis.com/2.8.1/build/base/base.css
* - camendesign.com/design/
* - praegnanz.de/weblog/htmlcssjs-kickstart
*/
/**
* html5doctor.com Reset Stylesheet (Eric Meyer's Reset Reloaded + HTML5 baseline)
* v1.6.1 2010-09-17 | Authors: Eric Meyer & Richard Clark
* html5doctor.com/html-5-reset-stylesheet/
*/
html, body, div, span, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp,
small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, figcaption, figure,
footer, header, hgroup, menu, nav, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
blockquote, q { quotes: none; }
blockquote:before, blockquote:after,
q:before, q:after { content: ""; content: none; }
ins { background-color: #ff9; color: #000; text-decoration: none; }
mark { background-color: #ff9; color: #000; font-style: italic; font-weight: bold; }
del { text-decoration: line-through; }
abbr[title], dfn[title] { border-bottom: 1px dotted; cursor: help; }
table { border-collapse: collapse; border-spacing: 0; }
hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; }
input, select { vertical-align: middle; }
/**
* Font normalization inspired by YUI Library's fonts.css: developer.yahoo.com/yui/
*/
body { font:13px/1.231 sans-serif; *font-size:small; } /* Hack retained to preserve specificity */
select, input, textarea, button { font:99% sans-serif; }
/* Normalize monospace sizing:
en.wikipedia.org/wiki/MediaWiki_talk:Common.css/Archive_11#Teletype_style_fix_for_Chrome */
pre, code, kbd, samp { font-family: monospace, sans-serif; }
/**
* Minimal base styles.
*/
/* Prevent mobile zooming while remain desktop zooming: github.com/shichuan/mobile-html5-boilerplate/issues/closed#issue/14 */
html { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
/* Accessible focus treatment: people.opera.com/patrickl/experiments/keyboard/test */
a:hover, a:active { outline: none; }
ul, ol { margin-left: 2em; }
ol { list-style-type: decimal; }
/* Remove margins for navigation lists */
nav ul, nav li { margin: 0; list-style:none; list-style-image: none; }
small { font-size: 85%; }
strong, th { font-weight: bold; }
td { vertical-align: top; }
/* Set sub, sup without affecting line-height: gist.github.com/413930 */
sub, sup { font-size: 75%; line-height: 0; position: relative; }
sup { top: -0.5em; }
sub { bottom: -0.25em; }
pre {
/* www.pathf.com/blogs/2008/05/formatting-quoted-code-in-blog-posts-css21-white-space-pre-wrap/ */
white-space: pre; white-space: pre-wrap; word-wrap: break-word;
padding: 15px;
}
textarea { overflow: auto; } /* www.sitepoint.com/blogs/2010/08/20/ie-remove-textarea-scrollbars/ */
.iem7 legend { margin-left: -7px; }
/* Align checkboxes, radios, text inputs with their label by: Thierry Koblentz tjkdesign.com/ez-css/css/base.css */
input[type="radio"] { vertical-align: text-bottom; }
input[type="checkbox"] { vertical-align: bottom; }
.iem7 input[type="checkbox"] { vertical-align: baseline; }
/* Hand cursor on clickable input elements */
label, input[type="button"], input[type="submit"], input[type="image"], button { cursor: pointer; }
/* Webkit browsers add a 2px margin outside the chrome of form elements */
button, input, select, textarea { margin: 0; }
/* Colors for form validity */
input:valid, textarea:valid { }
input:invalid, textarea:invalid {
border-radius: 1px; -moz-box-shadow: 0px 0px 5px red; -webkit-box-shadow: 0px 0px 5px red; box-shadow: 0px 0px 5px red;
}
.no-boxshadow input:invalid, .no-boxshadow textarea:invalid { background-color: #f0dddd; }
/* These selection declarations have to be separate
No text-shadow: twitter.com/miketaylr/status/12228805301
Also: hot pink! */
::-moz-selection{ background: #FF5E99; color:#fff; text-shadow: none; }
::selection { background:#FF5E99; color:#fff; text-shadow: none; }
/* j.mp/webkit-tap-highlight-color */
a:link { -webkit-tap-highlight-color: #FF5E99; }
/* Make buttons play nice in IE:
www.viget.com/inspire/styling-the-button-element-in-internet-explorer/ */
button { width: auto; overflow: visible; }
/* Bicubic resizing for non-native sized IMG:
code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ */
.iem7 img { -ms-interpolation-mode: bicubic; }
/**
* You might tweak these..
*/
body, select, input, textarea {
/* #444 looks better than black: twitter.com/H_FJ/statuses/11800719859 */
color: #444;
/* Set your base font here, to apply evenly */
/* font-family: Georgia, serif; */
}
/* Headers (h1, h2, etc) have no default font-size or margin; define those yourself */
h1, h2, h3, h4, h5, h6 { font-weight: bold; }
a, a:active, a:visited { color: #607890; }
a:hover { color: #036; }
/*
* Helper classes
*/
/* prevent callout */
.nocallout {-webkit-touch-callout: none;}
/* Text overflow with ellipsis */
.ellipsis {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
/* A hack for HTML5 contenteditable attribute on mobile */
textarea.contenteditable {-webkit-appearance: none;}
/* A workaround for S60 3.x and 5.0 devices which do not animated gif images if they have been set as display: none */
.gifhidden {position: absolute; left: -100%;}
/* For image replacement */
.ir { display: block; text-indent: -999em; overflow: hidden; background-repeat: no-repeat; text-align: left; direction: ltr; }
/* Hide for both screenreaders and browsers:
css-discuss.incutio.com/wiki/Screenreader_Visibility */
.hidden { display: none; visibility: hidden; }
/* Hide only visually, but have it available for screenreaders: by Jon Neal.
www.webaim.org/techniques/css/invisiblecontent/ & j.mp/visuallyhidden */
.visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; }
/* Extends the .visuallyhidden class to allow the element to be focusable when navigated to via the keyboard: drupal.org/node/897638 */
.visuallyhidden.focusable:active,
.visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; }
/* Hide visually and from screenreaders, but maintain layout */
.invisible { visibility: hidden; }
/* The Magnificent Clearfix: Updated to prevent margin-collapsing on child elements.
j.mp/bestclearfix */
.clearfix:before, .clearfix:after { content: "\0020"; display: block; height: 0; overflow: hidden; }
.clearfix:after { clear: both; }
/* Fix clearfix: blueprintcss.lighthouseapp.com/projects/15318/tickets/5-extra-margin-padding-bottom-of-page */
.clearfix { zoom: 1; }
/* Primary Styles for mobile
Author:
*/
/*
* Media queries for responsive design https://github.com/shichuan/mobile-html5-boilerplate/wiki/The-Style
*/
/* Styles for desktop and large screen ----------- */
/*styles for 800px and up!*/
@media only screen and (min-width: 800px) {
/* Styles */
}/*/mediaquery*/
/* iPhone 4, Opera Mobile 11 and other high pixel ratio devices ----------- */
@media
only screen and (-webkit-min-device-pixel-ratio: 1.5),
only screen and (-o-min-device-pixel-ratio: 3/2),
only screen and (min-device-pixel-ratio: 1.5) {
/* Styles */
}

View file

@ -0,0 +1,17 @@
CACHE MANIFEST
# version 1
img/l/apple-touch-icon.png
img/l/apple-touch-icon-precomposed.png
img/l/splash.png
img/m/apple-touch-icon.png
img/h/apple-touch-icon.png
img/h/splash.png
css/style.css
js/libs/jquery-1.5.1.min.js
js/libs/modernizr-custom.js
js/libs/respond.min.js
NETWORK:
#http://example.com/api/
FALLBACK:

View file

@ -0,0 +1,43 @@
/* the humans responsible & colophon */
/* humanstxt.org */
/* TEAM */
<your title>: <your name>
Site:
Twitter:
Location:
/* THANKS */
Names (& URL):
/* SITE */
Standards: HTML5, CSS3
Components: Modernizr, jQuery
Software:
-o/-
+oo//-
:ooo+//:
-ooooo///-
/oooooo//:
:ooooooo+//-
-+oooooooo///-
-://////////////+oooooooooo++////////////::
:+ooooooooooooooooooooooooooooooooooooo+:::-
-/+ooooooooooooooooooooooooooooooo+/::////:-
-:+oooooooooooooooooooooooooooo/::///////:-
--/+ooooooooooooooooooooo+::://////:-
-:+ooooooooooooooooo+:://////:--
/ooooooooooooooooo+//////:-
-ooooooooooooooooooo////-
/ooooooooo+oooooooooo//:
:ooooooo+/::/+oooooooo+//-
-oooooo/::///////+oooooo///-
/ooo+::://////:---:/+oooo//:
-o+/::///////:- -:/+o+//-
:-:///////:- -:/://
-////:- --//:
-- -:

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -0,0 +1,95 @@
<!doctype html>
<!-- Conditional comment for mobile ie7 http://blogs.msdn.com/b/iemobile/ -->
<!-- Appcache Facts http://appcachefacts.info/ -->
<!--[if IEMobile 7 ]> <html class="no-js iem7" manifest="default.appcache?v=1"> <![endif]-->
<!--[if (gt IEMobile 7)|!(IEMobile)]><!--> <html class="no-js" manifest="default.appcache?v=1"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="author" content="">
<!-- Mobile viewport optimization http://goo.gl/b9SaQ -->
<meta name="HandheldFriendly" content="True">
<meta name="MobileOptimized" content="320"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Home screen icon Mathias Bynens http://goo.gl/6nVq0 -->
<!-- For iPhone 4 with high-resolution Retina display: -->
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="img/h/apple-touch-icon.png">
<!-- For first-generation iPad: -->
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="img/m/apple-touch-icon.png">
<!-- For non-Retina iPhone, iPod Touch, and Android 2.1+ devices: -->
<link rel="apple-touch-icon-precomposed" href="img/l/apple-touch-icon-precomposed.png">
<!-- For nokia devices: -->
<link rel="shortcut icon" href="img/l/apple-touch-icon.png">
<!--iOS web app, deletable if not needed -->
<!--<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-startup-image" href="img/l/splash.png">-->
<!-- Mobile IE allows us to activate ClearType technology for smoothing fonts for easy reading -->
<meta http-equiv="cleartype" content="on">
<!-- more tags for your 'head' to consider https://gist.github.com/849231 -->
<!-- Main Stylesheet -->
<link rel="stylesheet" href="css/style.css?v=1">
<!-- All JavaScript at the bottom, except for Modernizr which enables HTML5 elements & feature detects -->
<script src="js/libs/modernizr-custom.js"></script>
</head>
<body>
<div id="container">
<header>
</header>
<div id="main" role="main">
</div>
<footer>
</footer>
</div> <!--! end of #container -->
<!-- JavaScript at the bottom for fast page loading -->
<!-- Grab Google CDN's jQuery, with a protocol relative URL; fall back to local if necessary -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.js"></script>
<script>window.jQuery || document.write("<script src='js/libs/jquery-1.5.1.min.js'>\x3C/script>")</script>
<!-- scripts concatenated and minified via ant build script -->
<script src="js/mylibs/helper.js"></script>
<!-- end concatenated and minified scripts-->
<script>
// iPhone Scale Bug Fix, read this when using http://www.blog.highub.com/mobile-2/a-fix-for-iphone-viewport-scale-bug/
MBP.scaleFix();
// Media Queries Polyfill https://github.com/shichuan/mobile-html5-boilerplate/wiki/Media-Queries-Polyfill
yepnope({
test : Modernizr.mq('(min-width)'),
nope : ['js/libs/respond.min.js']
});
</script>
<!-- Debugger - remove for production -->
<!-- <script src="https://getfirebug.com/firebug-lite.js"></script> -->
<!-- mathiasbynens.be/notes/async-analytics-snippet Change UA-XXXXX-X to be your site's ID -->
<script>
var _gaq=[["_setAccount","UA-XXXXX-X"],["_trackPageview"]];
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.async=1;
g.src=("https:"==location.protocol?"//ssl":"//www")+".google-analytics.com/ga.js";
s.parentNode.insertBefore(g,s)}(document,"script"));
</script>
</body>
</html>

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,7 @@
/*
* respond.js - A small and fast polyfill for min/max-width CSS3 Media Queries
* Copyright 2011, Scott Jehl, scottjehl.com
* Dual licensed under the MIT or GPL Version 2 licenses.
* Usage: Check out the readme file or github.com/scottjehl/respond
*/
(function(d,g){d.respond={};respond.update=function(){};respond.mediaQueriesSupported=g;if(g){return}var s=d.document,q=s.documentElement,h=[],j=[],o=[],n={},f=30,e=s.getElementsByTagName("head")[0]||q,b=e.getElementsByTagName("link"),a=function(){var y=s.styleSheets,u=y.length;for(var x=0;x<u;x++){var w=y[x],v=w.href;if(!!v&&!n[v]){if(!/^([a-zA-Z]+?:(\/\/)?(www\.)?)/.test(v)||v.replace(RegExp.$1,"").split("/")[0]===d.location.host){var t=v;m(v,function(z){l(z,t);n[t]=true})}else{n[v]=true}}}},l=function(B,t){var z=B.match(/@media ([^\{]+)\{([\S\s]+?)(?=\}\/\*\/mediaquery\*\/)/gmi),C=z&&z.length||0,t=t.substring(0,t.lastIndexOf("/"));if(t.length){t+="/"}for(var w=0;w<C;w++){var x=z[w].match(/@media ([^\{]+)\{([\S\s]+?)$/)&&RegExp.$1,u=x.split(","),A=u.length;j.push(RegExp.$2&&RegExp.$2.replace(/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,"$1"+t+"$2$3"));for(var v=0;v<A;v++){var y=u[v];h.push({media:y.match(/(only\s+)?([a-zA-Z]+)(\sand)?/)&&RegExp.$2,rules:j.length-1,minw:y.match(/\(min\-width:\s?(\s?[0-9]+)px\s?\)/)&&parseFloat(RegExp.$1),maxw:y.match(/\(max\-width:\s?(\s?[0-9]+)px\s?\)/)&&parseFloat(RegExp.$1)})}}i()},k,p,i=function(C){var t="clientWidth",v=q[t],B=s.compatMode==="CSS1Compat"&&v||s.body[t]||v,x={},A=s.createDocumentFragment(),z=b[b.length-1],u=(new Date()).getTime();if(C&&k&&u-k<f){clearTimeout(p);p=setTimeout(i,f);return}else{k=u}for(var w in h){var D=h[w];if(!D.minw&&!D.maxw||(!D.minw||D.minw&&B>=D.minw)&&(!D.maxw||D.maxw&&B<=D.maxw)){if(!x[D.media]){x[D.media]=[]}x[D.media].push(j[D.rules])}}for(var w in o){if(o[w]&&o[w].parentNode===e){e.removeChild(o[w])}}for(var w in x){var E=s.createElement("style"),y=x[w].join("\n");E.type="text/css";E.media=w;if(E.styleSheet){E.styleSheet.cssText=y}else{E.appendChild(s.createTextNode(y))}A.appendChild(E);o.push(E)}e.insertBefore(A,z.nextSibling)},m=function(t,v){var u=c();if(!u){return}u.open("GET",t,true);u.onreadystatechange=function(){if(u.readyState!=4||u.status!=200&&u.status!=304){return}v(u.responseText)};if(u.readyState==4){return}u.send()},c=(function(){var t=false,u=[function(){return new ActiveXObject("Microsoft.XMLHTTP")},function(){return new ActiveXObject("Msxml3.XMLHTTP")},function(){return new ActiveXObject("Msxml2.XMLHTTP")},function(){return new XMLHttpRequest()}],w=u.length;while(w--){try{t=u[w]()}catch(v){continue}break}return function(){return t}})();a();respond.update=a;function r(){i(true)}if(d.addEventListener){d.addEventListener("resize",r,false)}else{if(d.attachEvent){d.attachEvent("onresize",r)}}})(this,(function(f){var g=(function(k){var i=3,l=document.createElement("div"),j=l.getElementsByTagName("i");while(l.innerHTML="<!--[if gt IE "+(++i)+"]><i></i><![endif]-->",j[0]){}return i>4?i:k}());if(f.matchMedia||g&&g>=9){return true}if(g&&g<=8){return false}var e=f.document,a=e.documentElement,b=e.createElement("body"),h=e.createElement("div"),d=e.createElement("style"),c="@media only all { #qtest { position: absolute; } }";h.setAttribute("id","qtest");d.type="text/css";b.appendChild(h);if(d.styleSheet){d.styleSheet.cssText=c}else{d.appendChild(e.createTextNode(c))}a.insertBefore(b,a.firstChild);a.insertBefore(d,b);support=(f.getComputedStyle?f.getComputedStyle(h,null):h.currentStyle)["position"]=="absolute";a.removeChild(b);a.removeChild(d);return support})(this));

View file

@ -0,0 +1,147 @@
/*
* MBP - Mobile boilerplate helper functions
*/
(function(document){
window.MBP = window.MBP || {};
// Fix for iPhone viewport scale bug
// http://www.blog.highub.com/mobile-2/a-fix-for-iphone-viewport-scale-bug/
MBP.viewportmeta = document.querySelector && document.querySelector('meta[name="viewport"]');
MBP.ua = navigator.userAgent;
MBP.scaleFix = function () {
if (MBP.viewportmeta && /iPhone|iPad/.test(MBP.ua) && !/Opera Mini/.test(MBP.ua)) {
MBP.viewportmeta.content = "width=device-width, minimum-scale=1.0, maximum-scale=1.0";
document.addEventListener("gesturestart", MBP.gestureStart, false);
}
};
MBP.gestureStart = function () {
MBP.viewportmeta.content = "width=device-width, minimum-scale=0.25, maximum-scale=1.6";
};
// Hide URL Bar for iOS
// http://remysharp.com/2010/08/05/doing-it-right-skipping-the-iphone-url-bar/
MBP.hideUrlBar = function () {
/iPhone/.test(MBP.ua) && !pageYOffset && !location.hash && setTimeout(function () {
window.scrollTo(0, 1);
}, 1000);
};
// Fast Buttons - read wiki below before using
// https://github.com/shichuan/mobile-html5-boilerplate/wiki/JavaScript-Helper
MBP.fastButton = function (element, handler) {
this.element = element;
this.handler = handler;
if (element.addEventListener) {
element.addEventListener('touchstart', this, false);
element.addEventListener('click', this, false);
}
};
MBP.fastButton.prototype.handleEvent = function(event) {
switch (event.type) {
case 'touchstart': this.onTouchStart(event); break;
case 'touchmove': this.onTouchMove(event); break;
case 'touchend': this.onClick(event); break;
case 'click': this.onClick(event); break;
}
};
MBP.fastButton.prototype.onTouchStart = function(event) {
event.stopPropagation();
this.element.addEventListener('touchend', this, false);
document.body.addEventListener('touchmove', this, false);
this.startX = event.touches[0].clientX;
this.startY = event.touches[0].clientY;
this.element.style.backgroundColor = "rgba(0,0,0,.7)";
};
MBP.fastButton.prototype.onTouchMove = function(event) {
if(Math.abs(event.touches[0].clientX - this.startX) > 10 || Math.abs(event.touches[0].clientY - this.startY) > 10) {
this.reset();
}
};
MBP.fastButton.prototype.onClick = function(event) {
event.stopPropagation();
this.reset();
this.handler(event);
if(event.type == 'touchend') {
MBP.preventGhostClick(this.startX, this.startY);
}
this.element.style.backgroundColor = "";
};
MBP.fastButton.prototype.reset = function() {
this.element.removeEventListener('touchend', this, false);
document.body.removeEventListener('touchmove', this, false);
this.element.style.backgroundColor = "";
};
MBP.preventGhostClick = function (x, y) {
MBP.coords.push(x, y);
window.setTimeout(function (){
MBP.coords.splice(0, 2);
}, 2500);
};
MBP.ghostClickHandler = function (event) {
for(var i = 0, len = MBP.coords.length; i < len; i += 2) {
var x = MBP.coords[i];
var y = MBP.coords[i + 1];
if(Math.abs(event.clientX - x) < 25 && Math.abs(event.clientY - y) < 25) {
event.stopPropagation();
event.preventDefault();
}
}
};
if (document.addEventListener) {
document.addEventListener('click', MBP.ghostClickHandler, true);
}
MBP.coords = [];
// iOS Startup Image
// https://github.com/shichuan/mobile-html5-boilerplate/issues#issue/2
MBP.splash = function () {
var filename = navigator.platform === 'iPad' ? 'h/' : 'l/';
document.write('<link rel="apple-touch-startup-image" href="/img/' + filename + 'splash.png" />' );
};
// Autogrow
// http://googlecode.blogspot.com/2009/07/gmail-for-mobile-html5-series.html
MBP.autogrow = function (element, lh) {
function handler(e){
var newHeight = this.scrollHeight,
currentHeight = this.clientHeight;
if (newHeight > currentHeight) {
this.style.height = newHeight + 3 * textLineHeight + "px";
}
}
var setLineHeight = (lh) ? lh : 12,
textLineHeight = element.currentStyle ? element.currentStyle.lineHeight :
getComputedStyle(element, null).lineHeight;
textLineHeight = (textLineHeight.indexOf("px") == -1) ? setLineHeight :
parseInt(textLineHeight, 10);
element.style.overflow = "hidden";
element.addEventListener ? element.addEventListener('keyup', handler, false) :
element.attachEvent('onkeyup', handler);
};
})(document);

View file

@ -0,0 +1,5 @@
# www.robotstxt.org/
# www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449
User-agent: *

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0">
<!-- http://www.google.com/support/webmasters/bin/answer.py?answer=34648 -->
<url>
<!--<loc>http://mobile.example.com/index.html</loc>-->
<mobile:mobile/>
</url>
</urlset>

View file

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>QUnit Tests</title>
<link rel="stylesheet" href="qunit/qunit.css" media="screen">
<!-- reference your own javascript files here -->
<script src="../js/modernizr-1.5.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script src="../js/plugins.js"></script>
<script src="../js/script.js"></script>
<!-- test runner files -->
<script src="qunit/qunit.js"></script>
<script src="tests.js"></script>
</head>
<body class="flora">
<h1 id="qunit-header">QUnit Test Suite</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<div id="qunit-fixture">test markup</div>
</body>
</html>

View file

@ -0,0 +1,148 @@
/** Font Family and Sizes */
#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
}
#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
#qunit-tests { font-size: smaller; }
/** Resets */
#qunit-tests, #qunit-tests li ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult {
margin: 0;
padding: 0;
}
/** Header */
#qunit-header {
padding: 0.5em 0 0.5em 1em;
color: #fff;
text-shadow: rgba(0, 0, 0, 0.5) 4px 4px 1px;
background-color: #0d3349;
border-radius: 15px 15px 0 0;
-moz-border-radius: 15px 15px 0 0;
-webkit-border-top-right-radius: 15px;
-webkit-border-top-left-radius: 15px;
}
#qunit-banner {
height: 5px;
}
#qunit-testrunner-toolbar {
padding: 0em 0 0.5em 2em;
}
#qunit-userAgent {
padding: 0.5em 0 0.5em 2.5em;
background-color: #2b81af;
color: #fff;
text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
}
/** Tests: Pass/Fail */
#qunit-tests {
list-style-position: inside;
}
#qunit-tests li {
padding: 0.4em 0.5em 0.4em 2.5em;
border-bottom: 1px solid #fff;
list-style-position: inside;
}
#qunit-tests li strong {
cursor: pointer;
}
#qunit-tests li ol {
margin-top: 0.5em;
padding: 0.5em;
background-color: #fff;
border-radius: 15px;
-moz-border-radius: 15px;
-webkit-border-radius: 15px;
box-shadow: inset 0px 2px 13px #999;
-moz-box-shadow: inset 0px 2px 13px #999;
-webkit-box-shadow: inset 0px 2px 13px #999;
}
#qunit-tests li li {
margin: 0.5em;
padding: 0.4em 0.5em 0.4em 0.5em;
background-color: #fff;
border-bottom: none;
list-style-position: inside;
}
/*** Passing Styles */
#qunit-tests li li.pass {
color: #5E740B;
background-color: #fff;
border-left: 26px solid #C6E746;
}
#qunit-tests li.pass { color: #528CE0; background-color: #D2E0E6; }
#qunit-tests li.pass span.test-name { color: #366097; }
#qunit-tests li li.pass span.test-actual,
#qunit-tests li li.pass span.test-expected { color: #999999; }
strong b.pass { color: #5E740B; }
#qunit-banner.qunit-pass { background-color: #C6E746; }
/*** Failing Styles */
#qunit-tests li li.fail {
color: #710909;
background-color: #fff;
border-left: 26px solid #EE5757;
}
#qunit-tests li.fail { color: #000000; background-color: #EE5757; }
#qunit-tests li.fail span.test-name,
#qunit-tests li.fail span.module-name { color: #000000; }
#qunit-tests li li.fail span.test-actual { color: #EE5757; }
#qunit-tests li li.fail span.test-expected { color: green; }
strong b.fail { color: #710909; }
#qunit-banner.qunit-fail,
#qunit-testrunner-toolbar { background-color: #EE5757; }
/** Footer */
#qunit-testresult {
padding: 0.5em 0.5em 0.5em 2.5em;
color: #2b81af;
background-color: #D2E0E6;
border-radius: 0 0 15px 15px;
-moz-border-radius: 0 0 15px 15px;
-webkit-border-bottom-right-radius: 15px;
-webkit-border-bottom-left-radius: 15px;
}
/** Fixture */
#qunit-fixture {
position: absolute;
top: -10000px;
left: -10000px;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
// documentation on writing tests here: http://docs.jquery.com/QUnit
// example tests: https://github.com/jquery/qunit/blob/master/test/same.js
// below are some general tests but feel free to delete them.
module("example tests");
test('HTML5 Boilerplate is sweet',function(){
expect(1);
equals('boilerplate'.replace('boilerplate','sweet'),'sweet','Yes. HTML5 Boilerplate is, in fact, sweet');
})
// these test things from plugins.js
test('Environment is good',function(){
expect(3);
ok( !!window.log, 'log function present');
var history = log.history && log.history.length || 0;
log('logging from the test suite.')
equals( log.history.length - history, 1, 'log history keeps track' )
ok( !!window.Modernizr, 'Modernizr global is present')
})

View file

@ -0,0 +1,31 @@
<%@ Page Language="C#" %>
<script language="C#" runat="server">
// Copyright 2009 Google Inc. All Rights Reserved.
private const string GaAccount = "ACCOUNT ID GOES HERE";
private const string GaPixel = "ga.aspx";
private string GoogleAnalyticsGetImageUrl() {
System.Text.StringBuilder url = new System.Text.StringBuilder();
url.Append(GaPixel + "?");
url.Append("utmac=").Append(GaAccount);
Random RandomClass = new Random();
url.Append("&utmn=").Append(RandomClass.Next(0x7fffffff));
string referer = "-";
if (Request.UrlReferrer != null
&& "" != Request.UrlReferrer.ToString()) {
referer = Request.UrlReferrer.ToString();
}
url.Append("&utmr=").Append(HttpUtility.UrlEncode(referer));
if (HttpContext.Current.Request.Url != null) {
url.Append("&utmp=").Append(HttpUtility.UrlEncode(Request.Url.PathAndQuery));
}
url.Append("&guid=ON");
return url.ToString();
}
</script>

View file

@ -0,0 +1,2 @@
<% string googleAnalyticsImageUrl = GoogleAnalyticsGetImageUrl(); %>
<img src="<%= googleAnalyticsImageUrl %>" />

View file

@ -0,0 +1,195 @@
<% @Page Language="C#" ContentType="image/gif"%><%
@Import Namespace="System.Net" %><%
@Import Namespace="System.Security.Cryptography" %><%
@Import Namespace="System.Text" %><script runat="server" language="c#">
/**
Copyright 2009 Google Inc. All Rights Reserved.
**/
// Tracker version.
private const string Version = "4.4sa";
private const string CookieName = "__utmmobile";
// The path the cookie will be available to, edit this to use a different
// cookie path.
private const string CookiePath = "/";
// Two years in seconds.
private readonly TimeSpan CookieUserPersistence = TimeSpan.FromSeconds(63072000);
// 1x1 transparent GIF
private readonly byte[] GifData = {
0x47, 0x49, 0x46, 0x38, 0x39, 0x61,
0x01, 0x00, 0x01, 0x00, 0x80, 0xff,
0x00, 0xff, 0xff, 0xff, 0x00, 0x00,
0x00, 0x2c, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x01, 0x00, 0x00, 0x02,
0x02, 0x44, 0x01, 0x00, 0x3b
};
private static readonly Regex IpAddressMatcher =
new Regex(@"^([^.]+\.[^.]+\.[^.]+\.).*");
// A string is empty in our terms, if it is null, empty or a dash.
private static bool IsEmpty(string input) {
return input == null || "-" == input || "" == input;
}
// The last octect of the IP address is removed to anonymize the user.
private static string GetIP(string remoteAddress) {
if (IsEmpty(remoteAddress)) {
return "";
}
// Capture the first three octects of the IP address and replace the forth
// with 0, e.g. 124.455.3.123 becomes 124.455.3.0
Match m = IpAddressMatcher.Match(remoteAddress);
if (m.Success) {
return m.Groups[1] + "0";
} else {
return "";
}
}
// Generate a visitor id for this hit.
// If there is a visitor id in the cookie, use that, otherwise
// use the guid if we have one, otherwise use a random number.
private static string GetVisitorId(
string guid, string account, string userAgent, HttpCookie cookie) {
// If there is a value in the cookie, don't change it.
if (cookie != null && cookie.Value != null) {
return cookie.Value;
}
String message;
if (!IsEmpty(guid)) {
// Create the visitor id using the guid.
message = guid + account;
} else {
// otherwise this is a new user, create a new random id.
message = userAgent + GetRandomNumber() + Guid.NewGuid().ToString();
}
MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
byte[] sum = md5.ComputeHash(messageBytes);
string md5String = BitConverter.ToString(sum);
md5String = md5String.Replace("-","");
md5String = md5String.PadLeft(32, '0');
return "0x" + md5String.Substring(0, 16);
}
// Get a random number string.
private static String GetRandomNumber() {
Random RandomClass = new Random();
return RandomClass.Next(0x7fffffff).ToString();
}
// Writes the bytes of a 1x1 transparent gif into the response.
private void WriteGifData() {
Response.AddHeader(
"Cache-Control",
"private, no-cache, no-cache=Set-Cookie, proxy-revalidate");
Response.AddHeader("Pragma", "no-cache");
Response.AddHeader("Expires", "Wed, 17 Sep 1975 21:32:10 GMT");
Response.Buffer = false;
Response.OutputStream.Write(GifData, 0, GifData.Length);
}
// Make a tracking request to Google Analytics from this server.
// Copies the headers from the original request to the new one.
// If request containg utmdebug parameter, exceptions encountered
// communicating with Google Analytics are thown.
private void SendRequestToGoogleAnalytics(string utmUrl) {
try {
WebRequest connection = WebRequest.Create(utmUrl);
((HttpWebRequest)connection).UserAgent = Request.UserAgent;
connection.Headers.Add("Accepts-Language",
Request.Headers.Get("Accepts-Language"));
using (WebResponse resp = connection.GetResponse()) {
// Ignore response
}
} catch (Exception ex) {
if (Request.QueryString.Get("utmdebug") != null) {
throw new Exception("Error contacting Google Analytics", ex);
}
}
}
// Track a page view, updates all the cookies and campaign tracker,
// makes a server side request to Google Analytics and writes the transparent
// gif byte data to the response.
private void TrackPageView() {
TimeSpan timeSpan = (DateTime.Now - new DateTime(1970, 1, 1).ToLocalTime());
string timeStamp = timeSpan.TotalSeconds.ToString();
string domainName = Request.ServerVariables["SERVER_NAME"];
if (IsEmpty(domainName)) {
domainName = "";
}
// Get the referrer from the utmr parameter, this is the referrer to the
// page that contains the tracking pixel, not the referrer for tracking
// pixel.
string documentReferer = Request.QueryString.Get("utmr");
if (IsEmpty(documentReferer)) {
documentReferer = "-";
} else {
documentReferer = HttpUtility.UrlDecode(documentReferer);
}
string documentPath = Request.QueryString.Get("utmp");
if (IsEmpty(documentPath)) {
documentPath = "";
} else {
documentPath = HttpUtility.UrlDecode(documentPath);
}
string account = Request.QueryString.Get("utmac");
string userAgent = Request.UserAgent;
if (IsEmpty(userAgent)) {
userAgent = "";
}
// Try and get visitor cookie from the request.
HttpCookie cookie = Request.Cookies.Get(CookieName);
string visitorId = GetVisitorId(
Request.Headers.Get("X-DCMGUID"), account, userAgent, cookie);
// Always try and add the cookie to the response.
HttpCookie newCookie = new HttpCookie(CookieName);
newCookie.Value = visitorId;
newCookie.Expires = DateTime.Now + CookieUserPersistence;
newCookie.Path = CookiePath;
Response.Cookies.Add(newCookie);
string utmGifLocation = "http://www.google-analytics.com/__utm.gif";
// Construct the gif hit url.
string utmUrl = utmGifLocation + "?" +
"utmwv=" + Version +
"&utmn=" + GetRandomNumber() +
"&utmhn=" + HttpUtility.UrlEncode(domainName) +
"&utmr=" + HttpUtility.UrlEncode(documentReferer) +
"&utmp=" + HttpUtility.UrlEncode(documentPath) +
"&utmac=" + account +
"&utmcc=__utma%3D999.999.999.999.999.1%3B" +
"&utmvid=" + visitorId +
"&utmip=" + GetIP(Request.ServerVariables["REMOTE_ADDR"]);
SendRequestToGoogleAnalytics(utmUrl);
// If the debug parameter is on, add a header to the response that contains
// the url that was used to contact Google Analytics.
if (Request.QueryString.Get("utmdebug") != null) {
Response.AddHeader("X-GA-MOBILE-URL", utmUrl);
}
// Finally write the gif data to the response.
WriteGifData();
}
</script><% TrackPageView(); %>

View file

@ -0,0 +1,44 @@
<%@ Page Language="C#" %>
<script language="C#" runat="server">
private const string GaAccount = "MO-3845491-5";
private const string GaPixel = "ga.aspx";
private string GoogleAnalyticsGetImageUrl() {
System.Text.StringBuilder url = new System.Text.StringBuilder();
url.Append(GaPixel + "?");
url.Append("utmac=").Append(GaAccount);
Random RandomClass = new Random();
url.Append("&utmn=").Append(RandomClass.Next(0x7fffffff));
string referer = "-";
if (Request.UrlReferrer != null
&& "" != Request.UrlReferrer.ToString()) {
referer = Request.UrlReferrer.ToString();
}
url.Append("&utmr=").Append(HttpUtility.UrlEncode(referer));
if (HttpContext.Current.Request.Url != null) {
url.Append("&utmp=").Append(HttpUtility.UrlEncode(Request.Url.PathAndQuery));
}
url.Append("&guid=ON");
return url.ToString();
}
</script>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Sample Mobile Analytics Page</title>
</head>
<body>
Publisher's content here.
<%
string googleAnalyticsImageUrl = GoogleAnalyticsGetImageUrl();
%>
<img src="<%= googleAnalyticsImageUrl %>" />
Testing: <%= googleAnalyticsImageUrl %>
</body>
</html>

View file

@ -0,0 +1,225 @@
<%@ page language="java"
contentType="image/gif"
import="java.io.ByteArrayOutputStream,
java.io.IOException,
java.io.OutputStream,
java.io.UnsupportedEncodingException,
java.math.BigInteger,
java.net.HttpURLConnection,
java.net.URLConnection,
java.net.URL,
java.net.URLEncoder,
java.net.URLDecoder,
java.security.MessageDigest,
java.security.NoSuchAlgorithmException,
javax.servlet.http.Cookie,
java.util.regex.Pattern,
java.util.regex.Matcher,
java.util.UUID" %><%!
/**
Copyright 2009 Google Inc. All Rights Reserved.
**/
// Tracker version.
private static final String version = "4.4sj";
private static final String COOKIE_NAME = "__utmmobile";
// The path the cookie will be available to, edit this to use a different
// cookie path.
private static final String COOKIE_PATH = "/";
// Two years in seconds.
private static final int COOKIE_USER_PERSISTENCE = 63072000;
// 1x1 transparent GIF
private static final byte[] GIF_DATA = new byte[] {
(byte)0x47, (byte)0x49, (byte)0x46, (byte)0x38, (byte)0x39, (byte)0x61,
(byte)0x01, (byte)0x00, (byte)0x01, (byte)0x00, (byte)0x80, (byte)0xff,
(byte)0x00, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0x00, (byte)0x00,
(byte)0x00, (byte)0x2c, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00,
(byte)0x01, (byte)0x00, (byte)0x01, (byte)0x00, (byte)0x00, (byte)0x02,
(byte)0x02, (byte)0x44, (byte)0x01, (byte)0x00, (byte)0x3b
};
// A string is empty in our terms, if it is null, empty or a dash.
private static boolean isEmpty(String in) {
return in == null || "-".equals(in) || "".equals(in);
}
// The last octect of the IP address is removed to anonymize the user.
private static String getIP(String remoteAddress) {
if (isEmpty(remoteAddress)) {
return "";
}
// Capture the first three octects of the IP address and replace the forth
// with 0, e.g. 124.455.3.123 becomes 124.455.3.0
String regex = "^([^.]+\\.[^.]+\\.[^.]+\\.).*";
Pattern getFirstBitOfIPAddress = Pattern.compile(regex);
Matcher m = getFirstBitOfIPAddress.matcher(remoteAddress);
if (m.matches()) {
return m.group(1) + "0";
} else {
return "";
}
}
// Generate a visitor id for this hit.
// If there is a visitor id in the cookie, use that, otherwise
// use the guid if we have one, otherwise use a random number.
private static String getVisitorId(
String guid, String account, String userAgent, Cookie cookie)
throws NoSuchAlgorithmException, UnsupportedEncodingException {
// If there is a value in the cookie, don't change it.
if (cookie != null && cookie.getValue() != null) {
return cookie.getValue();
}
String message;
if (!isEmpty(guid)) {
// Create the visitor id using the guid.
message = guid + account;
} else {
// otherwise this is a new user, create a new random id.
message = userAgent + getRandomNumber() + UUID.randomUUID().toString();
}
MessageDigest m = MessageDigest.getInstance("MD5");
m.update(message.getBytes("UTF-8"), 0, message.length());
byte[] sum = m.digest();
BigInteger messageAsNumber = new BigInteger(1, sum);
String md5String = messageAsNumber.toString(16);
// Pad to make sure id is 32 characters long.
while (md5String.length() < 32) {
md5String = "0" + md5String;
}
return "0x" + md5String.substring(0, 16);
}
// Get a random number string.
private static String getRandomNumber() {
return Integer.toString((int) (Math.random() * 0x7fffffff));
}
// Writes the bytes of a 1x1 transparent gif into the response.
private void writeGifData(HttpServletResponse response) throws IOException {
response.addHeader(
"Cache-Control",
"private, no-cache, no-cache=Set-Cookie, proxy-revalidate");
response.addHeader("Pragma", "no-cache");
response.addHeader("Expires", "Wed, 17 Sep 1975 21:32:10 GMT");
ServletOutputStream output = response.getOutputStream();
output.write(GIF_DATA);
output.flush();
}
// Make a tracking request to Google Analytics from this server.
// Copies the headers from the original request to the new one.
// If request containg utmdebug parameter, exceptions encountered
// communicating with Google Analytics are thown.
private void sendRequestToGoogleAnalytics(
String utmUrl, HttpServletRequest request) throws Exception {
try {
URL url = new URL(utmUrl);
URLConnection connection = url.openConnection();
connection.setUseCaches(false);
connection.addRequestProperty("User-Agent",
request.getHeader("User-Agent"));
connection.addRequestProperty("Accepts-Language",
request.getHeader("Accepts-Language"));
connection.getContent();
} catch (Exception e) {
if (request.getParameter("utmdebug") != null) {
throw new Exception(e);
}
}
}
// Track a page view, updates all the cookies and campaign tracker,
// makes a server side request to Google Analytics and writes the transparent
// gif byte data to the response.
private void trackPageView(
HttpServletRequest request, HttpServletResponse response)
throws Exception {
String timeStamp = Long.toString(System.currentTimeMillis() / 1000);
String domainName = request.getServerName();
if (isEmpty(domainName)) {
domainName = "";
}
// Get the referrer from the utmr parameter, this is the referrer to the
// page that contains the tracking pixel, not the referrer for tracking
// pixel.
String documentReferer = request.getParameter("utmr");
if (isEmpty(documentReferer)) {
documentReferer = "-";
} else {
documentReferer = URLDecoder.decode(documentReferer, "UTF-8");
}
String documentPath = request.getParameter("utmp");
if (isEmpty(documentPath)) {
documentPath = "";
} else {
documentPath = URLDecoder.decode(documentPath, "UTF-8");
}
String account = request.getParameter("utmac");
String userAgent = request.getHeader("User-Agent");
if (isEmpty(userAgent)) {
userAgent = "";
}
// Try and get visitor cookie from the request.
Cookie[] cookies = request.getCookies();
Cookie cookie = null;
if (cookies != null) {
for(int i = 0; i < cookies.length; i++) {
if (cookies[i].getName().equals(COOKIE_NAME)) {
cookie = cookies[i];
}
}
}
String visitorId = getVisitorId(
request.getHeader("X-DCMGUID"), account, userAgent, cookie);
// Always try and add the cookie to the response.
Cookie newCookie = new Cookie(COOKIE_NAME, visitorId);
newCookie.setMaxAge(COOKIE_USER_PERSISTENCE);
newCookie.setPath(COOKIE_PATH);
response.addCookie(newCookie);
String utmGifLocation = "http://www.google-analytics.com/__utm.gif";
// Construct the gif hit url.
String utmUrl = utmGifLocation + "?" +
"utmwv=" + version +
"&utmn=" + getRandomNumber() +
"&utmhn=" + URLEncoder.encode(domainName, "UTF-8") +
"&utmr=" + URLEncoder.encode(documentReferer, "UTF-8") +
"&utmp=" + URLEncoder.encode(documentPath, "UTF-8") +
"&utmac=" + account +
"&utmcc=__utma%3D999.999.999.999.999.1%3B" +
"&utmvid=" + visitorId +
"&utmip=" + getIP(request.getRemoteAddr());
sendRequestToGoogleAnalytics(utmUrl, request);
// If the debug parameter is on, add a header to the response that contains
// the url that was used to contact Google Analytics.
if (request.getParameter("utmdebug") != null) {
response.setHeader("X-GA-MOBILE-URL", utmUrl);
}
// Finally write the gif data to the response.
writeGifData(response);
}
%><%
// Let exceptions bubble up to container, for better debugging.
trackPageView(request, response);
%>

View file

@ -0,0 +1,35 @@
<%@ page import="java.io.UnsupportedEncodingException,
java.net.URLEncoder" %>
<%!
// Copyright 2009 Google Inc. All Rights Reserved.
private static final String GA_ACCOUNT = "ACCOUNT ID GOES HERE";
private static final String GA_PIXEL = "ga.jsp";
private String googleAnalyticsGetImageUrl(
HttpServletRequest request) throws UnsupportedEncodingException {
StringBuilder url = new StringBuilder();
url.append(GA_PIXEL + "?");
url.append("utmac=").append(GA_ACCOUNT);
url.append("&utmn=").append(Integer.toString((int) (Math.random() * 0x7fffffff)));
String referer = request.getHeader("referer");
String query = request.getQueryString();
String path = request.getRequestURI();
if (referer == null || "".equals(referer)) {
referer = "-";
}
url.append("&utmr=").append(URLEncoder.encode(referer, "UTF-8"));
if (path != null) {
if (query != null) {
path += "?" + query;
}
url.append("&utmp=").append(URLEncoder.encode(path, "UTF-8"));
}
url.append("&guid=ON");
return url.toString();
}
%>

View file

@ -0,0 +1,2 @@
<% String googleAnalyticsImageUrl = googleAnalyticsGetImageUrl(request); %>
<img src="<%= googleAnalyticsImageUrl %>" />

View file

@ -0,0 +1,51 @@
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ page import="java.io.UnsupportedEncodingException,
java.net.URLEncoder" %>
<%!
private static final String GA_ACCOUNT = "MO-3845491-5";
private static final String GA_PIXEL = "ga.jsp";
private String googleAnalyticsGetImageUrl(
HttpServletRequest request) throws UnsupportedEncodingException {
StringBuilder url = new StringBuilder();
url.append(GA_PIXEL + "?");
url.append("utmac=").append(GA_ACCOUNT);
url.append("&utmn=").append(Integer.toString((int) (Math.random() * 0x7fffffff)));
String referer = request.getHeader("referer");
String query = request.getQueryString();
String path = request.getRequestURI();
if (referer == null || "".equals(referer)) {
referer = "-";
}
url.append("&utmr=").append(URLEncoder.encode(referer, "UTF-8"));
if (path != null) {
if (query != null) {
path += "?" + query;
}
url.append("&utmp=").append(URLEncoder.encode(path, "UTF-8"));
}
url.append("&guid=ON");
return url.toString();
}
%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Sample Mobile Analytics Page</title>
</head>
<body>
Publishers content here.
<%
String googleAnalyticsImageUrl = googleAnalyticsGetImageUrl(request);
%>
<img src="<%= googleAnalyticsImageUrl %>" />
Testing: <%= googleAnalyticsImageUrl %>
</body>
</html>

View file

@ -0,0 +1,176 @@
<?php
/**
Copyright 2009 Google Inc. All Rights Reserved.
**/
// Tracker version.
define("VERSION", "4.4sh");
define("COOKIE_NAME", "__utmmobile");
// The path the cookie will be available to, edit this to use a different
// cookie path.
define("COOKIE_PATH", "/");
// Two years in seconds.
define("COOKIE_USER_PERSISTENCE", 63072000);
// 1x1 transparent GIF
$GIF_DATA = array(
chr(0x47), chr(0x49), chr(0x46), chr(0x38), chr(0x39), chr(0x61),
chr(0x01), chr(0x00), chr(0x01), chr(0x00), chr(0x80), chr(0xff),
chr(0x00), chr(0xff), chr(0xff), chr(0xff), chr(0x00), chr(0x00),
chr(0x00), chr(0x2c), chr(0x00), chr(0x00), chr(0x00), chr(0x00),
chr(0x01), chr(0x00), chr(0x01), chr(0x00), chr(0x00), chr(0x02),
chr(0x02), chr(0x44), chr(0x01), chr(0x00), chr(0x3b)
);
// The last octect of the IP address is removed to anonymize the user.
function getIP($remoteAddress) {
if (empty($remoteAddress)) {
return "";
}
// Capture the first three octects of the IP address and replace the forth
// with 0, e.g. 124.455.3.123 becomes 124.455.3.0
$regex = "/^([^.]+\.[^.]+\.[^.]+\.).*/";
if (preg_match($regex, $remoteAddress, $matches)) {
return $matches[1] . "0";
} else {
return "";
}
}
// Generate a visitor id for this hit.
// If there is a visitor id in the cookie, use that, otherwise
// use the guid if we have one, otherwise use a random number.
function getVisitorId($guid, $account, $userAgent, $cookie) {
// If there is a value in the cookie, don't change it.
if (!empty($cookie)) {
return $cookie;
}
$message = "";
if (!empty($guid)) {
// Create the visitor id using the guid.
$message = $guid . $account;
} else {
// otherwise this is a new user, create a new random id.
$message = $userAgent . uniqid(getRandomNumber(), true);
}
$md5String = md5($message);
return "0x" . substr($md5String, 0, 16);
}
// Get a random number string.
function getRandomNumber() {
return rand(0, 0x7fffffff);
}
// Writes the bytes of a 1x1 transparent gif into the response.
function writeGifData() {
global $GIF_DATA;
header("Content-Type: image/gif");
header("Cache-Control: " .
"private, no-cache, no-cache=Set-Cookie, proxy-revalidate");
header("Pragma: no-cache");
header("Expires: Wed, 17 Sep 1975 21:32:10 GMT");
echo join($GIF_DATA);
}
// Make a tracking request to Google Analytics from this server.
// Copies the headers from the original request to the new one.
// If request containg utmdebug parameter, exceptions encountered
// communicating with Google Analytics are thown.
function sendRequestToGoogleAnalytics($utmUrl) {
$options = array(
"http" => array(
"method" => "GET",
"user_agent" => $_SERVER["HTTP_USER_AGENT"],
"header" => ("Accepts-Language: " . $_SERVER["HTTP_ACCEPT_LANGUAGE"]))
);
if (!empty($_GET["utmdebug"])) {
$data = file_get_contents(
$utmUrl, false, stream_context_create($options));
} else {
$data = @file_get_contents(
$utmUrl, false, stream_context_create($options));
}
}
// Track a page view, updates all the cookies and campaign tracker,
// makes a server side request to Google Analytics and writes the transparent
// gif byte data to the response.
function trackPageView() {
$timeStamp = time();
$domainName = $_SERVER["SERVER_NAME"];
if (empty($domainName)) {
$domainName = "";
}
// Get the referrer from the utmr parameter, this is the referrer to the
// page that contains the tracking pixel, not the referrer for tracking
// pixel.
$documentReferer = $_GET["utmr"];
if (empty($documentReferer) && $documentReferer !== "0") {
$documentReferer = "-";
} else {
$documentReferer = urldecode($documentReferer);
}
$documentPath = $_GET["utmp"];
if (empty($documentPath)) {
$documentPath = "";
} else {
$documentPath = urldecode($documentPath);
}
$account = $_GET["utmac"];
$userAgent = $_SERVER["HTTP_USER_AGENT"];
if (empty($userAgent)) {
$userAgent = "";
}
// Try and get visitor cookie from the request.
$cookie = $_COOKIE[COOKIE_NAME];
$visitorId = getVisitorId(
$_SERVER["HTTP_X_DCMGUID"], $account, $userAgent, $cookie);
// Always try and add the cookie to the response.
setrawcookie(
COOKIE_NAME,
$visitorId,
$timeStamp + COOKIE_USER_PERSISTENCE,
COOKIE_PATH);
$utmGifLocation = "http://www.google-analytics.com/__utm.gif";
// Construct the gif hit url.
$utmUrl = $utmGifLocation . "?" .
"utmwv=" . VERSION .
"&utmn=" . getRandomNumber() .
"&utmhn=" . urlencode($domainName) .
"&utmr=" . urlencode($documentReferer) .
"&utmp=" . urlencode($documentPath) .
"&utmac=" . $account .
"&utmcc=__utma%3D999.999.999.999.999.1%3B" .
"&utmvid=" . $visitorId .
"&utmip=" . getIP($_SERVER["REMOTE_ADDR"]);
sendRequestToGoogleAnalytics($utmUrl);
// If the debug parameter is on, add a header to the response that contains
// the url that was used to contact Google Analytics.
if (!empty($_GET["utmdebug"])) {
header("X-GA-MOBILE-URL:" . $utmUrl);
}
// Finally write the gif data to the response.
writeGifData();
}
?><?php
trackPageView();
?>

View file

@ -0,0 +1,30 @@
<?php
// Copyright 2009 Google Inc. All Rights Reserved.
$GA_ACCOUNT = "ACCOUNT ID GOES HERE";
$GA_PIXEL = "ga.php";
function googleAnalyticsGetImageUrl() {
global $GA_ACCOUNT, $GA_PIXEL;
$url = "";
$url .= $GA_PIXEL . "?";
$url .= "utmac=" . $GA_ACCOUNT;
$url .= "&utmn=" . rand(0, 0x7fffffff);
$referer = $_SERVER["HTTP_REFERER"];
$query = $_SERVER["QUERY_STRING"];
$path = $_SERVER["REQUEST_URI"];
if (empty($referer)) {
$referer = "-";
}
$url .= "&utmr=" . urlencode($referer);
if (!empty($path)) {
$url .= "&utmp=" . urlencode($path);
}
$url .= "&guid=ON";
return $url;
}
?>

View file

@ -0,0 +1,4 @@
<?php
$googleAnalyticsImageUrl = googleAnalyticsGetImageUrl();
?>
<img src="<?= $googleAnalyticsImageUrl ?>" />

View file

@ -0,0 +1,44 @@
<?php
$GA_ACCOUNT = "MO-3845491-5";
$GA_PIXEL = "ga.php";
function googleAnalyticsGetImageUrl() {
global $GA_ACCOUNT, $GA_PIXEL;
$url = "";
$url .= $GA_PIXEL . "?";
$url .= "utmac=" . $GA_ACCOUNT;
$url .= "&utmn=" . rand(0, 0x7fffffff);
$referer = $_SERVER["HTTP_REFERER"];
$query = $_SERVER["QUERY_STRING"];
$path = $_SERVER["REQUEST_URI"];
if (empty($referer)) {
$referer = "-";
}
$url .= "&utmr=" . urlencode($referer);
if (!empty($path)) {
$url .= "&utmp=" . urlencode($path);
}
$url .= "&guid=ON";
return $url;
}
?>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Sample Mobile Analytics Page</title>
</head>
<body>
Publishers content here.
<?php
$googleAnalyticsImageUrl = googleAnalyticsGetImageUrl();
?>
<img src="<?= $googleAnalyticsImageUrl ?>" />
Testing: <?= $googleAnalyticsImageUrl ?>
</body>
</html>

View file

@ -0,0 +1,195 @@
#!/usr/bin/perl -w
#
# Copyright 2009 Google Inc. All Rights Reserved.
use CGI;
use Digest::MD5 qw(md5_hex);
use LWP::UserAgent;
use URI::Escape;
use strict;
# Tracker version.
use constant VERSION => '4.4sp';
use constant COOKIE_NAME => '__utmmobile';
# The path the cookie will be available to, edit this to use a different
# cookie path.
use constant COOKIE_PATH => '/';
# Two years.
use constant COOKIE_USER_PERSISTENCE => '+2y';
# 1x1 transparent GIF
my @GIF_DATA = (
0x47, 0x49, 0x46, 0x38, 0x39, 0x61,
0x01, 0x00, 0x01, 0x00, 0x80, 0xff,
0x00, 0xff, 0xff, 0xff, 0x00, 0x00,
0x00, 0x2c, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x01, 0x00, 0x00, 0x02,
0x02, 0x44, 0x01, 0x00, 0x3b);
my $query = new CGI;
# The last octect of the IP address is removed to anonymize the user.
sub get_ip {
my ($remote_address) = @_;
if ($remote_address eq "") {
return "";
}
# Capture the first three octects of the IP address and replace the forth
# with 0, e.g. 124.455.3.123 becomes 124.455.3.0
if ($remote_address =~ /^((\d{1,3}\.){3})\d{1,3}$/) {
return $1 . "0";
} else {
return "";
}
}
# Generate a visitor id for this hit.
# If there is a visitor id in the cookie, use that, otherwise
# use the guid if we have one, otherwise use a random number.
sub get_visitor_id {
my ($guid, $account, $user_agent, $cookie) = @_;
# If there is a value in the cookie, don't change it.
if ($cookie ne "") {
return $cookie;
}
my $message = "";
if ($guid ne "") {
# Create the visitor id using the guid.
$message = $guid . $account;
} else {
# otherwise this is a new user, create a new random id.
$message = $user_agent . get_random_number();
}
my $md5_string = md5_hex($message);
return "0x" . substr($md5_string, 0, 16);
}
# Get a random number string.
sub get_random_number {
return int(rand(0x7fffffff));
}
# Writes the bytes of a 1x1 transparent gif into the response.
sub write_gif_data {
my ($cookie, $utm_url) = @_;
my @header_args = (
-type => 'image/gif',
-Cache_Control =>
'private, no-cache, no-cache=Set-Cookie, proxy-revalidate',
-Pragma => 'no-cache',
-cookie => $cookie,
-expires => '-1d');
# If the debug parameter is on, add a header to the response that contains
# the url that was used to contact Google Analytics.
if (defined($query->param('utmdebug'))) {
push(@header_args, -X_GA_MOBILE_URL => $utm_url);
}
print $query->header(@header_args);
print pack("C35", @GIF_DATA);
}
# Make a tracking request to Google Analytics from this server.
# Copies the headers from the original request to the new one.
# If request containg utmdebug parameter, exceptions encountered
# communicating with Google Analytics are thown.
sub send_request_to_google_analytics {
my ($utm_url) = @_;
my $ua = LWP::UserAgent->new;
if (exists($ENV{'HTTP_ACCEPT_LANGUAGE'})) {
$ua->default_header('Accepts-Language' => $ENV{'HTTP_ACCEPT_LANGUAGE'});
}
if (exists($ENV{'HTTP_USER_AGENT'})) {
$ua->agent($ENV{'HTTP_USER_AGENT'});
}
my $ga_output = $ua->get($utm_url);
if (defined($query->param('utmdebug')) && !$ga_output->is_success) {
print $ga_output->status_line;
}
}
# Track a page view, updates all the cookies and campaign tracker,
# makes a server side request to Google Analytics and writes the transparent
# gif byte data to the response.
sub track_page_view {
my $domain_name = "";
if (exists($ENV{'SERVER_NAME'})) {
$domain_name = $ENV{'SERVER_NAME'};
}
# Get the referrer from the utmr parameter, this is the referrer to the
# page that contains the tracking pixel, not the referrer for tracking
# pixel.
my $document_referer = "-";
if (defined($query->param('utmr'))) {
$document_referer = uri_unescape($query->param('utmr'));
}
my $document_path = "";
if (defined($query->param('utmp'))) {
$document_path = uri_unescape($query->param('utmp'));
}
my $account = $query->param('utmac');
my $user_agent = "";
if (exists($ENV{'HTTP_USER_AGENT'})) {
$user_agent = $ENV{'HTTP_USER_AGENT'};
}
# Try and get visitor cookie from the request.
my $cookie = "";
if (defined($query->cookie(COOKIE_NAME))) {
$cookie = $query->cookie(COOKIE_NAME);
}
my $guid = "";
if (exists($ENV{'HTTP_X_DCMGUID'})) {
$guid = $ENV{'HTTP_X_DCMGUID'};
}
my $visitor_id = get_visitor_id($guid, $account, $user_agent, $cookie);
# Always try and add the cookie to the response.
my $new_cookie = $query->cookie(
-name => COOKIE_NAME,
-value => $visitor_id,
-path => COOKIE_PATH,
-expires => COOKIE_USER_PERSISTENCE);
my $utm_gif_location = "http://www.google-analytics.com/__utm.gif";
my $remote_address = "";
if (exists($ENV{'REMOTE_ADDR'})) {
$remote_address = $ENV{'REMOTE_ADDR'};
}
# Construct the gif hit url.
my $utm_url = $utm_gif_location . '?' .
'utmwv=' . VERSION .
'&utmn=' . get_random_number() .
'&utmhn=' . uri_escape($domain_name) .
'&utmr=' . uri_escape($document_referer) .
'&utmp=' . uri_escape($document_path) .
'&utmac=' . $account .
'&utmcc=__utma%3D999.999.999.999.999.1%3B' .
'&utmvid=' . $visitor_id .
'&utmip=' . get_ip($remote_address);
send_request_to_google_analytics($utm_url);
# Finally write the gif data to the response.
write_gif_data($new_cookie, $utm_url);
}
track_page_view();

View file

@ -0,0 +1,27 @@
# Copyright 2009 Google Inc. All Rights Reserved.
use URI::Escape;
use constant GA_ACCOUNT => 'ACCOUNT ID GOES HERE';
use constant GA_PIXEL => 'ga.pl';
sub google_analytics_get_image_url {
my $url = '';
$url .= GA_PIXEL . '?';
$url .= 'utmac=' . GA_ACCOUNT;
$url .= '&utmn=' . int(rand(0x7fffffff));
my $referer = $ENV{'HTTP_REFERER'};
my $query = $ENV{'QUERY_STRING'};
my $path = $ENV{'REQUEST_URI'};
if ($referer eq "") {
$referer = '-';
}
$url .= '&utmr=' . uri_escape($referer);
$url .= '&utmp=' . uri_escape($path);
$url .= '&guid=ON';
$url;
}

View file

@ -0,0 +1 @@
print '<img src="' . google_analytics_get_image_url() . '" />';

View file

@ -0,0 +1,38 @@
#!/usr/bin/perl -w
#
# Copyright 2009 Google Inc. All Rights Reserved.
use strict;
use URI::Escape;
use constant GA_ACCOUNT => 'MO-3845491-5';
use constant GA_PIXEL => 'ga.pl';
sub google_analytics_get_image_url {
my $url = '';
$url .= GA_PIXEL . '?';
$url .= 'utmac=' . GA_ACCOUNT;
$url .= '&utmn=' . int(rand(0x7fffffff));
my $referer = $ENV{'HTTP_REFERER'};
my $query = $ENV{'QUERY_STRING'};
my $path = $ENV{'REQUEST_URI'};
if ($referer eq "") {
$referer = '-';
}
$url .= '&utmr=' . uri_escape($referer);
$url .= '&utmp=' . uri_escape($path);
$url .= '&guid=ON';
$url;
}
print "Content-Type: text/html; charset=ISO-8859-1\n\n";
print '<html><head></head><body>';
print '<img src="' . google_analytics_get_image_url() . '" />';
print google_analytics_get_image_url();
print '</body></html>';
exit (0);

View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

View file

@ -0,0 +1,559 @@
/*
Copyright 2010 Google 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.
*/
/**
* @fileoverview Bookmark bubble library. This is meant to be included in the
* main JavaScript binary of a mobile web application.
*
* Supported browsers: iPhone / iPod / iPad Safari 3.0+
*/
var google = google || {};
google.bookmarkbubble = google.bookmarkbubble || {};
/**
* Binds a context object to the function.
* @param {Function} fn The function to bind to.
* @param {Object} context The "this" object to use when the function is run.
* @return {Function} A partially-applied form of fn.
*/
google.bind = function(fn, context) {
return function() {
return fn.apply(context, arguments);
};
};
/**
* Function used to define an abstract method in a base class. If a subclass
* fails to override the abstract method, then an error will be thrown whenever
* that method is invoked.
*/
google.abstractMethod = function() {
throw Error('Unimplemented abstract method.');
};
/**
* The bubble constructor. Instantiating an object does not cause anything to
* be rendered yet, so if necessary you can set instance properties before
* showing the bubble.
* @constructor
*/
google.bookmarkbubble.Bubble = function() {
/**
* Handler for the scroll event. Keep a reference to it here, so it can be
* unregistered when the bubble is destroyed.
* @type {function()}
* @private
*/
this.boundScrollHandler_ = google.bind(this.setPosition, this);
/**
* The bubble element.
* @type {Element}
* @private
*/
this.element_ = null;
/**
* Whether the bubble has been destroyed.
* @type {boolean}
* @private
*/
this.hasBeenDestroyed_ = false;
};
/**
* Shows the bubble if allowed. It is not allowed if:
* - The browser is not Mobile Safari, or
* - The user has dismissed it too often already, or
* - The hash parameter is present in the location hash, or
* - The application is in fullscreen mode, which means it was already loaded
* from a homescreen bookmark.
* @return {boolean} True if the bubble is being shown, false if it is not
* allowed to show for one of the aforementioned reasons.
*/
google.bookmarkbubble.Bubble.prototype.showIfAllowed = function() {
if (!this.isAllowedToShow_()) {
return false;
}
this.show_();
return true;
};
/**
* Shows the bubble if allowed after loading the icon image. This method creates
* an image element to load the image into the browser's cache before showing
* the bubble to ensure that the image isn't blank. Use this instead of
* showIfAllowed if the image url is http and cacheable.
* This hack is necessary because Mobile Safari does not properly render
* image elements with border-radius CSS.
* @param {function()} opt_callback Closure to be called if and when the bubble
* actually shows.
* @return {boolean} True if the bubble is allowed to show.
*/
google.bookmarkbubble.Bubble.prototype.showIfAllowedWhenLoaded =
function(opt_callback) {
if (!this.isAllowedToShow_()) {
return false;
}
var self = this;
// Attach to self to avoid garbage collection.
var img = self.loadImg_ = document.createElement('img');
img.src = self.getIconUrl_();
img.onload = function() {
if (img.complete) {
delete self.loadImg_;
img.onload = null; // Break the circular reference.
self.show_();
opt_callback && opt_callback();
}
};
img.onload();
return true;
};
/**
* Sets the parameter in the location hash. As it is
* unpredictable what hash scheme is to be used, this method must be
* implemented by the host application.
*
* This gets called automatically when the bubble is shown. The idea is that if
* the user then creates a bookmark, we can later recognize on application
* startup whether it was from a bookmark suggested with this bubble.
*/
google.bookmarkbubble.Bubble.prototype.setHashParameter = google.abstractMethod;
/**
* Whether the parameter is present in the location hash. As it is
* unpredictable what hash scheme is to be used, this method must be
* implemented by the host application.
*
* Call this method during application startup if you want to log whether the
* application was loaded from a bookmark with the bookmark bubble promotion
* parameter in it.
*
* @return {boolean} Whether the bookmark bubble parameter is present in the
* location hash.
*/
google.bookmarkbubble.Bubble.prototype.hasHashParameter = google.abstractMethod;
/**
* The number of times the user must dismiss the bubble before we stop showing
* it. This is a public property and can be changed by the host application if
* necessary.
* @type {number}
*/
google.bookmarkbubble.Bubble.prototype.NUMBER_OF_TIMES_TO_DISMISS = 2;
/**
* Time in milliseconds. If the user does not dismiss the bubble, it will auto
* destruct after this amount of time.
* @type {number}
*/
google.bookmarkbubble.Bubble.prototype.TIME_UNTIL_AUTO_DESTRUCT = 15000;
/**
* The prefix for keys in local storage. This is a public property and can be
* changed by the host application if necessary.
* @type {string}
*/
google.bookmarkbubble.Bubble.prototype.LOCAL_STORAGE_PREFIX = 'BOOKMARK_';
/**
* The key name for the dismissed state.
* @type {string}
* @private
*/
google.bookmarkbubble.Bubble.prototype.DISMISSED_ = 'DISMISSED_COUNT';
/**
* The arrow image in base64 data url format.
* @type {string}
* @private
*/
google.bookmarkbubble.Bubble.prototype.IMAGE_ARROW_DATA_URL_ = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAATCAMAAABSrFY3AAABKVBMVEUAAAD///8AAAAAAAAAAAAAAAAAAADf398AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD09PQAAAAAAAAAAAC9vb0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD19fUAAAAAAAAAAAAAAADq6uoAAAAAAAAAAAC8vLzU1NTT09MAAADg4OAAAADs7OwAAAAAAAAAAAD///+cueenwerA0vC1y+3a5fb5+/3t8vr4+v3w9PuwyOy3zO3h6vfh6vjq8Pqkv+mat+fE1fHB0/Cduuifu+iuxuuivemrxOvC1PDz9vzJ2fKpwuqmwOrb5vapw+q/0vDf6ffK2vLN3PPprJISAAAAQHRSTlMAAAEGExES7FM+JhUoQSxIRwMbNfkJUgXXBE4kDQIMHSA0Tw4xIToeTSc4Chz4OyIjPfI3QD/X5OZR6zzwLSUPrm1y3gAAAQZJREFUeF5lzsVyw0AURNE3IMsgmZmZgszQZoeZOf//EYlG5Yrhbs+im4Dj7slM5wBJ4OJ+undAUr68gK/Hyb6Bcp5yBR/w8jreNeAr5Eg2XE7g6e2/0z6cGw1JQhpmHP3u5aiPPnTTkIK48Hj9Op7bD3btAXTfgUdwYjwSDCVXMbizO0O4uDY/x4kYC5SWFnfC6N1a9RCO7i2XEmQJj2mHK1Hgp9Vq3QBRl9shuBLGhcNtHexcdQCnDUoUGetxDD+H2DQNG2xh6uAWgG2/17o1EmLqYH0Xej0UjHAaFxZIV6rJ/WK1kg7QZH8HU02zmdJinKZJaDV3TVMjM5Q9yiqYpUwiMwa/1apDXTNESjsAAAAASUVORK5CYII=';
/**
* The close image in base64 data url format.
* @type {string}
* @private
*/
google.bookmarkbubble.Bubble.prototype.IMAGE_CLOSE_DATA_URL_ = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAALVBMVEXM3fm+1Pfb5/rF2fjw9f23z/aavPOhwfTp8PyTt/L3+v7T4vqMs/K7zP////+qRWzhAAAAXElEQVQIW2O4CwUM996BwVskxtOqd++2rwMyPI+ve31GD8h4Madqz2mwms5jZ/aBGS/mHIDoen3m+DowY8/hOVUgxusz+zqPg7SvPA1UxQfSvu/du0YUK2AMmDMA5H1qhVX33T8AAAAASUVORK5CYII=';
/**
* The link used to locate the application's home screen icon to display inside
* the bubble. The default link used here is for an iPhone home screen icon
* without gloss. If your application uses a glossy icon, change this to
* 'apple-touch-icon'.
* @type {string}
* @private
*/
google.bookmarkbubble.Bubble.prototype.REL_ICON_ =
'apple-touch-icon-precomposed';
/**
* Regular expression for detecting an iPhone or iPod or iPad.
* @type {!RegExp}
* @private
*/
google.bookmarkbubble.Bubble.prototype.MOBILE_SAFARI_USERAGENT_REGEX_ =
/iPhone|iPod|iPad/;
/**
* Regular expression for detecting an iPad.
* @type {!RegExp}
* @private
*/
google.bookmarkbubble.Bubble.prototype.IPAD_USERAGENT_REGEX_ = /iPad/;
/**
* Determines whether the bubble should be shown or not.
* @return {boolean} Whether the bubble should be shown or not.
* @private
*/
google.bookmarkbubble.Bubble.prototype.isAllowedToShow_ = function() {
return this.isMobileSafari_() &&
!this.hasBeenDismissedTooManyTimes_() &&
!this.isFullscreen_() &&
!this.hasHashParameter();
};
/**
* Builds and shows the bubble.
* @private
*/
google.bookmarkbubble.Bubble.prototype.show_ = function() {
this.element_ = this.build_();
document.body.appendChild(this.element_);
this.element_.style.WebkitTransform =
'translateY(' + this.getHiddenYPosition_() + 'px)';
this.setHashParameter();
window.setTimeout(this.boundScrollHandler_, 1);
window.addEventListener('scroll', this.boundScrollHandler_, false);
// If the user does not dismiss the bubble, slide out and destroy it after
// some time.
window.setTimeout(google.bind(this.autoDestruct_, this),
this.TIME_UNTIL_AUTO_DESTRUCT);
};
/**
* Destroys the bubble by removing its DOM nodes from the document.
*/
google.bookmarkbubble.Bubble.prototype.destroy = function() {
if (this.hasBeenDestroyed_) {
return;
}
window.removeEventListener('scroll', this.boundScrollHandler_, false);
if (this.element_ && this.element_.parentNode == document.body) {
document.body.removeChild(this.element_);
this.element_ = null;
}
this.hasBeenDestroyed_ = true;
};
/**
* Remember that the user has dismissed the bubble once more.
* @private
*/
google.bookmarkbubble.Bubble.prototype.rememberDismissal_ = function() {
if (window.localStorage) {
try {
var key = this.LOCAL_STORAGE_PREFIX + this.DISMISSED_;
var value = Number(window.localStorage[key]) || 0;
window.localStorage[key] = String(value + 1);
} catch (ex) {
// Looks like we've hit the storage size limit. Currently we have no
// fallback for this scenario, but we could use cookie storage instead.
// This would increase the code bloat though.
}
}
};
/**
* Whether the user has dismissed the bubble often enough that we will not
* show it again.
* @return {boolean} Whether the user has dismissed the bubble often enough
* that we will not show it again.
* @private
*/
google.bookmarkbubble.Bubble.prototype.hasBeenDismissedTooManyTimes_ =
function() {
if (!window.localStorage) {
// If we can not use localStorage to remember how many times the user has
// dismissed the bubble, assume he has dismissed it. Otherwise we might end
// up showing it every time the host application loads, into eternity.
return true;
}
try {
var key = this.LOCAL_STORAGE_PREFIX + this.DISMISSED_;
// If the key has never been set, localStorage yields undefined, which
// Number() turns into NaN. In that case we'll fall back to zero for
// clarity's sake.
var value = Number(window.localStorage[key]) || 0;
return value >= this.NUMBER_OF_TIMES_TO_DISMISS;
} catch (ex) {
// If we got here, something is wrong with the localStorage. Make the same
// assumption as when it does not exist at all. Exceptions should only
// occur when setting a value (due to storage limitations) but let's be
// extra careful.
return true;
}
};
/**
* Whether the application is running in fullscreen mode.
* @return {boolean} Whether the application is running in fullscreen mode.
* @private
*/
google.bookmarkbubble.Bubble.prototype.isFullscreen_ = function() {
return !!window.navigator.standalone;
};
/**
* Whether the application is running inside Mobile Safari.
* @return {boolean} True if the current user agent looks like Mobile Safari.
* @private
*/
google.bookmarkbubble.Bubble.prototype.isMobileSafari_ = function() {
return this.MOBILE_SAFARI_USERAGENT_REGEX_.test(window.navigator.userAgent);
};
/**
* Whether the application is running on an iPad.
* @return {boolean} True if the current user agent looks like an iPad.
* @private
*/
google.bookmarkbubble.Bubble.prototype.isIpad_ = function() {
return this.IPAD_USERAGENT_REGEX_.test(window.navigator.userAgent);
};
/**
* Positions the bubble at the bottom of the viewport using an animated
* transition.
*/
google.bookmarkbubble.Bubble.prototype.setPosition = function() {
this.element_.style.WebkitTransition = '-webkit-transform 0.7s ease-out';
this.element_.style.WebkitTransform =
'translateY(' + this.getVisibleYPosition_() + 'px)';
};
/**
* Destroys the bubble by removing its DOM nodes from the document, and
* remembers that it was dismissed.
* @private
*/
google.bookmarkbubble.Bubble.prototype.closeClickHandler_ = function() {
this.destroy();
this.rememberDismissal_();
};
/**
* Gets called after a while if the user ignores the bubble.
* @private
*/
google.bookmarkbubble.Bubble.prototype.autoDestruct_ = function() {
if (this.hasBeenDestroyed_) {
return;
}
this.element_.style.WebkitTransition = '-webkit-transform 0.7s ease-in';
this.element_.style.WebkitTransform =
'translateY(' + this.getHiddenYPosition_() + 'px)';
window.setTimeout(google.bind(this.destroy, this), 700);
};
/**
* Gets the y offset used to show the bubble (i.e., position it on-screen).
* @return {number} The y offset.
* @private
*/
google.bookmarkbubble.Bubble.prototype.getVisibleYPosition_ = function() {
return this.isIpad_() ? window.pageYOffset + 17 :
window.pageYOffset - this.element_.offsetHeight + window.innerHeight - 17;
};
/**
* Gets the y offset used to hide the bubble (i.e., position it off-screen).
* @return {number} The y offset.
* @private
*/
google.bookmarkbubble.Bubble.prototype.getHiddenYPosition_ = function() {
return this.isIpad_() ? window.pageYOffset - this.element_.offsetHeight :
window.pageYOffset + window.innerHeight;
};
/**
* The url of the app's bookmark icon.
* @type {string|undefined}
* @private
*/
google.bookmarkbubble.Bubble.prototype.iconUrl_;
/**
* Scrapes the document for a link element that specifies an Apple favicon and
* returns the icon url. Returns an empty data url if nothing can be found.
* @return {string} A url string.
* @private
*/
google.bookmarkbubble.Bubble.prototype.getIconUrl_ = function() {
if (!this.iconUrl_) {
var link = this.getLink(this.REL_ICON_);
if (!link || !(this.iconUrl_ = link.href)) {
this.iconUrl_ = 'data:image/png;base64,';
}
}
return this.iconUrl_;
};
/**
* Gets the requested link tag if it exists.
* @param {string} rel The rel attribute of the link tag to get.
* @return {Element} The requested link tag or null.
*/
google.bookmarkbubble.Bubble.prototype.getLink = function(rel) {
rel = rel.toLowerCase();
var links = document.getElementsByTagName('link');
for (var i = 0; i < links.length; ++i) {
var currLink = /** @type {Element} */ (links[i]);
if (currLink.getAttribute('rel').toLowerCase() == rel) {
return currLink;
}
}
return null;
};
/**
* Creates the bubble and appends it to the document.
* @return {Element} The bubble element.
* @private
*/
google.bookmarkbubble.Bubble.prototype.build_ = function() {
var bubble = document.createElement('div');
var isIpad = this.isIpad_();
bubble.style.position = 'absolute';
bubble.style.zIndex = 1000;
bubble.style.width = '100%';
bubble.style.left = '0';
bubble.style.top = '0';
var bubbleInner = document.createElement('div');
bubbleInner.style.position = 'relative';
bubbleInner.style.width = '214px';
bubbleInner.style.margin = isIpad ? '0 0 0 82px' : '0 auto';
bubbleInner.style.border = '2px solid #fff';
bubbleInner.style.padding = '20px 20px 20px 10px';
bubbleInner.style.WebkitBorderRadius = '8px';
bubbleInner.style.WebkitBoxShadow = '0 0 8px rgba(0, 0, 0, 0.7)';
bubbleInner.style.WebkitBackgroundSize = '100% 8px';
bubbleInner.style.backgroundColor = '#b0c8ec';
bubbleInner.style.background = '#cddcf3 -webkit-gradient(linear, ' +
'left bottom, left top, ' + isIpad ?
'from(#cddcf3), to(#b3caed)) no-repeat top' :
'from(#b3caed), to(#cddcf3)) no-repeat bottom';
bubbleInner.style.font = '13px/17px sans-serif';
bubble.appendChild(bubbleInner);
// The "Add to Home Screen" text is intended to be the exact same size text
// that is displayed in the menu of Mobile Safari on iPhone.
bubbleInner.innerHTML = 'Install this web app on your phone: tap ' +
'<b style="font-size:15px">+</b> and then <b>\'Add to Home Screen\'</b>';
var icon = document.createElement('div');
icon.style['float'] = 'left';
icon.style.width = '55px';
icon.style.height = '55px';
icon.style.margin = '-2px 7px 3px 5px';
icon.style.background =
'#fff url(' + this.getIconUrl_() + ') no-repeat -1px -1px';
icon.style.WebkitBackgroundSize = '57px';
icon.style.WebkitBorderRadius = '10px';
icon.style.WebkitBoxShadow = '0 2px 5px rgba(0, 0, 0, 0.4)';
bubbleInner.insertBefore(icon, bubbleInner.firstChild);
var arrow = document.createElement('div');
arrow.style.backgroundImage = 'url(' + this.IMAGE_ARROW_DATA_URL_ + ')';
arrow.style.width = '25px';
arrow.style.height = '19px';
arrow.style.position = 'absolute';
arrow.style.left = '111px';
if (isIpad) {
arrow.style.WebkitTransform = 'rotate(180deg)';
arrow.style.top = '-19px';
} else {
arrow.style.bottom = '-19px';
}
bubbleInner.appendChild(arrow);
var close = document.createElement('a');
close.onclick = google.bind(this.closeClickHandler_, this);
close.style.position = 'absolute';
close.style.display = 'block';
close.style.top = '-3px';
close.style.right = '-3px';
close.style.width = '16px';
close.style.height = '16px';
close.style.border = '10px solid transparent';
close.style.background =
'url(' + this.IMAGE_CLOSE_DATA_URL_ + ') no-repeat';
bubbleInner.appendChild(close);
return bubble;
};

View file

@ -0,0 +1,43 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<!--
Copyright 2010 Google 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.
-->
<html>
<head>
<title>Sample</title>
<meta name="viewport"
content="width=device-width,minimum-scale=1.0,maximum-scale=1.0" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<link rel="apple-touch-icon-precomposed" href="../images/icon_calendar.png" />
<script type="text/javascript" src="../bookmark_bubble.js"></script>
<script type="text/javascript" src="example.js"></script>
</head>
<body style="height: 3000px;">
<p>The bookmark bubble will show after a second, if:</p>
<ul>
<li>It has not been previously dismissed too often</li>
<li>The application is not running in full screen mode</li>
<li>The bookmark bubble hash token is not present</li>
</ul>
<p>Supported browsers:</p>
<ul>
<li>iPhone / iPod / iPad Mobile Safari 3.0+</li>
</ul>
</body>
</html>

View file

@ -0,0 +1,57 @@
/*
Copyright 2010 Google 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.
*/
/** @fileoverview Example of how to use the bookmark bubble. */
window.addEventListener('load', function() {
window.setTimeout(function() {
var bubble = new google.bookmarkbubble.Bubble();
var parameter = 'bmb=1';
bubble.hasHashParameter = function() {
return window.location.hash.indexOf(parameter) != -1;
};
bubble.setHashParameter = function() {
if (!this.hasHashParameter()) {
window.location.hash += parameter;
}
};
bubble.getViewportHeight = function() {
window.console.log('Example of how to override getViewportHeight.');
return window.innerHeight;
};
bubble.getViewportScrollY = function() {
window.console.log('Example of how to override getViewportScrollY.');
return window.pageYOffset;
};
bubble.registerScrollHandler = function(handler) {
window.console.log('Example of how to override registerScrollHandler.');
window.addEventListener('scroll', handler, false);
};
bubble.deregisterScrollHandler = function(handler) {
window.console.log('Example of how to override deregisterScrollHandler.');
window.removeEventListener('scroll', handler, false);
};
bubble.showIfAllowed();
}, 1000);
}, false);

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

View file

@ -0,0 +1,33 @@
#!/bin/bash
# Copyright 2010 Google 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.
# Generates base64 versions of images so they can be inlined using the 'data:'
# URI scheme.
declare -ra IMAGE_FILES=( close.png arrow.png )
for image in ${IMAGE_FILES[@]}; do
OUT="$image.base64"
cat "$image" \
| uuencode -m ignore-this \
| grep -v begin-base64 \
| grep -v "====" \
| xargs echo \
| sed -e 's/ //g' \
| xargs echo -n \
> $OUT
ls -l $OUT
done

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -0,0 +1,27 @@
How to run the tests:
* The tests themselves assume that jsunit is in a sibling directory
to the one containing the distrbution. If this is not so, it is sufficient
to edit the paths in the test files. (On UNIX, symlinks may be your
friend if this is not convenient.)
* jsmock.js (available from http://jsmock.sourceforge.net/) should
be placed in the distribution directory.
* Specify the test files via a URL parameter. (This might be an issue
with jsunit: http://digitalmihailo.blogspot.com/2008/06/make-jsunit-work-in-firefox-30.html)
For example, if the root of your downloaded of the distribution is /mypath:
file:///mypath/jsunit/testRunner.html?testpage=mypath/webstorageportabilitylayer/dbwrapper_gears_test.html
file:///mypath/jsunit/testRunner.html?testpage=mypath/webstorageportabilitylayer/dbwrapper_html5_test.html
file:///mypath/jsunit/testRunner.html?testpage=mypath/webstorageportabilitylayer/dbwrapperapi_test.html
NB: the leading / in a UNIX path is not included in mypath so setting
it via pwd will not deliver the desired effect.

View file

@ -0,0 +1,45 @@
/*
Copyright 2009 Google 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.
*/
// Namespace.
google.wspl.DatabaseFactory = google.wspl.DatabaseFactory || {};
/**
* Factory function to build databases in a cross-API manner.
* @param {string} dbName of the database
* @param {string} dbworkerUrl the URL for Gears worker.
* @return {google.wspl.Database} The database object.
*/
google.wspl.DatabaseFactory.createDatabase = function(dbName, dbworkerUrl) {
var dbms;
if (window.openDatabase) {
// We have HTML5 functionality.
dbms = new google.wspl.html5.Database(dbName);
} else {
// Try to use Google Gears.
var gearsDb = goog.gears.getFactory().create('beta.database');
var wp = goog.gears.getFactory().create('beta.workerpool');
// Note that Gears will not allow file based URLs when creating a worker.
dbms = new wireless.db.gears.Database();
dbms.openDatabase('', dbName, gearsDb);
wp.onmessage = google.bind(dbms.onMessage_, dbms);
// Comment this line out to use the synchronous database.
dbms.startWorker(wp, dbworkerUrl, 0);
}
return dbms;
};

View file

@ -0,0 +1,324 @@
/*
Copyright 2009 Google 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.
*/
/**
* @fileoverview A worker thread that performs synchronous queries against a
* Gears database on behalf of an asynchronous calling client.
*
* The worker replies to the sender with messages to pass results, errors, and
* notifications about completed transactions. The type field of the message
* body specifies the message type. For each successful statement, a RESULT
* message is sent with a result attribute containing the Gears result set. For
* the first unsuccessful statement, an ERROR message will be sent with details
* stored in the error field. After the transaction has been committed, a COMMIT
* message is sent. If the transaction is rolled back, a ROLLBACK message is
* sent.
*
* NB: The worker must be served over http. Further, to operate successfully,
* it requires the inclusion of global_functions.js and gearsutils.js.
*/
/**
* Creates a DbWorker to handle incoming messages, execute queries, and return
* results to the main thread.
*
* @param {GearsWorkerPool} wp The gears worker pool.
* @constructor
*/
google.wspl.gears.DbWorker = function(wp) {
/**
* An array of transaction ids representing the transactions that are open on
* the database.
* @type {Array.<number>}
* @private
*/
this.transactions_ = [];
/**
* The gears worker pool.
* @type {GearsWorkerPool}
* @private
*/
this.wp_ = wp;
this.wp_.onmessage = google.bind(this.onMessage_, this);
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.ReplyTypes.STARTED
});
};
/**
* The gears database that this worker thread will interact with.
* @type {GearsDatabase}
* @private
*/
google.wspl.gears.DbWorker.prototype.db_;
/**
* A singleton instance of DbWorker.
* @type {google.wspl.gears.DbWorker?}
* @private
*/
google.wspl.gears.DbWorker.instance_;
/**
* The sender ID of the incomming messages. Default to 0 for workerpool ID.
* @type {number}
* @private
*/
google.wspl.gears.DbWorker.prototype.senderId_ = 0;
/**
* Message type constants for worker command messages.
* @enum {number}
*/
google.wspl.gears.DbWorker.CommandTypes = {
OPEN: 1,
BEGIN: 2,
EXECUTE: 3,
COMMIT: 4,
ROLLBACK: 5
};
/**
* Message type constants for worker reply messages.
* @enum {number}
*/
google.wspl.gears.DbWorker.ReplyTypes = {
RESULT: 1,
FAILURE: 2,
COMMIT: 3,
ROLLBACK: 4,
STARTED: 5,
OPEN_SUCCESSFUL: 6,
OPEN_FAILED: 7,
LOG: 8
};
/**
* Starts the DbWorker.
*/
google.wspl.gears.DbWorker.start = function() {
var wp = google.gears.workerPool;
google.wspl.gears.DbWorker.instance_ = new google.wspl.gears.DbWorker(wp);
};
/**
* Handles an OPEN command from the main thread.
*
* @param {string} userId The user to which the database belongs.
* @param {string} name The database's name.
*/
google.wspl.gears.DbWorker.prototype.handleOpen_ = function(userId, name) {
this.log_('Attempting to create Gears database: userId=' + userId + ', name='
+ name);
try {
this.db_ = google.gears.factory.create('beta.database', '1.0');
google.wspl.GearsUtils.openDatabase(userId, name, this.db_, this.log_);
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.ReplyTypes.OPEN_SUCCESSFUL
});
} catch (ex) {
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.ReplyTypes.OPEN_FAILED,
'error': ex
});
}
};
/**
* Handles a EXECUTE command from the main thread.
*
* @param {!Array.<google.wspl.Statement>} statements The statements to execute.
* @param {number} callbackId The callback to invoke after each execution.
* @param {number} transactionId The transaction that the statements belong to.
* @private
*/
google.wspl.gears.DbWorker.prototype.handleExecute_ =
function(statements, callbackId, transactionId) {
var self = this;
try {
this.executeAll_(statements, function(results) {
self.sendMessageToWorker_(/** @type {string} */({
'type': google.wspl.gears.DbWorker.ReplyTypes.RESULT,
'results': results,
'callbackId': callbackId,
'transactionId': transactionId
}));
});
} catch (e) {
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.ReplyTypes.FAILURE,
'error': e,
'callbackId': callbackId,
'transactionId': transactionId
});
}
};
/**
* Executes all of the statements on the Gears database. The callback is
* invoked with the query results after each successful query execution.
*
* @param {!Array.<Object>} statements The statements to execute.
* @param {Function} callback The callback to invoke with query results.
* @private
*/
google.wspl.gears.DbWorker.prototype.executeAll_ =
function(statements, callback) {
var results = [];
for (var i = 0; i < statements.length; i++) {
var resultset = this.db_.execute(statements[i]['sql'],
statements[i]['params']);
var result = google.wspl.GearsUtils.resultSetToObjectArray(resultset);
results.push(result);
}
callback(results);
};
/**
* Handles a BEGIN command from the main thread.
*
* @param {number} transactionId The transaction that the statements belong to.
* @private
*/
google.wspl.gears.DbWorker.prototype.handleBegin_ = function(transactionId) {
this.transactions_.push(transactionId);
this.db_.execute('BEGIN IMMEDIATE');
};
/**
* Handles a COMMIT command from the main thread.
*
* @param {number} transactionId The transaction that the statements belong to.
* @private
*/
google.wspl.gears.DbWorker.prototype.handleCommit_ = function(transactionId) {
this.db_.execute('COMMIT');
this.postCommit_();
};
/**
* Handles a ROLLBACK command from the main thread.
*
* @param {number} transactionId The transaction that the statements belong to.
* @private
*/
google.wspl.gears.DbWorker.prototype.handleRollback_ = function(transactionId) {
this.db_.execute('ROLLBACK');
this.postRollback_();
};
/**
* Sends a COMMIT reply to the main thread for each transaction that was
* committed.
*
* @private
*/
google.wspl.gears.DbWorker.prototype.postCommit_ = function() {
for (var i = this.transactions_.length - 1; i >= 0; i--) {
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.ReplyTypes.COMMIT,
'transactionId': this.transactions_[i]
});
}
this.transactions_ = [];
};
/**
* Sends a ROLLBACK reply to the main thread for each transaction that was
* rolled back.
*
* @private
*/
google.wspl.gears.DbWorker.prototype.postRollback_ = function() {
for (var i = this.transactions_.length - 1; i >= 0; i --) {
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.ReplyTypes.ROLLBACK,
'transactionId': this.transactions_[i]
});
}
this.transactions_ = [];
};
/**
* Handles incomming messages.
* @param {string} a Deprecated.
* @param {number} b Deprecated.
* @param {Object} messageObject The message object.
* @private
*/
google.wspl.gears.DbWorker.prototype.onMessage_ =
function(a, b, messageObject) {
this.senderId_ = messageObject.sender;
var message = messageObject.body;
var type = message['type'];
var name = message['name'];
var statements = message['statements'];
var callbackId = message['callbackId'];
var transactionId = message['transactionId'];
var userId = message['userId'];
try {
switch(type) {
case google.wspl.gears.DbWorker.CommandTypes.OPEN:
this.handleOpen_(userId, name);
break;
case google.wspl.gears.DbWorker.CommandTypes.EXECUTE:
this.handleExecute_(statements, callbackId, transactionId);
break;
case google.wspl.gears.DbWorker.CommandTypes.BEGIN:
this.handleBegin_(transactionId);
break;
case google.wspl.gears.DbWorker.CommandTypes.COMMIT:
this.handleCommit_(transactionId);
break;
case google.wspl.gears.DbWorker.CommandTypes.ROLLBACK:
this.handleRollback_(transactionId);
break;
}
} catch (ex) {
this.log_('Database worker failed: ' + ex.message);
}
};
/**
* Sends a log message to the main thread to be logged.
* @param {string} msg The message to log.
* @private
*/
google.wspl.gears.DbWorker.prototype.log_ = function(msg) {
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.ReplyTypes.LOG,
'msg': msg
});
};
/**
* Sends a message to the main worker thread.
* @param {Object} msg The message object to send.
* @private
*/
google.wspl.gears.DbWorker.prototype.sendMessageToWorker_ = function(msg) {
this.wp_.sendMessage(msg, this.senderId_);
};

View file

@ -0,0 +1,393 @@
<!DOCTYPE html>
<!--
Copyright 2009 Google 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.
-->
<html>
<head>
<title>Gears worker tests</title>
<script type="text/javascript" src="../jsunit/app/jsUnitCore.js"></script>
<script type="text/javascript" src="jsmock.js"></script>
<script type="text/javascript" src="global_functions.js"></script>
<script type="text/javascript" src="gearsutils.js"></script>
<script type="text/javascript" src="dbworker.js"></script>
</head>
<body>
<script type='text/javascript'>
var mockControl;
var db;
var wp;
var factory;
var callbackId = 10;
var transactionId = 15;
var name = 'name';
var userId = 'userId';
var utils;
function setUp() {
mockControl = new MockControl();
// Mock the Gears factory.
factory = mockControl.createMock();
factory.addMockMethod('create');
// Mock Google Gears.
google.gears = {};
google.gears.factory = {};
google.gears.factory.create = factory.create;
// Mock the Gears workerpool object.
wp = mockControl.createMock({
allowCrossOrigin: function(){},
sendMessage: function(){}
});
// Mock the Gears database object.
db = mockControl.createMock({
execute: function(){},
open: function(){},
close: function(){},
remove: function(){}
});
// Mock the Gears utility classes
utils = mockControl.createMock({
openDatabase: function(){},
});
google.wspl = google.wspl || {};
google.wspl.GearsUtils = google.wspl.GearsUtils || {};
google.wspl.GearsUtils.openDatabase = utils.openDatabase;
google.wspl.GearsUtils.resultSetToObjectArray = function(rs) {
return rs;
};
}
function buildWorker() {
wp.expects().sendMessage(TypeOf.isA(Object), 0).andStub(
function() {
var msg = arguments[0];
assertEquals('Wrong message type.',
google.wspl.gears.DbWorker.ReplyTypes.STARTED, msg.type);
});
var worker = new google.wspl.gears.DbWorker(wp);
worker.db_ = db;
worker.log_ = function() {};
return worker;
}
function testConstruction() {
var worker = buildWorker();
mockControl.verify();
}
function testHandleExecute_success() {
var worker = buildWorker();
var stat1 = {sql: 'sql1', params: [1, 2]};
var stat2 = {sql: 'sql2', params: [3, 4]};
var statements = [stat1, stat2];
var type = google.wspl.gears.DbWorker.ReplyTypes.RESULT;
db.expects().execute(stat1.sql, stat1.params).andReturn('result1');
db.expects().execute(stat2.sql, stat2.params).andReturn('result2');
wp.expects().sendMessage(TypeOf.isA(Object), worker.senderId_).andStub(
function() {
var msg = arguments[0];
assertEquals('Wrong message type.', type, msg.type);
assertEquals('Wrong results.length', 2, msg.results.length);
assertEquals('Wrong results[0].', 'result1', msg.results[0]);
assertEquals('Wrong results[1].', 'result2', msg.results[1]);
assertEquals('Wrong callbackId.', callbackId, msg.callbackId);
assertEquals('Wrong transactionId.', transactionId, msg.transactionId);
});
worker.handleExecute_(statements, callbackId, transactionId);
mockControl.verify();
}
function testHandleExecute_failure() {
var worker = buildWorker();
var stat1 = {sql: 'sql1', params: [1, 2]};
var stat2 = {sql: 'sql2', params: [3, 4]};
var stat3 = {sql: 'sql3', params: [5, 6]};
var statements = [stat1, stat2, stat3];
var type1 = google.wspl.gears.DbWorker.ReplyTypes.RESULT;
var type2 = google.wspl.gears.DbWorker.ReplyTypes.FAILURE;
var error = 'sql error';
db.expects().execute(stat1.sql, stat1.params).andReturn('result1');
db.expects().execute(stat2.sql, stat2.params).andThrow(error);
wp.expects().sendMessage(TypeOf.isA(Object), worker.senderId_).andStub(
function() {
var msg = arguments[0];
assertEquals('Wrong message type.', type2, msg.type);
assertEquals('Wrong result.', error, msg.error.message);
assertEquals('Wrong callbackId.', callbackId, msg.callbackId);
assertEquals('Wrong transactionId.', transactionId, msg.transactionId);
});
worker.handleExecute_(statements, callbackId, transactionId);
mockControl.verify();
}
function testHandleBegin() {
var worker = buildWorker();
// Expecting two transactions to begin.
db.expects().execute('BEGIN IMMEDIATE');
db.expects().execute('BEGIN IMMEDIATE');
worker.handleBegin_(transactionId);
worker.handleBegin_(22);
assertEquals('Did not save first transaction id', transactionId,
worker.transactions_[0]);
assertEquals('Did not save second transaction id', 22,
worker.transactions_[1]);
mockControl.verify();
}
function testHandleCommit() {
var worker = buildWorker();
db.expects().execute('COMMIT');
worker.handleCommit_(transactionId);
mockControl.verify();
}
function testHandleRollback() {
var worker = buildWorker();
db.expects().execute('ROLLBACK');
worker.handleRollback_(transactionId);
mockControl.verify();
}
function testHandleOpen_success() {
var worker = buildWorker();
worker.db_ = null;
factory.expects().create('beta.database', '1.0').andReturn(db);
utils.expects().openDatabase(userId, name, db, worker.log_).andReturn(db);
wp.expects().sendMessage(TypeOf.isA(Object), worker.senderId_).andStub(
function(msg) {
assertEquals('Type not set correctly.',
google.wspl.gears.DbWorker.ReplyTypes.OPEN_SUCCESSFUL, msg.type);
});
worker.handleOpen_(userId, name);
assertEquals('Database wrongly set', db, worker.db_);
mockControl.verify();
}
function testHandleOpen_failure_gearsfactory() {
var worker = buildWorker();
worker.db_ = null;
factory.expects().create('beta.database', '1.0').andThrow('blah!');
wp.expects().sendMessage(TypeOf.isA(Object), worker.senderId_).andStub(
function(msg) {
assertEquals('Type not set correctly.',
google.wspl.gears.DbWorker.ReplyTypes.OPEN_FAILED, msg.type);
});
worker.handleOpen_(userId, name);
mockControl.verify();
}
function testHandleOpen_failure_dbopen() {
var worker = buildWorker();
worker.db_ = null;
factory.expects().create('beta.database', '1.0').andReturn(null);
utils.expects().openDatabase(userId, name, null, worker.log_).andThrow('blah!');
wp.expects().sendMessage(TypeOf.isA(Object), worker.senderId_).andStub(
function(msg) {
assertEquals('Type not set correctly.',
google.wspl.gears.DbWorker.ReplyTypes.OPEN_FAILED, msg.type);
});
worker.handleOpen_(userId, name);
mockControl.verify();
}
function testPostCommit() {
var worker = buildWorker();
worker.transactions_ = [4, 5];
wp.expects().sendMessage(TypeOf.isA(Object), worker.senderId_).andStub(
function() {
var msg = arguments[0];
assertEquals('Type not set correctly.',
google.wspl.gears.DbWorker.ReplyTypes.COMMIT, msg.type);
assertEquals('Transaction id not set correctly.',
5, msg.transactionId);
});
wp.expects().sendMessage(TypeOf.isA(Object), worker.senderId_).andStub(
function() {
var msg = arguments[0];
assertEquals('Type not set correctly.',
google.wspl.gears.DbWorker.ReplyTypes.COMMIT, msg.type);
assertEquals('Transaction id not set correctly.',
4, msg.transactionId);
});
worker.postCommit_();
assertEquals('Did not clear the transactions.', 0,
worker.transactions_.length);
mockControl.verify();
}
function testPostRollback() {
var worker = buildWorker();
worker.transactions_ = [4, 5];
wp.expects().sendMessage(TypeOf.isA(Object), worker.senderId_).andStub(
function() {
var msg = arguments[0];
assertEquals('Type not set correctly.',
google.wspl.gears.DbWorker.ReplyTypes.ROLLBACK, msg.type);
assertEquals('Transaction id not set correctly.',
5, msg.transactionId);
});
wp.expects().sendMessage(TypeOf.isA(Object), worker.senderId_).andStub(
function() {
var msg = arguments[0];
assertEquals('Type not set correctly.',
google.wspl.gears.DbWorker.ReplyTypes.ROLLBACK, msg.type);
assertEquals('Transaction id not set correctly.',
4, msg.transactionId);
});
worker.postRollback_();
assertEquals('Did not clear the transactions.', 0,
worker.transactions_.length);
mockControl.verify();
}
function testOnmessage() {
var messageObject = {sender: 123, body: {}};
var worker = buildWorker();
worker.onMessage_(null, null, messageObject);
assertEquals('Wrong sender ID.', 123, worker.senderId_);
mockControl.verify();
}
function testOnmessage_open() {
var messageObject = {sender: 123, body: {
type: google.wspl.gears.DbWorker.CommandTypes.OPEN,
name: name,
userId: userId
}};
var worker = buildWorker();
var handler = mockControl.createMock();
handler.addMockMethod('open');
worker.handleOpen_ = handler.open;
handler.expects().open(userId, name);
worker.onMessage_(null, null, messageObject);
mockControl.verify();
}
function testOnmessage_execute() {
var worker = buildWorker();
var statements = ['stat1', 'stat2'];
var messageObject = {sender: 123, body: {
type: google.wspl.gears.DbWorker.CommandTypes.EXECUTE,
statements: statements,
callbackId: callbackId,
transactionId: transactionId
}};
var called = false;
worker.handleExecute_ = function(stat, call, trans) {
called = true;
assertEquals('Wrong statements.', statements, stat);
assertEquals('Wrong callback id.', callbackId, call);
assertEquals('Wrong transaction id.', transactionId, trans);
};
worker.onMessage_(null, null, messageObject);
assertTrue('handleExecute_ not called.', called);
mockControl.verify();
}
function testOnmessage_begin() {
var worker = buildWorker();
var messageObject = {sender: 123, body: {
type: google.wspl.gears.DbWorker.CommandTypes.BEGIN,
transactionId: transactionId
}};
var called = false;
worker.handleBegin_ = function(trans) {
called = true;
assertEquals('Wrong transaction id.', transactionId, trans);
};
worker.onMessage_(null, null, messageObject);
assertTrue('handleBegin_ not called.', called);
mockControl.verify();
}
function testOnmessage_commit() {
var worker = buildWorker();
var messageObject = {sender: 123, body: {
type: google.wspl.gears.DbWorker.CommandTypes.COMMIT,
transactionId: transactionId
}};
var called = false;
worker.handleCommit_ = function(trans) {
called = true;
assertEquals('Wrong transaction id.', transactionId, trans);
};
worker.onMessage_(null, null, messageObject);
assertTrue('handleCommit_ not called.', called);
mockControl.verify();
}
function testOnmessage_rollback() {
var worker = buildWorker();
var messageObject = {sender: 123, body: {
type: google.wspl.gears.DbWorker.CommandTypes.ROLLBACK,
transactionId: transactionId
}};
var called = false;
worker.handleRollback_ = function(trans) {
called = true;
assertEquals('Wrong transaction id.', transactionId, trans);
};
worker.onMessage_(null, null, messageObject);
assertTrue('handleRollback_ not called.', called);
mockControl.verify();
}
</script>
</body>
</html>

View file

@ -0,0 +1,32 @@
/*
Copyright 2009 Google 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.
*/
/**
* @fileoverview Starts the dbworker.
*
* When constructing the worker for execution, this needs to be the last
* file. The worker consists of the following source files combined together.
*
* globalfunctions.js
* gearsutils.js
* dbworker.js
* dbworkerstarter.js
*
* and then loaded into a Gears worker process as implemented in
* databasefactory.js
*/
google.wspl.gears.DbWorker.start();

View file

@ -0,0 +1,595 @@
/*
Copyright 2009 Google 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.
*/
/**
* @fileoverview A Gears implementation of dbwrapperapi Database.
*
* This implementation locks database access upon invoking the transaction's
* populate callback. Statements are then asynchronously sent to a worker
* thread for execution.
*/
/**
* @see google.wspl.Database#Database
* @param {boolean} opt_sync Perform all callbacks synchronously.
* @constructor
* @extends google.wspl.Database
*/
google.wspl.gears.Database = function(opt_sync) {
google.wspl.Database.call(this);
/**
* Begin transactions synchronously.
* @type {boolean}
* @private
*/
this.synchronous_ = !!opt_sync;
};
google.inherits(google.wspl.gears.Database, google.wspl.Database);
/**
* The time to wait for the dbworker to reply with STARTED.
* @type {number}
*/
google.wspl.gears.Database.TIMEOUT = 60000;
/**
* Whether the gears worker failed to reply with STARTED before TIMEOUT.
* @type {boolean}
* @private
*/
google.wspl.gears.Database.prototype.workerTimeout_ = false;
/**
* Flag set when the worker is ready with an open database connection.
* @type {boolean}
* @private
*/
google.wspl.gears.Database.prototype.workerReady_ = false;
/**
* Flag set when this database should use the worker to process transactions.
* @type {boolean}
* @private
*/
google.wspl.gears.Database.prototype.useWorker_ = false;
/**
* The user for this database.
* @type {string}
* @private
*/
google.wspl.gears.Database.prototype.userId_;
/**
* The name for this database.
* @type {string}
* @private
*/
google.wspl.gears.Database.prototype.name_;
/**
* A map of open transactions and their callbacks.
* @type {Object}
* @private
*/
google.wspl.gears.Database.prototype.transactions_ = {};
/**
* An array of transaction ids that should be executed in order as the lock
* becomes available.
* @type {Array.<number>}
* @private
*/
google.wspl.gears.Database.prototype.queuedTransactions_ = [];
/**
* The transaction lock for this database.
* @type {boolean}
* @private
*/
google.wspl.gears.Database.prototype.locked_ = false;
/**
* The number of transactions to be used as an index.
* @type {number}
* @private
*/
google.wspl.gears.Database.prototype.transCount_ = 1;
/**
* The id of the transaction being executed.
* @type {number}
* @private
*/
google.wspl.gears.Database.prototype.currentTransactionId_;
/**
* The Gears worker pool.
* @type {GearsWorkerPool}
* @private
*/
google.wspl.gears.Database.prototype.wp_;
/**
* The worker ID.
* @type {number}
* @private
*/
google.wspl.gears.Database.prototype.workerId_;
/**
* The Gears database object.
* @type {GearsDatabase}
* @private
*/
google.wspl.gears.Database.prototype.db_;
/**
* Opens a new Gears database. This operation can only be performed once.
* @param {string} userId The user for this database.
* @param {string} name The name for this database.
* @param {GearsDatabase} gearsDb The gears database.
*/
google.wspl.gears.Database.prototype.openDatabase = function(userId, name,
gearsDb) {
if (!this.db_) {
this.db_ = gearsDb;
this.userId_ = userId;
this.name_ = name;
google.wspl.GearsUtils.openDatabase(userId, name, this.db_,
google.logger);
} else {
google.logger('openDatabase already invoked.');
}
};
/**
* Starts a worker to handle the database interactions. The worker will be
* asynchronously started after the specified delay and will not be used until
* the completion of any pending transaction.
* @param {GearsWorkerPool} wp The Gears worker pool.
* @param {string} workerUrl The URL to find the gears database worker.
* @return {number} The worker ID.
*/
google.wspl.gears.Database.prototype.startWorker = function(wp, workerUrl) {
this.wp_ = wp;
google.logger('Starting dbworker thread.');
this.workerId_ = wp.createWorkerFromUrl(workerUrl);
this.timeoutId_ = window.setTimeout(google.bind(this.handleTimeout_, this),
google.wspl.gears.Database.TIMEOUT);
return this.workerId_;
};
/**
* @see google.wspl.Transaction#createTransaction
* @inheritDoc
*/
google.wspl.gears.Database.prototype.createTransaction = function(populate,
opt_callback) {
var transactionCallback = opt_callback || {
onSuccess : function() {},
onFailure : function() {}
};
var id = this.transCount_++;
var transaction = new google.wspl.gears.Transaction(id, this);
this.saveTransaction_(transaction, transactionCallback, populate);
this.queuedTransactions_.push(transaction.id_);
this.nextTransaction_();
};
/**
* Saves the transaction and transaction callback to be accessed later when a
* commit or rollback is performed.
*
* @param {google.wspl.gears.Transaction} transaction The transaction that the
* callback belongs to.
* @param {Object} callback A transaction callback with onSuccess and onFailure
* @private
*/
google.wspl.gears.Database.prototype.saveTransaction_ = function(
transaction, callback, populate) {
this.transactions_[transaction.id_] = {
transaction: transaction,
callback: callback,
populate: populate
};
};
/**
* Handles incomming messages.
* @param {string} a Deprecated.
* @param {number} b Deprecated.
* @param {Object} messageObject The message object.
* @private
*/
google.wspl.gears.Database.prototype.onMessage_ =
function(a, b, messageObject) {
var message = messageObject.body;
try {
switch(message['type']) {
case google.wspl.gears.DbWorker.ReplyTypes.RESULT:
this.handleResult_(message['results'], message['callbackId'],
message['transactionId']);
break;
case google.wspl.gears.DbWorker.ReplyTypes.FAILURE:
this.handleFailure_(message['error'], message['callbackId'],
message['transactionId']);
break;
case google.wspl.gears.DbWorker.ReplyTypes.COMMIT:
this.handleCommit_(message['transactionId']);
break;
case google.wspl.gears.DbWorker.ReplyTypes.ROLLBACK:
this.handleRollback_(message['transactionId']);
break;
case google.wspl.gears.DbWorker.ReplyTypes.STARTED:
this.handleStarted_();
break;
case google.wspl.gears.DbWorker.ReplyTypes.OPEN_SUCCESSFUL:
this.handleOpenSuccessful_();
break;
case google.wspl.gears.DbWorker.ReplyTypes.OPEN_FAILED:
this.handleOpenFailed_(message['error']);
break;
case google.wspl.gears.DbWorker.ReplyTypes.LOG:
google.logger(message['msg']);
break;
}
} catch (ex) {
google.logger('Gears database failed: ' + ex.message, ex);
}
};
/**
* Opens a new Gears database.
*
* @param {string} userId The user to which the database belongs.
* @param {string} name The name of the database.
*/
google.wspl.gears.Database.prototype.doOpen = function(userId, name) {
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.CommandTypes.OPEN,
'name': name,
'userId': userId
});
};
/**
* Begins a new transaction on the Gears database.
*
* @param {number} transactionId The id of the transaction being committed.
*/
google.wspl.gears.Database.prototype.doBegin = function(transactionId) {
if (!this.useWorker_) {
this.db_.execute('BEGIN IMMEDIATE');
return;
}
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.CommandTypes.BEGIN,
'transactionId': transactionId
});
};
/**
* Commits the current transaction on the Gears database. The transactionId
* is used to invoke the callback associated with the transaction.
*
* @param {number} transactionId The id of the transaction being committed.
*/
google.wspl.gears.Database.prototype.doCommit = function(transactionId) {
if (!this.useWorker_) {
this.db_.execute('COMMIT');
this.postCommit_();
return;
}
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.CommandTypes.COMMIT,
'transactionId': transactionId
});
};
/**
* Rolls the current transaction back on the Gears database. The transactionId
* is used to invoke the callback associated with the transaction.
*
* @param {number} transactionId The id of the transaction being rolled back.
*/
google.wspl.gears.Database.prototype.doRollback = function(transactionId) {
if (!this.useWorker_) {
this.db_.execute('ROLLBACK');
this.postRollback_();
return;
}
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.CommandTypes.ROLLBACK,
'transactionId': transactionId
});
};
/**
* Executes an array of statements on the Gears database. The transactionId and
* callbackId are used to identify the callback that should be invoked when
* handleResult or handleFailure is called.
*
* @param {Array.<google.wspl.Statement>} statements The group of statements to
* execute
* @param {number} callbackId The callback to invoke for each statement
* @param {number} transactionId The transaction that the statements belong to
*/
google.wspl.gears.Database.prototype.doExecute = function(statements,
callbackId,
transactionId) {
if (!this.useWorker_) {
this.doExecuteSynchronously_(statements, callbackId, transactionId);
return;
}
var newStatements = [];
for (var i = 0; i < statements.length; i++) {
newStatements[i] = {
'sql': statements[i].sql,
'params': statements[i].params
};
}
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.CommandTypes.EXECUTE,
'statements': newStatements,
'callbackId': callbackId,
'transactionId': transactionId
});
};
/**
* Executes an array of statements on the synchronous Gears databse.
* @param {Array.<google.wspl.Statement>} statements
* @param {number} callbackId
* @param {number} transactionId
* @private
*/
google.wspl.gears.Database.prototype.doExecuteSynchronously_ =
function(statements, callbackId, transactionId) {
var db = this;
var results = [];
for (var i = 0; i < statements.length; i++) {
try {
var resultset = this.db_.execute(statements[i].sql, statements[i].params);
var result = google.wspl.GearsUtils.resultSetToObjectArray(resultset);
results.push(result);
} catch (e) {
var error = e;
function failureCallback() {
db.handleFailure_(error, callbackId, transactionId);
};
this.setTimeout_(failureCallback, 0);
return;
}
}
function resultCallback() {
db.handleResult_(results, callbackId, transactionId);
};
this.setTimeout_(resultCallback, 0);
};
/**
* Handles a RESULT message from the worker thread.
*
* @param {!Array.<!Array.<Object>>} results A Gears result set.
* @param {number} callbackId The callback to invoke.
* @param {number} transactionId The transaction that the statement is executing
* in.
* @private
*/
google.wspl.gears.Database.prototype.handleResult_ = function(results,
callbackId, transactionId) {
var transInfo = this.transactions_[transactionId];
if (transInfo) {
for (var i = 0, l = results.length; i < l; i++) {
var resultSet = new google.wspl.gears.ResultSet(results[i]);
transInfo.transaction.success(resultSet, callbackId);
}
}
};
/**
* Handles a FAILURE message from the worker thread.
*
* @param {Error} error An error produced by the Gears database
* @param {number} callbackId The callback to invoke
* @param {number} transactionId The transaction that the statement is executing
* in
* @private
*/
google.wspl.gears.Database.prototype.handleFailure_ = function(error,
callbackId, transactionId) {
var transInfo = this.transactions_[transactionId];
if (transInfo) {
transInfo.error = error;
transInfo.transaction.failure(error, callbackId);
}
};
/**
* Handles a COMMIT message from the worker thread.
*
* @param {number} id The transaction id.
* @private
*/
google.wspl.gears.Database.prototype.handleCommit_ = function(id) {
var transaction = this.removeTransaction_(id);
if (transaction) {
transaction.callback.onSuccess();
}
this.nextTransaction_();
};
/**
* Handles the completion of a commit from the synchronous database.
* @private
*/
google.wspl.gears.Database.prototype.postCommit_ = function() {
this.handleCommit_(this.currentTransactionId_);
};
/**
* Handles a ROLLBACK message from the worker thread.
*
* @param {number} id The transaction id
* @private
*/
google.wspl.gears.Database.prototype.handleRollback_ = function(id) {
var transaction = this.removeTransaction_(id);
if (transaction) {
transaction.callback.onFailure(transaction.error);
}
this.nextTransaction_();
};
/**
* Handles the completion of a rollback from the synchronous database.
* @private
*/
google.wspl.gears.Database.prototype.postRollback_ = function() {
this.handleRollback_(this.currentTransactionId_);
};
/**
* Handles a STARTED message from the worker thread.
*
* @private
*/
google.wspl.gears.Database.prototype.handleStarted_ = function() {
if (!this.workerTimeout_) {
google.logger('Dbworker started.');
window.clearTimeout(this.timeoutId_);
this.timeoutId_ = 0;
this.doOpen(this.userId_, this.name_);
}
};
/**
* Handles a timeout of waiting for a STARTED message from the worker thread.
*
* @private
*/
google.wspl.gears.Database.prototype.handleTimeout_ = function() {
this.workerTimeout_ = true;
google.logger('Timed out while waiting for the dbworker to start.');
};
/**
* Handles a OPEN_SUCCESSFUL message from the worker thread.
*
* @private
*/
google.wspl.gears.Database.prototype.handleOpenSuccessful_ = function() {
this.workerReady_ = true;
};
/**
* Handles a OPEN_FAILED message from the worker thread.
* @param {string} error
* @private
*/
google.wspl.gears.Database.prototype.handleOpenFailed_ = function(error) {
google.logger('Worker failed to open Gears database.');
};
/**
* Executes the next transaction if there is one queued.
*
* @private
*/
google.wspl.gears.Database.prototype.nextTransaction_ = function() {
if (this.queuedTransactions_.length && !this.locked_) {
this.locked_ = true;
if (this.workerReady_ && !this.useWorker_) {
this.useWorker_ = true;
google.logger('Switching to asynchronous database interface.');
}
var id = this.queuedTransactions_.shift();
this.currentTransactionId_ = id;
var transactionData = this.transactions_[id];
var db = this;
function populate() {
transactionData.populate(transactionData.transaction);
// If populate did not execute statements on the database, invoke the
// success callback and process the next transaction.
if (!transactionData.transaction.isExecuting()) {
db.handleCommit_(id);
}
};
this.setTimeout_(populate, 0);
}
};
/**
* Cleans up the transaction and transaction callback for the id specified.
*
* @param {number} id The transaction id.
* @return {google.wspl.Transaction} The transaction and callback in an object.
* @private
*/
google.wspl.gears.Database.prototype.removeTransaction_ = function(id) {
this.locked_ = false;
var transaction = this.transactions_[id];
if (transaction) {
delete this.transactions_[id];
}
return transaction;
};
/**
* Execute a function using window's setTimeout.
* @param {Function} func The function to execute.
* @param {number} time The time delay before invocation.
* @private
*/
google.wspl.gears.Database.prototype.setTimeout_ = function(func, time) {
if (this.synchronous_) {
func();
} else {
window.setTimeout(func, time);
}
};
/**
* Sends a message to the database worker thread.
* @param {Object} msg The message object to send.
* @private
*/
google.wspl.gears.Database.prototype.sendMessageToWorker_ = function(msg) {
this.wp_.sendMessage(msg, this.workerId_);
};

View file

@ -0,0 +1,404 @@
<!DOCTYPE html>
<!--
Copyright 2009 Google 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.
-->
<html>
<head>
<title>Gears database wrapper tests</title>
<script type="text/javascript" src="../jsunit/app/jsUnitCore.js"></script>
<script type="text/javascript" src="jsmock.js"></script>
<script type="text/javascript" src="global_functions.js"></script>
<script type="text/javascript" src="gearsutils.js"></script>
<script type="text/javascript" src="dbwrapperapi.js"></script>
<script type="text/javascript" src="gears_resultset.js"></script>
<script type="text/javascript" src="gears_transaction.js"></script>
<script type="text/javascript" src="dbworker.js"></script>
<script type="text/javascript" src="dbwrapper_gears.js"></script>
</head>
<body>
<script type='text/javascript'>
var mockControl;
var callbackMock;
var dbMock;
var wp;
var workerId = 123;
var transMock;
function setUp() {
mockControl = new MockControl();
callbackMock = mockControl.createMock({
onSuccess: function(){},
onFailure: function(){}
});
transMock = mockControl.createMock({
success: function(){},
failure: function(){}
});
wp = mockControl.createMock({
allowCrossOrigin: function(){},
createWorkerFromUrl: function(){},
sendMessage: function(){}
});
dbMock = mockControl.createMock({
execute: function(){},
open: function(){},
close: function(){},
remove: function(){}
});
google.wspl.GearsUtils.resultSetToObjectArray = function(rs) {
return rs;
};
}
function buildSyncDatabase() {
var database = new google.wspl.gears.Database(true);
database.userId_ = 'userId';
database.name_ = 'name';
database.db_ = dbMock;
return database;
}
function buildDatabase() {
var database = buildSyncDatabase();
var type = google.wspl.gears.DbWorker.CommandTypes.OPEN;
wp.expects().createWorkerFromUrl('workerUrl').andReturn(workerId);
wp.expects().sendMessage(TypeOf.isA(Object), workerId).andStub(function() {
assertEquals('Wrong message type.', type, arguments[0].type);
assertEquals('Incorrect name.', 'name', arguments[0].name);
assertEquals('Incorrect user id.', 'userId', arguments[0].userId);
});
database.startWorker(wp, 'workerUrl', 0);
database.handleStarted_();
database.handleOpenSuccessful_();
database.useWorker_ = true;
return database;
}
function testConstructor() {
var db = buildSyncDatabase();
mockControl.verify();
}
function testStartWorker() {
var db = buildDatabase();
assertTrue('Expected worker to be ready.', db.workerReady_);
mockControl.verify();
}
function testCreateTransaction() {
var db = buildDatabase();
var tx;
var populateMock = function(txa) {
tx = txa;
var transactions = db.transactions_;
assertEquals('missing transaction', tx, transactions[tx.id_].transaction);
assertEquals('missing callback', callbackMock,
transactions[tx.id_].callback);
assertEquals('database not saved', db, tx.db_);
assertTrue('database should be locked', db.locked_);
};
callbackMock.expects().onSuccess();
var transactions = db.transactions_;
db.createTransaction(populateMock, callbackMock);
assertEquals('failed to clean up transaction', undefined,
transactions[tx.id_]);
assertFalse('database should not be locked', db.locked_);
mockControl.verify();
}
function testMultipleTransactions() {
var db = buildDatabase();
var handler = mockControl.createMock();
handler.addMockMethod('populate');
handler.addMockMethod('onSuccess');
handler.addMockMethod('onFailure');
var trans;
handler.expects().populate(TypeOf.isA(google.wspl.gears.Transaction)).andStub(
function(tx) {
trans = tx;
tx.numActiveExecutes_ = 1;
});
db.createTransaction(handler.populate, handler);
db.createTransaction(handler.populate, handler);
mockControl.verify();
handler.expects().onSuccess();
handler.expects().populate(TypeOf.isA(google.wspl.gears.Transaction)).andStub(
function(tx) {
trans = tx;
tx.numActiveExecutes_ = 1;
});
db.handleCommit_(trans.id_);
mockControl.verify();
handler.expects().onFailure(undefined).andStub(function() {
db.createTransaction(handler.populate, handler);
});
handler.expects().populate(TypeOf.isA(google.wspl.gears.Transaction)).andStub(
function(tx) {
trans = tx;
tx.numActiveExecutes_ = 1;
});
db.handleRollback_(trans.id_);
mockControl.verify();
handler.expects().onSuccess();
db.handleCommit_(trans.id_);
mockControl.verify();
}
function testDoBegin() {
var db = buildDatabase();
var transactionId = 10;
var type = google.wspl.gears.DbWorker.CommandTypes.BEGIN;
wp.expects().sendMessage(TypeOf.isA(Object), workerId).andStub(function() {
assertEquals('wrong message type', type, arguments[0].type);
assertNotUndefined('Missing transaction id.', arguments[0].transactionId);
});
db.doBegin(transactionId);
mockControl.verify();
}
function testSynchronousBegin() {
var db = buildSyncDatabase();
dbMock.expects().execute('BEGIN IMMEDIATE');
db.doBegin(1);
mockControl.verify();
}
function testDoExecute() {
var statements = [{sql: 's1', params: 'p1'},
{sql: 's2', params: 'p2'},
{sql: 's3', params: 'p3'}];
var callbackId = 5;
var transactionId = 10;
var db = buildDatabase();
var type = google.wspl.gears.DbWorker.CommandTypes.EXECUTE;
wp.expects().sendMessage(TypeOf.isA(Object), workerId).andStub(function() {
assertEquals('wrong message type', type, arguments[0].type);
assertEquals('statements do not match', statements.length,
arguments[0].statements.length);
for (var i = 0; i < statements.length; i++) {
assertEquals('a statement sql does not match', statements[i].sql,
arguments[0].statements[i].sql);
assertEquals('a statement params does not match', statements[i].params,
arguments[0].statements[i].params);
}
assertEquals('missing callback id', callbackId, arguments[0].callbackId);
assertEquals('missing trans id', transactionId, arguments[0].transactionId);
});
db.doExecute(statements, callbackId, transactionId);
mockControl.verify();
}
function testSynchronousExecute() {
var statements = [{sql: 's1', params: 'p1'},
{sql: 's2', params: 'p2'},
{sql: 's3', params: 'p3'}];
var callbackId = 5;
var transactionId = 10;
var db = buildSyncDatabase();
for (var i = 0; i < 3; i++) {
var stat = statements[i];
dbMock.expects().execute(stat.sql, stat.params).andReturn('result' + i);
}
var handler = mockControl.createMock();
handler.addMockMethod('handleResult_');
db.handleResult_ = handler.handleResult_;
handler.expects().handleResult_(
TypeOf.isA(Array), callbackId, transactionId).andStub(function(results) {
var expected = ['result0', 'result1', 'result2'];
assertArrayEquals('Wrong results.', expected, results);
});
db.doExecute(statements, callbackId, transactionId);
mockControl.verify();
}
function testSynchronousExecute_failure() {
var statements = [{sql: 's1', params: 'p1'},
{sql: 's2', params: 'p2'},
{sql: 's3', params: 'p3'}];
var callbackId = 5;
var transactionId = 10;
var db = buildSyncDatabase();
dbMock.expects().execute('s1', 'p1').andReturn('result0');
dbMock.expects().execute('s2', 'p2').andThrow(Error('db error'));
var handler = mockControl.createMock();
handler.addMockMethod('handleFailure_');
db.handleFailure_ = handler.handleFailure_;
handler.expects().handleFailure_(
TypeOf.isA(Error), callbackId, transactionId);
db.doExecute(statements, callbackId, transactionId);
mockControl.verify();
}
function testDoCommit() {
var transactionId = 10;
var db = buildDatabase();
var type = google.wspl.gears.DbWorker.CommandTypes.COMMIT;
wp.expects().sendMessage(TypeOf.isA(Object), workerId).andStub(function() {
assertEquals('wrong message type', type, arguments[0].type);
assertEquals('missing trans id', transactionId, arguments[0].transactionId);
});
db.doCommit(transactionId);
mockControl.verify();
}
function testSynchronousCommit() {
var db = buildSyncDatabase();
dbMock.expects().execute('COMMIT');
db.doCommit(1);
mockControl.verify();
}
function testDoRollback() {
var transactionId = 10;
var db = buildDatabase();
var type = google.wspl.gears.DbWorker.CommandTypes.ROLLBACK;
wp.expects().sendMessage(TypeOf.isA(Object), workerId).andStub(function() {
assertEquals('wrong message type', type, arguments[0].type);
assertEquals('missing trans id', transactionId, arguments[0].transactionId);
});
db.doRollback(transactionId);
mockControl.verify();
}
function testSynchronousRollback() {
var db = buildSyncDatabase();
dbMock.expects().execute('ROLLBACK');
db.doRollback(1);
mockControl.verify();
}
function testHandleCommit() {
var db = buildDatabase();
db.transactions_[5] = {transaction: 'tx', callback: callbackMock};
callbackMock.expects().onSuccess();
db.handleCommit_(5);
assertUndefined('failed to remove transaction', db.transactions_[5]);
mockControl.verify();
}
function testHandleRollback() {
var db = buildDatabase();
db.transactions_[5] = {
transaction: 'tx',
callback: callbackMock,
error: Error('error')
};
callbackMock.expects().onFailure(TypeOf.isA(Error)).andStub(function() {
assertEquals('did not pass error', 'error', arguments[0].message);
});
db.handleRollback_(5);
assertUndefined('failed to remove transaction', db.transactions_[5]);
mockControl.verify();
}
function testHandleResult() {
var db = buildDatabase();
transMock.expects().success(TypeOf.isA(google.wspl.gears.ResultSet),
22).andStub(function() {
assertEquals('result not set', 'result1', arguments[0].resultArray_);
});
transMock.expects().success(TypeOf.isA(google.wspl.gears.ResultSet),
22).andStub(function() {
assertEquals('result not set', 'result2', arguments[0].resultArray_);
});
db.transactions_[5] = {transaction: transMock, callback: 'cb'};
db.handleResult_(['result1', 'result2'], 22, 5);
mockControl.verify();
}
function testHandleFailure() {
var db = buildDatabase();
transMock.expects().failure(TypeOf.isA(Error), 22).andStub(function() {
assertEquals('error not set', 'error', arguments[0].message);
});
db.transactions_[5] = {transaction: transMock, callback: 'cb'};
db.handleFailure_(Error('error'), 22, 5);
assertEquals('failed to save error', 'error',
db.transactions_[5].error.message);
mockControl.verify();
}
function testHandleStarted() {
var db = buildDatabase();
var type = google.wspl.gears.DbWorker.CommandTypes.OPEN;
wp.expects().sendMessage(TypeOf.isA(Object), workerId).andStub(function() {
assertEquals('wrong message type', type, arguments[0].type);
assertEquals('wrong db userId', 'userId', arguments[0].userId);
assertEquals('wrong db name', 'name', arguments[0].name);
});
db.handleStarted_();
mockControl.verify();
}
function testHandleStarted_afterTimeout() {
var db = buildDatabase();
db.handleTimeout_();
// This should do nothing.
db.handleStarted_();
mockControl.verify();
}
function testHandleTimeout() {
var db = buildDatabase();
db.handleTimeout_();
mockControl.verify();
}
function testHandleOpenSuccessful() {
var db = buildDatabase();
db.handleOpenSuccessful_();
assertTrue('Worker should be ready.', db.workerReady_);
mockControl.verify();
}
function testHandleOpenFailed() {
var db = buildDatabase();
db.handleOpenFailed_('error');
mockControl.verify();
}
</script>
</body>
</html>

View file

@ -0,0 +1,203 @@
/*
Copyright 2009 Google 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.
*/
/**
* @fileoverview Generic Database API.
*
* A small set of classes to define how we interact with databases that
* can easily be implemented on top of HTML5.
*/
google.wspl.html5 = google.wspl.html5 || {};
/**
* Specification's default largest database size in HTML5 databases.
* @type{number}
*/
google.wspl.LARGEST_SUPPORTED_DATABASE = 1024 * 1024 * 4;
/**
* Creates an HTML5 Transaction object.
* @see google.wspl.Transaction#Transaction
*
* @constructor
* @extends google.wspl.Transaction
*
* @param {SQLTransaction} html5tx The HTML5 implementation of transactions.
*/
google.wspl.html5.Transaction = function(html5tx) {
this.tx_ = html5tx;
};
google.inherits(google.wspl.html5.Transaction, google.wspl.Transaction);
/**
* Runs an array of statements in a single database transaction.
* Invokes the onSuccess callback once for each succeesfully executed
* statement and
* once for the first failed statement.
*
* @param {Array.<google.wspl.Statement>} statements The statements to
* execute.
* @param {Object?} opt_callback An object containing onSuccess and onFailure
* handlers.
*/
google.wspl.html5.Transaction.prototype.executeAll = function(statements,
opt_callback) {
if (statements.length == 0) {
throw Error('Possibly silly attempt to execute empty statement list.');
}
var self = this;
for (var i = 0; i < statements.length; ++i) {
var statement = statements[i];
google.logger('SQL: ' + statement.sql + ' PARAMS: ' + statement.params);
this.tx_.executeSql(statement.sql, statement.params,
function(tx, result) {
if (opt_callback && opt_callback.onSuccess) {
var resultSet = new google.wspl.html5.ResultSet(result);
opt_callback.onSuccess(self, resultSet);
}
},
function(tx, error) {
if (opt_callback && opt_callback.onFailure) {
opt_callback.onFailure(error);
}
// fail the whole transaction if any step fails
return true;
});
}
};
/**
* @see google.wspl.Database#Database
* @param {string} name The name for this database.
* @param {window} opt_window A window object for dependency injection.
* @constructor
* @extends google.wspl.Database
*/
google.wspl.html5.Database = function(name, opt_window) {
/**
* Sequence number for transactions.
* @type {number}
* @private
*/
this.sequenceNum_ = 1;
/**
* Map of transactionIds -> transaction start time in millis.
* @type {Object}
* @private
*/
this.inflightTransactions_ = {};
var win = opt_window || window;
this.db_ = win.openDatabase(name, '',
name, google.wspl.LARGEST_SUPPORTED_DATABASE);
if (this.db_ == null) {
throw Error('The returned database was null.');
}
};
google.inherits(google.wspl.html5.Database, google.wspl.Database);
/**
* @see google.wspl.Database#createTransaction
*/
google.wspl.html5.Database.prototype.createTransaction = function(populate,
opt_callback) {
var transactionCallback = opt_callback || {
onSuccess: function() {},
onFailure: function() {}
};
var transactionId = this.sequenceNum_++;
var inflightTransactions = this.inflightTransactions_;
inflightTransactions[transactionId] = this.getCurrentTime();
this.db_.transaction(
function(tx) {
// Delete the transaction before the executing it because our
// definition of an 'in-flight' transaction is the time between
// when the request was made and when the database starts to
// execute the transaction.
delete inflightTransactions[transactionId];
populate(new google.wspl.html5.Transaction(tx));
},
function(error) {transactionCallback.onFailure(error);},
function() {transactionCallback.onSuccess();});
};
/**
* Determine if there is an in-flight database transaction that's older than
* the given time period.
* @param {number} olderThanMillis The time period.
* @return {boolean} True if the database has an in-flight transaction older
* than the given time period, false otherwise.
*/
google.wspl.html5.Database.prototype.hasInflightTransactions =
function(olderThanMillis) {
for (var transactionId in this.inflightTransactions_) {
var startTime = this.inflightTransactions_[transactionId];
if (this.getCurrentTime() - startTime > olderThanMillis) {
return true;
}
}
return false;
};
/**
* Returns the current time.
* @return {number} The current time in millis.
*/
google.wspl.html5.Database.prototype.getCurrentTime = function() {
// The iPhone does not support Date.now()
var d = new Date();
return d.getTime();
};
/**
* Creates an HTML5 ResultSet object.
* @see google.wspl.ResultSet#ResultSet
*
* @constructor
* @extends google.wspl.ResultSet
*
* @param {Object} html5_result The HTML5 implementation of result set.
*/
google.wspl.html5.ResultSet = function(html5_result) {
this.result_ = html5_result;
this.index_ = 0;
};
google.inherits(google.wspl.html5.ResultSet, google.wspl.ResultSet);
/**
* @see google.wspl.ResultSet#isValidRow
*/
google.wspl.html5.ResultSet.prototype.isValidRow = function() {
return this.index_ >= 0 && this.index_ < this.result_.rows.length;
};
/**
* @see google.wspl.ResultSet#next
*/
google.wspl.html5.ResultSet.prototype.next = function() {
this.index_ ++;
};
/**
* @see google.wspl.ResultSet#getRow
*/
google.wspl.html5.ResultSet.prototype.getRow = function() {
return this.result_.rows.item(this.index_);
};

View file

@ -0,0 +1,468 @@
<!DOCTYPE html>
<!--
Copyright 2009 Google 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.
-->
<html>
<head>
<title>Database wrapper tests</title>
<script type="text/javascript" src="../jsunit/app/jsUnitCore.js"></script>
<script type="text/javascript" src="jsmock.js"></script>
<script type="text/javascript" src="global_functions.js"></script>
<script type="text/javascript" src="dbwrapperapi.js"></script>
<script type="text/javascript" src="dbwrapper_html5.js"></script>
</head>
<body>
<script type='text/javascript'>
var mockControl;
var html5Tx;
var callbackMock;
var html5Window;
var html5Db;
var populateMock;
function setUp() {
mockControl = new MockControl();
html5Tx = mockControl.createMock({executeSql: function(){}});
callbackMock = mockControl.createMock({onSuccess: function(){},
onFailure: function(){}});
html5Window = mockControl.createMock({openDatabase: function(){}});
html5Db = mockControl.createMock({transaction: function(){}});
populateMock = mockControl.createMock({populate: function(){}});
dbTx = mockControl.createMock({execute: function(){}, executeAll:
function(){}});
dbRows = mockControl.createMock({item: function(){}});
}
function testConstructTransaction() {
var trans = new google.wspl.html5.Transaction('foo');
assertEquals('transaction instance not set correctly', 'foo', trans.tx_);
}
function testTransactionExecuteAll() {
var trans = new google.wspl.html5.Transaction(html5Tx);
try {
trans.executeAll([], callbackMock);
fail('Should never get here');
} catch(e) {
assertEquals(
'did not exception fault on empty statement list',
'Error: Possibly silly attempt to execute empty statement list.',
e.toString());
}
mockControl.verify();
}
/*
The sequence of digits at the end of the test names indicate as boolean
variables the success or failure of each statement.
*/
function testTransactionExecuteAll_1() {
var stat1 = new google.wspl.Statement('stat1');
var trans = new google.wspl.html5.Transaction(html5Tx);
var successCallback = null;
var failureCallback = null;
html5Tx.expects().executeSql('stat1', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback = arguments[2];
});
callbackMock.expects().onSuccess(trans,
TypeOf.isA(google.wspl.html5.ResultSet)).andStub(function() {
assertEquals('result not returned', 'resultset', arguments[1].result_)});
trans.executeAll([stat1], callbackMock);
successCallback(html5Tx, 'resultset');
mockControl.verify();
}
function testTransactionExecuteAll_11() {
var stat1 = new google.wspl.Statement('stat1');
var stat2 = new google.wspl.Statement('stat2', [1]);
var trans = new google.wspl.html5.Transaction(html5Tx);
var successCallback1 = null;
var successCallback2 = null;
var failureCallback = null;
html5Tx.expects().executeSql('stat1', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback1 = arguments[2];
});
html5Tx.expects().executeSql('stat2', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback2 = arguments[2];
var params = arguments[1];
assertEquals('incorrect params to sql', 1, params[0]);
});
callbackMock.expects().onSuccess(trans,
TypeOf.isA(google.wspl.html5.ResultSet)).andStub(function() {
assertEquals('result not returned', 'resultset1', arguments[1].result_)});
callbackMock.expects().onSuccess(trans,
TypeOf.isA(google.wspl.html5.ResultSet)).andStub(function() {
assertEquals('result not returned', 'resultset2', arguments[1].result_)});
trans.executeAll([stat1, stat2], callbackMock);
successCallback1(html5Tx, 'resultset1');
successCallback2(html5Tx, 'resultset2');
mockControl.verify();
}
function testTransactionExecuteAll_111() {
var stat1 = new google.wspl.Statement('stat1');
var stat2 = new google.wspl.Statement('stat2', [1]);
var stat3 = new google.wspl.Statement('stat3', [2, 3]);
var trans = new google.wspl.html5.Transaction(html5Tx);
var successCallback1 = null;
var successCallback2 = null;
var successCallback3 = null;
var failureCallback = null;
html5Tx.expects().executeSql('stat1', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback1 = arguments[2];
});
html5Tx.expects().executeSql('stat2', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback2 = arguments[2];
var params = arguments[1];
assertEquals('incorrect params to sql', 1, params[0]);
});
html5Tx.expects().executeSql('stat3', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback3 = arguments[2];
var params = arguments[1];
assertEquals('incorrect params to sql', 2, params[0]);
assertEquals('incorrect params to sql', 3, params[1]);
});
callbackMock.expects().onSuccess(trans,
TypeOf.isA(google.wspl.html5.ResultSet)).andStub(function() {
assertEquals('result not returned', 'resultset1', arguments[1].result_)});
callbackMock.expects().onSuccess(trans,
TypeOf.isA(google.wspl.html5.ResultSet)).andStub(function() {
assertEquals('result not returned', 'resultset2', arguments[1].result_)});
callbackMock.expects().onSuccess(trans,
TypeOf.isA(google.wspl.html5.ResultSet)).andStub(function() {
assertEquals('result not returned', 'resultset3', arguments[1].result_)});
trans.executeAll([stat1, stat2, stat3], callbackMock);
successCallback1(html5Tx, 'resultset1');
successCallback2(html5Tx, 'resultset2');
successCallback3(html5Tx, 'resultset3');
mockControl.verify();
}
function testTransactionExecuteAll_noCallback_111() {
var stat1 = new google.wspl.Statement('stat1');
var stat2 = new google.wspl.Statement('stat2', [1]);
var stat3 = new google.wspl.Statement('stat3', [2, 3]);
var trans = new google.wspl.html5.Transaction(html5Tx);
var successCallback1 = null;
var successCallback2 = null;
var successCallback3 = null;
var failureCallback = null;
html5Tx.expects().executeSql('stat1', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback1 = arguments[2];
});
html5Tx.expects().executeSql('stat2', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(
function() { successCallback2 = arguments[2];
var params = arguments[1];
assertEquals('incorrect params to sql', 1, params[0]);
});
html5Tx.expects().executeSql('stat3', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(
function() { successCallback3 = arguments[2];
var params = arguments[1];
assertEquals('incorrect params to sql', 2, params[0]);
assertEquals('incorrect params to sql', 3, params[1]);
});
trans.executeAll([stat1, stat2, stat3]);
successCallback1(html5Tx, 'resultset1');
successCallback2(html5Tx, 'resultset2');
successCallback3(html5Tx, 'resultset3');
mockControl.verify();
}
function testTransactionExecuteAll_110() {
var stat1 = new google.wspl.Statement('stat1');
var stat2 = new google.wspl.Statement('stat2', [1]);
var stat3 = new google.wspl.Statement('stat3', [2, 3]);
var trans = new google.wspl.html5.Transaction(html5Tx);
var successCallback1 = null;
var successCallback2 = null;
var successCallback3 = null;
var failureCallback3 = null;
html5Tx.expects().executeSql('stat1', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback1 = arguments[2];
});
html5Tx.expects().executeSql('stat2', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback2 = arguments[2];
var params = arguments[1];
assertEquals('incorrect params to sql', 1, params[0]);
});
html5Tx.expects().executeSql('stat3', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback3 = arguments[2];
failureCallback3 = arguments[3];
var params = arguments[1];
assertEquals('incorrect params to sql', 2, params[0]);
assertEquals('incorrect params to sql', 3, params[1]);
});
callbackMock.expects().onSuccess(trans,
TypeOf.isA(google.wspl.html5.ResultSet)).andStub(function() {
assertEquals('result not returned', 'resultset1', arguments[1].result_)});
callbackMock.expects().onSuccess(trans,
TypeOf.isA(google.wspl.html5.ResultSet)).andStub(function() {
assertEquals('result not returned', 'resultset2', arguments[1].result_)});
callbackMock.expects().onFailure('error3');
trans.executeAll([stat1, stat2, stat3], callbackMock);
successCallback1(html5Tx, 'resultset1');
successCallback2(html5Tx, 'resultset2');
assertTrue('failure case callback not terminating transaction',
failureCallback3(html5Tx, 'error3'));
mockControl.verify();
}
function testTransactionExecute_nocallback() {
var stat1 = new google.wspl.Statement('stat1');
var trans = new google.wspl.html5.Transaction(html5Tx);
var successCallback = null;
var failureCallback = null;
html5Tx.expects().executeSql('stat1', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback = arguments[2];
});
trans.execute(stat1);
successCallback(html5Tx, 'resultset');
mockControl.verify();
}
function buildDatabase() {
html5Window.expects().openDatabase('name', '', 'name',
google.wspl.LARGEST_SUPPORTED_DATABASE).
andReturn(html5Db);
return new google.wspl.html5.Database('name', html5Window);
}
function testConstructDatabase() {
var db = buildDatabase();
mockControl.verify();
assertEquals('did not set db_ correctly', db.db_, html5Db);
}
function testConstructDatabase_null() {
html5Window.expects().openDatabase('name', '', 'name',
google.wspl.LARGEST_SUPPORTED_DATABASE).
andReturn(null);
try {
var db = google.wspl.html5.Database('name', html5Window);
fail('Should never get here');
} catch (e) {
if (e.isJsUnitException) {
throw e;
}
}
mockControl.verify();
}
function testConstructDatabase_undefined() {
html5Window.expects().openDatabase('name', '', 'name',
google.wspl.LARGEST_SUPPORTED_DATABASE).
andReturn(undefined);
try {
var db = google.wspl.html5.Database('name', html5Window);
fail('Should never get here');
} catch (e) {
if (e.isJsUnitException) {
throw e;
}
}
mockControl.verify();
}
function createTransactionCore() {
var db = buildDatabase();
var fun1 = null;
var failure = null;
var success = null;
html5Db.expects().transaction(TypeOf.isA(Function), TypeOf.isA(Function),
TypeOf.isA(Function)).andStub(function() {
fun1 = arguments[0];
failure = arguments[1];
success = arguments[2];
});
var tx = null;
var populateMock = function(txa) {
tx = txa;
};
db.createTransaction(populateMock, callbackMock);
fun1('transactionTest');
assertEquals('transaction not saved', 'transactionTest', tx.tx_);
assertEquals('did not set db_ correctly', db.db_, html5Db);
return {failure: failure, success: success};
}
function testCreateTransaction_success() {
cbs = createTransactionCore();
callbackMock.expects().onSuccess();
cbs.success('success');
mockControl.verify();
}
function testCreateTransaction_failure() {
cbs = createTransactionCore();
callbackMock.expects().onFailure('error');
cbs.failure('error');
mockControl.verify();
}
function testCreateTransaction_onlyPopulate() {
cbs = createTransactionCore();
mockControl.verify();
}
function testDatabaseExecute_nocallback() {
var db = buildDatabase();
dbTx.expects().execute('statementText', null);
db.execute('statementText');
}
function testDatabaseExecute_nocallback() {
var db = buildDatabase();
dbTx.expects().execute('statementText', null);
db.execute('statementText');
}
function testDatabaseExecuteAll() {
var db = buildDatabase();
dbTx.expects().executeAll(TypeOf.isA(Array));
}
function testResultSetNext() {
var res = {rows: {length: 4}};
var result = new google.wspl.html5.ResultSet(res);
for (var i = 0; i < 4; i++) {
assertTrue('expected valid row', result.isValidRow());
result.next();
}
assertFalse('expected invalid row', result.isValidRow());
}
function testResultSetIsValidRow() {
var res = {rows: {length: 0}};
var result = new google.wspl.html5.ResultSet(res);
assertFalse('expected invalid row', result.isValidRow());
res.rows.length = 1;
assertTrue('expected valid row', result.isValidRow());
result.next();
assertFalse('expected invalid row', result.isValidRow());
}
function testResultSetGetRow() {
var res = {rows: {
length: 3,
item: function(index) {
assertEquals('expected index of 1', 1, index);
return {h0: 'value0', h1: 'value1', h2: 'value2'};
}
}};
var result = new google.wspl.html5.ResultSet(res);
result.next();
var row = result.getRow();
assertEquals('first field is not valid', 'value0', row.h0);
assertEquals('first field is not valid', 'value1', row.h1);
assertEquals('first field is not valid', 'value2', row.h2);
}
function testHasInflightTransactions() {
var fakeTime = 1000;
var db = buildDatabase();
var dummyPopulate = function() {};
db.getCurrentTime = function() {return fakeTime;};
// Create a transaction, make sure it's kept track of.
var transId = db.sequenceNum_;
db.createTransaction(dummyPopulate);
assertEquals('There should be an inflight request.',
fakeTime, db.inflightTransactions_[transId]);
// Haven't advanced time, so hasInflightTransactions should return false.
assertFalse('Expected hasInflightTransactions=false.',
db.hasInflightTransactions(100));
// Advance time, now hasInflightTransactions should return true.
fakeTime += 10000;
var faqs = db.hasInflightTransactions(100);
assertTrue('Expected hasInflightTransactions=true.',
db.hasInflightTransactions(100));
// Now now have the transaction completed, hasInflightTransactions
// should return false.
delete db.inflightTransactions_[transId];
assertFalse('Expected hasInflightTransactions=false.',
db.hasInflightTransactions(100));
};
</script>
</body>
</html>

View file

@ -0,0 +1,202 @@
/*
Copyright 2009 Google 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.
*/
/**
* @fileoverview Generic Database API.
*
* A small set of classes to define how we interact with databases that can
* easily be implemented on top of HTML5 and Gears. The classes in this file
* should be extended to provide the missing method implementations and more
* sophisticated constructors where applicable.
*
*/
/**
* Constructs a Statement object. A Statement is an SQL statement paired
* with the parameters needed to execute it.
*
* @constructor
* @param {!string} sql The SQL statement.
* @param {Array.<Object>?} opt_params The parameters for the SQL statement.
*/
google.wspl.Statement = function(sql, opt_params) {
/**
* The SQL statement with '?' in place of parameters.
* @type {string}
*/
this.sql = sql;
/**
* The parameters to use with the SQL statement.
* @type {!Array}
*/
this.params = opt_params || [];
};
/**
* Returns a new statement object from the given statement with the parameters
* set as specified.
* @param {Array.<Object>} params The array of values for ? placeholders.
* @return {!google.wspl.Statement} The created Statement.
*/
google.wspl.Statement.prototype.createStatement = function(params) {
return new google.wspl.Statement(this.sql, params);
};
/**
* Constructs a Transaction object. Transaction objects
* group together a series of statements into a single atomic
* action on the database.
*
* @constructor
*/
google.wspl.Transaction = function() {
};
/**
* Takes a statement and an optional callback object and
* runs the statement on the database. The callback can be used to
* add more statements to the same transaction, or execute can be
* called repeatedly and the transactions will later execute in the
* order provided.
*
* @param {google.wspl.Statement} statement The statement to execute.
* @param {Object} opt_callback An object containing onSuccess and onFailure
* handlers.
*/
google.wspl.Transaction.prototype.execute = function(statement,
opt_callback) {
this.executeAll([statement], opt_callback);
};
/**
* Runs an array of statements in a single database transaction.
* Invokes the onSuccess callback once for each successfully executed
* statement and once for the first failed statement. The callback can be
* used to add more statements to the same transaction, or executeAll can
* be called repeatedly and each block of statements given will execute
* in the same order as the sequence of calls to executeAll.
*
* @param {Array.<google.wspl.Statement>} statements The statements to
* execute.
* @param {Object?} opt_callback An object containing onSuccess and onFailure
* handlers.
*/
google.wspl.Transaction.prototype.executeAll = function(statements,
opt_callback) {
throw Error('executeAll not implemented');
};
/**
* Constructs a Database object. Database objects are handles that allow
* access to a database (and create the corresponding database if it doesn't
* already exist). To open the database, pass the name of the needed
* database to the constructor, and then execute transactions on it using
* the execute method.
*
* @constructor
*/
google.wspl.Database = function() {
};
/**
* Creates a transaction object that can execute a series of SQL statements
* atomically.
*
* @param {Function} populate A callback to run execute calls on the
* transaction.
* @param {Object?} opt_callback An optional success/failure callback that is
* called when the entire transaction is finished executing.
*/
google.wspl.Database.prototype.createTransaction = function(populate,
opt_callback) {
throw Error('createTransaction not implemented');
};
/**
* Executes an array of statements on the database, invoking the optional
* callback after statement execution and the optional transactionCallback upon
* completion of the transaction.
*
* @param {google.wspl.Statement} statement the statement to execute
* @param {Object?} opt_callback object that defines onSuccess and onFailure
* @param {Object?} opt_transactionCallback object that defines onSuccess and
* onFailure
*/
google.wspl.Database.prototype.execute = function(statement,
opt_callback,
opt_transactionCallback) {
this.createTransaction(function(tx) {
tx.execute(statement, opt_callback);
}, opt_transactionCallback);
};
/**
* Executes an array of statements on the database, invoking the optional
* callback for each statement in the transaction. In the case of a statement
* failure, only the first failed statement will be reported and the transaction
* will be rolled back. This method invokes the optional transactionCallback
* upon completion of the transaction.
*
* @param {Array.<google.wspl.Statement>} statements the statements to execute
* @param {Object?} opt_callback object that defines onSuccess and onFailure
* @param {Object?} opt_transactionCallback object that defines onSuccess and
* onFailure
*/
google.wspl.Database.prototype.executeAll = function(statements,
opt_callback,
opt_transactionCallback) {
this.createTransaction(function(tx) {
tx.executeAll(statements, opt_callback);
}, opt_transactionCallback);
};
/**
* An immutable set of results that is returned from a single successful query
* on the database.
*
* @constructor
*/
google.wspl.ResultSet = function() {
};
/**
* Returns true if next() will advance to a valid row in the result set.
*
* @return {boolean} if next() will advance to a valid row in the result set
*/
google.wspl.ResultSet.prototype.isValidRow = function() {
throw Error('isValidRow not implemented');
};
/**
* Advances to the next row in the results.
*/
google.wspl.ResultSet.prototype.next = function() {
throw Error('next not implemented');
};
/**
* Returns the current row as an object with a property for each field returned
* by the database. The property will have the name of the column and the value
* of the cell.
*
* @return {Object} The current row
*/
google.wspl.ResultSet.prototype.getRow = function() {
throw Error('getRow not implemented');
};

View file

@ -0,0 +1,51 @@
<!DOCTYPE html>
<!--
Copyright 2009 Google 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.
-->
<html>
<head>
<title>Database wrapper api</title>
<script type="text/javascript" src="../jsunit/app/jsUnitCore.js"></script>
<script type="text/javascript" src="jsmock.js"></script>
<script type="text/javascript" src="global_functions.js"></script>
<script type="text/javascript" src="dbwrapperapi.js"></script>
</head>
<body>
<script type='text/javascript'>
function subStatementTest(stm) {
assertEquals('statement not set correctly', 'bar', stm.sql);
assertEquals('first param not set correctly', 1, stm.params[0]);
assertEquals('second param not set correctly', 2, stm.params[1]);
}
function testConstructStatement() {
var stm = new google.wspl.Statement('foo');
assertEquals('statement not set correctly', 'foo', stm.sql);
var stm = new google.wspl.Statement('bar', [1,2]);
subStatementTest(stm);
}
function testConstructStatementFromTemplate() {
var temp = new google.wspl.Statement('bar');
var stm = temp.createStatement([1,2]);
subStatementTest(stm);
}
</script>
</body>
</html>

View file

@ -0,0 +1,71 @@
/*
Copyright 2009 Google 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.
*/
/**
* @fileoverview A Gears implementation of dbwrapperapi ResultSet.
*/
/**
* Creates a Gears ResultSet object.
* @see google.wspl.ResultSet#ResultSet
*
* @constructor
* @extends google.wspl.ResultSet
* @param {!Array.<Object>} resultArray An array of hash objects where the
* column names in the query are used as members of the objects.
*/
google.wspl.gears.ResultSet = function(resultArray) {
google.wspl.ResultSet.call(this);
/**
* The result set as an array of hash objects.
* @type {!Array.<Object>}
* @private
*/
this.resultArray_ = resultArray;
};
google.inherits(google.wspl.gears.ResultSet, google.wspl.ResultSet);
/**
* The current record in the result set.
* @type {number}
* @private
*/
google.wspl.gears.ResultSet.prototype.current_ = 0;
/**
* @see google.wspl.ResultSet#isValidRow
* @inheritDoc
*/
google.wspl.gears.ResultSet.prototype.isValidRow = function() {
return this.current_ < this.resultArray_.length;
};
/**
* @see google.wspl.ResultSet#next
* @inheritDoc
*/
google.wspl.gears.ResultSet.prototype.next = function() {
this.current_++;
};
/**
* @see google.wspl.ResultSet#getRow
* @inheritDoc
*/
google.wspl.gears.ResultSet.prototype.getRow = function() {
return this.resultArray_[this.current_];
};

View file

@ -0,0 +1,86 @@
<!DOCTYPE html>
<!--
Copyright 2009 Google 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.
-->
<html>
<head>
<title>Gears resultset tests</title>
<script type="text/javascript" src="../jsunit/app/jsUnitCore.js"></script>
<script type="text/javascript" src="jsmock.js"></script>
<script type="text/javascript" src="global_functions.js"></script>
<script type="text/javascript" src="dbwrapperapi.js"></script>
<script type="text/javascript" src="gears_resultset.js"></script>
</head>
<body>
<script type='text/javascript'>
var resultArray;
function setUp() {
var obj1 = {
field1: 'a',
field2: 2,
field3: 'c'
};
var obj2 = {
field1: 'd',
field2: 4,
field3: 'f'
};
var obj3 = {
field1: 'g',
field2: 6,
field3: 'i'
};
resultArray = [obj1, obj2, obj3];
}
function testResultSetNext() {
var result = new google.wspl.gears.ResultSet(resultArray);
assertEquals('incorrect value for current', 0, result.current_);
result.next();
assertEquals('incorrect value for current', 1, result.current_);
result.next();
assertEquals('incorrect value for current', 2, result.current_);
}
function testResultSetIsValidRow() {
var result = new google.wspl.gears.ResultSet(resultArray);
assertTrue('incorrect return from isValidRow', result.isValidRow());
result.next();
assertTrue('incorrect return from isValidRow', result.isValidRow());
result.next();
assertTrue('incorrect return from isValidRow', result.isValidRow());
result.next();
assertFalse('incorrect return from isValidRow', result.isValidRow());
}
function testResultSetGetRow() {
var result = new google.wspl.gears.ResultSet(resultArray);
result.next();
var row = result.getRow();
assertEquals('first field is not valid', 'd', row.field1);
assertEquals('first field is not valid', 4, row.field2);
assertEquals('first field is not valid', 'f', row.field3);
}
</script>
</body>
</html>

View file

@ -0,0 +1,196 @@
/*
Copyright 2009 Google 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.
*/
/**
* @fileoverview A Gears implementation of dbwrapperapi Transaction.
*/
/**
* Creates a Gears Transaction object.
* @see google.wspl.ResultSet#ResultSet
*
* @constructor
* @extends google.wspl.Transaction
*
* @param {number} id The unique id for this transaction
* @param {google.wspl.gears.Database} db The Gears implementation of the
* dbwrapperapi database
*/
google.wspl.gears.Transaction = function(id, db) {
google.wspl.Transaction.call(this);
/**
* The unique id for this transaction.
* @type {number}
* @private
*/
this.id_ = id;
/**
* The Gears implementation of the dbwrapperapi database.
* @type {google.wspl.gears.Database}
* @private
*/
this.db_ = db;
/**
* A map of statements, callback, and current statement.
* @type {Object}
* @private
*/
this.activeExecutes_ = {};
};
google.inherits(google.wspl.gears.Transaction, google.wspl.Transaction);
/**
* The number of active executes.
* @type {number}
* @private
*/
google.wspl.gears.Transaction.prototype.numActiveExecutes_ = 0;
/**
* The id for the next call to execute. Incremented after use.
* @type {number}
* @private
*/
google.wspl.gears.Transaction.prototype.nextCallbackId_ = 1;
/**
* Whether the transaction should be rolled back or not. This property is set
* to true when a statement fails.
* @type {boolean}
* @private
*/
google.wspl.gears.Transaction.prototype.needsRollback_ = false;
/**
* Begins a new transaction with the Gears database. Commits the transaction if
* all calls to executeAll for this transaction have finished receiving
* callbacks. Rolls the transaction back if a statement failed.
*
* @see google.wspl.Transaction#executeAll
* @inheritDoc
*/
google.wspl.gears.Transaction.prototype.executeAll = function(statements,
opt_callback) {
if (statements.length == 0) {
throw Error('Possibly silly attempt to execute empty statement list.');
}
if (this.numActiveExecutes_ == 0) {
this.db_.doBegin(this.id_);
}
this.numActiveExecutes_++;
var callbackId = this.nextCallbackId_++;
var callback = opt_callback || {
onSuccess : function() {},
onFailure : function() {}
};
this.activeExecutes_[callbackId] = {
statements: statements,
currentStatement: 0,
callback: callback
};
this.db_.doExecute(statements, callbackId, this.id_);
};
/**
* Invokes onSuccess on the specified callback.
*
* @param {google.wspl.ResultSet} result The result of a successful statement
* @param {number} callbackId The callback to invoke
*/
google.wspl.gears.Transaction.prototype.success = function(result,
callbackId) {
if (!this.needsRollback_) {
var activeExecute = this.activeExecutes_[callbackId];
activeExecute.callback.onSuccess(this, result);
}
this.endStatement_(callbackId);
};
/**
* Invokes onFailure on the specified callback.
*
* @param {Error} error The error of an unsuccessful statement
* @param {number} callbackId The callback to invoke
*/
google.wspl.gears.Transaction.prototype.failure = function(error,
callbackId) {
if (!this.needsRollback_) {
this.needsRollback_ = true;
var activeExecute = this.activeExecutes_[callbackId];
activeExecute.callback.onFailure(error);
}
this.endStatement_(callbackId);
};
/**
* Handles clean up for the end of a single execution.
*
* @param {number} callbackId The callback to clean up.
* @private
*/
google.wspl.gears.Transaction.prototype.endStatement_ = function(callbackId) {
var activeExecute = this.activeExecutes_[callbackId];
var statements = activeExecute.statements;
var currentStatement = ++activeExecute.currentStatement;
if (currentStatement == statements.length) {
this.endExecute_(callbackId);
}
};
/**
* Handles clean up for the end of a call to executeAll. Performs a commit or
* rollback if this is the last active execute to clean up.
*
* @param {number} callbackId The callback to clean up
* @private
*/
google.wspl.gears.Transaction.prototype.endExecute_ = function(callbackId) {
delete this.activeExecutes_[callbackId];
this.numActiveExecutes_--;
if (!this.isExecuting()) {
this.endTransaction_();
}
};
/**
* Instructs the worker to commit the transaction or roll it back if a failure
* occurred and a rollback is required.
*
* @private
*/
google.wspl.gears.Transaction.prototype.endTransaction_ = function() {
if (this.needsRollback_) {
this.db_.doRollback(this.id_);
} else {
this.db_.doCommit(this.id_);
}
};
/**
* @return {boolean} True if the transaction has statements executing, false
* otherwise.
*/
google.wspl.gears.Transaction.prototype.isExecuting = function() {
return this.numActiveExecutes_ > 0;
};

View file

@ -0,0 +1,221 @@
<!DOCTYPE html>
<!--
Copyright 2009 Google 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.
-->
<html>
<head>
<title>Gears transaction tests</title>
<script type="text/javascript" src="../jsunit/app/jsUnitCore.js"></script>
<script type="text/javascript" src="jsmock.js"></script>
<script type="text/javascript" src="global_functions.js"></script>
<script type="text/javascript" src="dbwrapperapi.js"></script>
<script type="text/javascript" src="gears_resultset.js"></script>
<script type="text/javascript" src="gears_transaction.js"></script>
</head>
<body>
<script type='text/javascript'>
var mockControl;
var callbackMock;
var dbMock;
function setUp() {
mockControl = new MockControl();
callbackMock = mockControl.createMock({
onSuccess: function(){},
onFailure: function(){}
});
dbMock = mockControl.createMock({
doBegin: function(){},
doCommit: function(){},
doRollback: function(){},
doExecute: function(){}
});
}
function testConstructTransaction() {
var trans = new google.wspl.gears.Transaction(10, 'foo');
assertEquals('foo', trans.db_);
assertEquals(10, trans.id_);
}
function testExecuteAll_noStatements() {
var trans = new google.wspl.gears.Transaction(27, dbMock);
try {
trans.executeAll([], callbackMock);
fail('Should never get here');
} catch(e) {
if (e.isJsUnitException) {
throw e;
}
assertEquals('did not exception fault on empty statement list',
'Error: Possibly silly attempt to execute empty statement list.',
e.toString());
}
mockControl.verify();
}
function testExecuteAll_noCallback() {
var stat1 = new google.wspl.Statement('stat1');
var stat2 = new google.wspl.Statement('stat2', [1]);
var stat3 = new google.wspl.Statement('stat3', [2, 3]);
var transactionId = 27;
var callbackId = 1;
var trans = new google.wspl.gears.Transaction(transactionId, dbMock);
dbMock.expects().doBegin(transactionId);
dbMock.expects().doExecute([stat1, stat2, stat3], callbackId, transactionId);
dbMock.expects().doCommit(transactionId);
trans.executeAll([stat1, stat2, stat3]);
trans.success('resultset1', callbackId);
trans.success('resultset2', callbackId);
trans.success('resultset3', callbackId);
}
function testExecuteAll_success() {
var stat1 = new google.wspl.Statement('stat1');
var stat2 = new google.wspl.Statement('stat2', [1]);
var stat3 = new google.wspl.Statement('stat3', [2, 3]);
var transactionId = 27;
var callbackId = 1;
var trans = new google.wspl.gears.Transaction(transactionId, dbMock);
dbMock.expects().doBegin(transactionId);
dbMock.expects().doExecute([stat1, stat2, stat3], callbackId, transactionId);
callbackMock.expect().onSuccess(trans, 'resultset1');
callbackMock.expect().onSuccess(trans, 'resultset2');
callbackMock.expect().onSuccess(trans, 'resultset3');
dbMock.expects().doCommit(transactionId);
trans.executeAll([stat1, stat2, stat3], callbackMock);
trans.success('resultset1', callbackId);
trans.success('resultset2', callbackId);
trans.success('resultset3', callbackId);
mockControl.verify();
}
function testExecuteAll_failure() {
var stat1 = new google.wspl.Statement('stat1');
var stat2 = new google.wspl.Statement('stat2', [1]);
var stat3 = new google.wspl.Statement('stat3', [2, 3]);
var stat4 = new google.wspl.Statement('stat4', [4, 5]);
var transactionId = 27;
var callbackId = 1;
var trans = new google.wspl.gears.Transaction(transactionId, dbMock);
dbMock.expects().doBegin(transactionId);
dbMock.expects().doExecute([stat1, stat2, stat3, stat4], callbackId,
transactionId);
callbackMock.expect().onSuccess(trans, 'resultset1');
callbackMock.expect().onFailure('sql error');
dbMock.expects().doRollback(transactionId);
trans.executeAll([stat1, stat2, stat3, stat4], callbackMock);
trans.success('resultset1', callbackId);
trans.failure('sql error', callbackId);
// These should do nothing.
trans.success('resultset3', callbackId);
trans.failure('sql error', callbackId);
mockControl.verify();
}
function testExecuteAll_multipleCalls() {
var stat1 = new google.wspl.Statement('stat1');
var stat2 = new google.wspl.Statement('stat2', [1]);
var stat3 = new google.wspl.Statement('stat3', [2, 3]);
var stat4 = new google.wspl.Statement('stat4', [4, 5]);
var transactionId = 27;
var callbackId_1 = 1;
var callbackId_2 = 2;
var callbackId_3 = 3;
var trans = new google.wspl.gears.Transaction(transactionId, dbMock);
dbMock.expects().doBegin(transactionId);
dbMock.expects().doExecute([stat1, stat2], callbackId_1, transactionId);
dbMock.expects().doExecute([stat3], callbackId_2, transactionId);
callbackMock.expects().onSuccess(trans, 'resultset1').andStub(function() {
trans.executeAll([stat4], callbackMock);
});
dbMock.expects().doExecute([stat4], callbackId_3, transactionId);
callbackMock.expects().onSuccess(trans, 'resultset2').andStub(function() {});
callbackMock.expects().onSuccess(trans, 'resultset3');
callbackMock.expects().onSuccess(trans, 'resultset4');
dbMock.expects().doCommit(transactionId);
trans.executeAll([stat1, stat2], callbackMock);
trans.executeAll([stat3], callbackMock);
trans.success('resultset1', callbackId_1);
trans.success('resultset2', callbackId_1);
trans.success('resultset3', callbackId_2);
trans.success('resultset4', callbackId_3);
mockControl.verify();
}
function testOnSuccess() {
var transactionId = 27;
var callbackId = 3;
var trans = new google.wspl.gears.Transaction(transactionId, dbMock);
callbackMock.expects().onSuccess(trans, 'result2');
callbackMock.expects().onSuccess(trans, 'result3');
dbMock.expects().doCommit(transactionId);
trans.numActiveExecutes_ = 1;
trans.activeExecutes_[callbackId] = {
statements: ['s1', 's2', 's3'],
currentStatement: 1,
callback: callbackMock
};
trans.success('result2', callbackId);
trans.success('result3', callbackId);
assertUndefined('activeExecute not removed',
trans.activeExecutes_[callbackId]);
mockControl.verify();
}
function testOnFailure() {
var transactionId = 27;
var callbackId = 5;
callbackMock.expects().onFailure(TypeOf.isA(Error)).andStub(function() {
assertEquals('error not returned', 'error', arguments[0].message);
});
dbMock.expects().doRollback(transactionId);
var trans = new google.wspl.gears.Transaction(transactionId, dbMock);
trans.numActiveExecutes_ = 1;
trans.activeExecutes_[callbackId] = {
statements: ['s1', 's2', 's3'],
currentStatement: 1,
callback: callbackMock
};
trans.failure(Error('error'), callbackId);
trans.success('result3', callbackId);
assertUndefined('activeExecute not removed',
trans.activeExecutes_[callbackId]);
mockControl.verify();
}
</script>
</body>
</html>

View file

@ -0,0 +1,94 @@
/*
Copyright 2009 Google 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.
*/
/**
* @fileoverview Some simple utilities for supporting Gears usage.
*/
google.wspl.GearsUtils = google.wspl.GearsUtils || {};
/**
* Returns an array of hash objects, one per row in the result set,
* where the column names in the query are used as the members of
* the object.
*
* @param {GearsResultSet} rs the result set returned by execute.
* @return {Array.<Object>} An array containing hashes. Returns an empty
* array if there are no matching rows.
*/
google.wspl.GearsUtils.resultSetToObjectArray = function(rs) {
var rv = [];
if (rs) {
var cols = rs['fieldCount']();
var colNames = [];
for (var i = 0; i < cols; i++) {
colNames.push(rs['fieldName'](i));
}
while (rs['isValidRow']()) {
var h = {};
for (var i = 0; i < cols; i++) {
h[colNames[i]] = rs['field'](i);
}
rv.push(h);
rs['next']();
}
}
return rv;
};
/**
* Maximum file name length.
* @type {number}
* @private
*/
google.wspl.GearsUtils.MAX_FILE_NAME_LENGTH_ = 64;
/**
* Ensures that the given dbName is safe to use as a Gears database name.
* @type {!string} dbName
* @return {!string} The sanitized name.
* @private
*/
google.wspl.GearsUtils.makeSafeFileName_ = function(dbName) {
var sanitizedFileName = dbName.replace(/[^a-zA-Z0-9\.\-@_]/g, '');
if (sanitizedFileName.length <=
google.wspl.GearsUtils.MAX_FILE_NAME_LENGTH_) {
return sanitizedFileName;
} else {
return sanitizedFileName.substring(0,
google.wspl.GearsUtils.MAX_FILE_NAME_LENGTH_);
}
};
/**
* Opens a Gears Database using the provided userid and name.
* @param {string} userId The user to which the database belongs.
* @param {string} name The database's name.
* @param {GearsDatabase} db The Gears database to open.
* @param {function(string)} opt_logger A logger function for writing
* messages.
* @return {GearsDatabase} The open GearsDatabase object.
*/
google.wspl.GearsUtils.openDatabase = function(userId, name, db,
opt_logger) {
var dbId = userId + '-' + name;
var safeDbId = google.wspl.GearsUtils.makeSafeFileName_(dbId);
if (opt_logger && dbId != safeDbId) {
opt_logger('database name ' + dbId + '->' + safeDbId);
}
db.open(safeDbId);
return db;
};

View file

@ -0,0 +1,84 @@
<!DOCTYPE html>
<!--
Copyright 2009 Google 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.
-->
<html>
<head>
<title>Gears Utilities Tests</title>
<script type="text/javascript" src="../jsunit/app/jsUnitCore.js"></script>
<script type="text/javascript" src="jsmock.js"></script>
<script type="text/javascript" src="global_functions.js"></script>
<script type="text/javascript" src="gearsutils.js"></script>
</head>
<body>
<script type='text/javascript'>
var mockControl;
var mockGearsDb;
var mockLogger;
function setUp() {
mockControl = new MockControl();
mockGearsDb = mockControl.createMock({
execute: function(){},
open: function(){},
close: function(){},
remove: function(){}
});
mockLogger = mockControl.createMock({
info: function(){}
});
}
function testMakeSafeFileName() {
assertEquals('simple name wrongly modified', 'ABCDEFGHIJKLMNOPQRSTUVXYZ',
google.wspl.GearsUtils.makeSafeFileName_('ABCDEFGHIJKLMNOPQRSTUVXYZ'));
assertEquals('simple name wrongly modified', 'abcdefghijklmnopqrstuvxyz',
google.wspl.GearsUtils.makeSafeFileName_('abcdefghijklmnopqrstuvxyz'));
assertEquals('simple name wrongly modified', '.-@_',
google.wspl.GearsUtils.makeSafeFileName_('.-@_'));
var longName = 'abcdefghijklmnopqrstuvxyz' + 'abcdefghijklmnopqrstuvxyz' +
'abcdefghijklmnopqrstuvxyz';
assertEquals('long name not truncated',
google.wspl.GearsUtils.MAX_FILE_NAME_LENGTH_,
google.wspl.GearsUtils.makeSafeFileName_(longName).length);
assertEquals('forbidden character not stripped', 'abc',
google.wspl.GearsUtils.makeSafeFileName_('a#b$()//c'));
}
function testCreateDatabase_success() {
mockGearsDb.expects().open('hello-there');
google.wspl.GearsUtils.openDatabase('hello', 'there', mockGearsDb,
mockLogger);
mockControl.verify();
}
function testCreateDatabase_invalidname() {
mockLogger.expects().info('database name foo/bar-ranch->foobar-ranch');
mockGearsDb.expects().open('foobar-ranch');
assertEquals('bad return value', mockGearsDb,
google.wspl.GearsUtils.openDatabase('foo/bar', 'ranch',
mockGearsDb, mockLogger.info));
mockControl.verify();
}
</script>
</body>
</html>

View file

@ -0,0 +1,72 @@
/*
Copyright 2009 Google 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.
*/
/**
* @fileoverview Global function implementations used for running the
* web storage portability layer code outside of the Google internal
* development environment.
*
* Include this file only once.
*
*/
/**
* Namespace object.
* @type {Object}
*/
var google = google || {};
google.wspl = google.wspl || {};
google.wspl.gears = google.wspl.gears || {};
/**
* Inherit the prototype methods from one constructor into another.
* @param {Function} childConstructor Child class.
* @param {Function} parentConstructor Parent class.
*/
google.inherits = function(childConstructor, parentConstructor) {
function tempConstructor() {};
tempConstructor.prototype = parentConstructor.prototype;
childConstructor.prototype = new tempConstructor();
childConstructor.prototype.constructor = childConstructor;
};
/**
* Binds a context object to the function.
* @param {Function} fn The function to bind to.
* @param {Object} context The "this" object to use when the function is run.
* @return {Function} A partially-applied form of fn.
*/
google.bind = function(fn, context) {
return function() {
return fn.apply(context, arguments);
};
};
/**
* Null function used for callbacks.
* @type {Function}
*/
google.nullFunction = function() {};
/**
* Simple logging facility.
* @param {string} msg A message to write to the console.
*/
google.logger = function(msg) {
// Uncomment the below to get log messages
// May require firebug enabled to work in FireFox.
// window.console.info(msg);
};

View file

@ -0,0 +1,347 @@
<!DOCTYPE html>
<!--
Copyright 2009 Google 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.
-->
<!--
A simple application showing the use of the web storage portability layer
code and cache pattern for offline web application.
-->
<html>
<head>
<title>SimpleNotes Demo</title>
<link href="styles.css" rel="stylesheet" type="text/css" />
<script src="../global_functions.js" language="javascript"
type="text/javascript"></script>
<script src="../dbwrapperapi.js" language="javascript"
type="text/javascript"></script>
<script src="../dbwrapper_html5.js" language="javascript"
type="text/javascript"></script>
<script src="../databasefactory.js" language="javascript"
type="text/javascript"></script>
<script src="template.js" language="javascript"
type="text/javascript"></script>
<script src="simplenotes.js" language="javascript"
type="text/javascript"></script>
</head>
<body>
<div id="list-note" class="screen list-note-screen" >
<div class="title-bar">Note List</div>
<div class="top command-bar">
<button id="view-note-new">New</button>
</div>
<div class="status-bar">status bar</div>
<div class="list-note-container-empty" style="display:none;"></div>
<div class="list-note-container" style="display:none;">
<div class="list-note-item">
<a href="javascript:showNote(%noteKey%);">%subject%</a>
</div>
</div>
<div class="top command-bar">
<button id="view-note-new">New</button>
</div>
</div>
<div id="view-note" class="screen view-note-screen" style="display:none;">
<div class="title-bar">View Note</div>
<div class="top command-bar">
<button id="view-note-back">Back</button>
<button id="view-note-edit">Edit Note</button>
</div>
<div class="status-bar">status bar</div>
<div class="view-note-container" style="display:none;">
<div class="subject">%subject%</div>
<div class="body">%body%</div>
</div>
<div class="bottom command-bar">
<button id="view-note-back">Back</button>
<button id="view-note-edit">Edit Note</button>
</div>
</div>
<div id="edit-note" class="screen edit-note-screen" style="display:none;">
<div class="title-bar">Edit Note</div>
<div class="top command-bar">
<button id="edit-back-save">Save Note</button>
<button id="edit-back-revert">Revert Note</button>
</div>
<div class="status-bar">status bar</div>
<div>Subject:</div>
<textarea rows="1" style="width:100%;" id="edit-note-subject"></textarea>
<br>
<div>Note:</div>
<textarea rows="20" style="width:100%;" id="edit-note-body"></textarea>
<div class="bottom command-bar">
<button id="edit-back-save">Save Note</button>
<button id="edit-back-revert">Revert Note</button>
</div>
</div>
</body>
<script>
/**
* Tests the given DOM element for a specific CSS class.
* @param {Object} e A DOM element.
* @param {string} css A CSS class identifier to match.
* @return {boolean} true if the element has the CSS class.
*/
function hasCssClass(e, css) {
return e && e.className && e.className.search(css + '( |$)') > -1;
};
/**
* Finds the parent element of a given element that has
* the specified CSS class.
* @param {Object} e A DOM element.
* @param {string} css A CSS class identifier to match.
* @param {Object} opt_stop A parent DOM element to stop at.
* @return {Object} A DOM element parent of e or null.
*/
function findParentOfClass(e, css, opt_stop) {
var stop = opt_stop || document;
while (e && e != stop && !hasCssClass(e, css)) {
e = e.parentElement;
}
return hasCssClass(e, css) ? e : null;
};
/**
* Navigates to the note creation screen.
*/
function editNote(event) {
google.logger('editNote: <' + currentNote.subject + '>');
hideBlock('#edit-note .status-bar');
var subject = document.getElementById('edit-note-subject');
var body = document.getElementById('edit-note-body');
subject.value = currentNote.subject;
body.value = currentNote.body;
google.logger('end of editNote');
}
/**
* Saves a note.
*/
function saveNote(event) {
google.logger('saveNote: do database or other activity here');
// Copy contents from fields and save.
var subject = document.getElementById('edit-note-subject');
var body = document.getElementById('edit-note-body');
cache.applyUiChange(currentNote.noteKey, subject.value,
body.value, showNote);
google.logger('saveNote');
}
/**
* Abandons the note.
*/
function abandonNote(event) {
google.logger('abandonNote: do database or other activity here');
showNote(currentNote.noteKey);
google.logger('abandonNote');
}
/**
* Prepares to edit a new note.
*/
function newNote(event) {
google.logger('newNote: do database or other activity here');
alert('not implemented');
// grab the contents and inject into the database.
var subject = document.getElementById('edit-note-subject');
var body = document.getElementById('edit-note-body');
subject.value = '';
body.value = '';
google.logger('newNote');
}
/**
* Maps button names to customizer functions.
*/
var buttonHandlerSpecification = {
'view-note-new': ['edit-note', newNote],
'view-note-back': ['list-note', showNoteList],
'view-note-edit': ['edit-note', editNote],
'edit-back-save': ['view-note', saveNote],
'edit-back-revert': ['view-note', abandonNote]
};
/**
* Hides a block specified by a CSS selector and parent node.
* @param {string} selector A CSS selector string.
* @param {Object} opt_el A DOM element.
*/
function hideBlock(selector, opt_el) {
var el = opt_el || document;
var targetEl = el.querySelector(selector);
targetEl.style.display = 'none';
}
/**
* Shows a block specified by a CSS selector and parent node, inserting
* the specified string as the innerHTML property of the selected block.
* @param {string} selector A CSS selector
* @param {string} html String to assign to the innerHTML property.
* @param {Object} opt_el A DOM element.
*/
function showBlock(selector, html, opt_el) {
var el = opt_el || document;
var targetEl = el.querySelector(selector);
if (html) targetEl.innerHTML = html;
targetEl.style.display = 'block';
}
/**
* Handles all button events.
*/
function buttonHandler(event) {
google.logger('clicked on: ' + event.target.id);
var currentScreen = findParentOfClass(event.target, 'screen');
var nextScreen = document.getElementById(
buttonHandlerSpecification[event.target.id][0]);
var helperFunction = buttonHandlerSpecification[event.target.id][1];
// All button handlers have a common implementation.
// 1. hide current screen
currentScreen.style.display = 'none';
// 2. show new screen
nextScreen.style.display = 'block';
// 3. Activate status
showBlock('.status-bar', 'Working...', nextScreen);
// 4. Call customizer function (which will probably query the model
// stored in the cache.
helperFunction(event);
// 5. A (possibly asynchronous) callback from the customizer will
// finish the action by running the appropriate view function.
google.logger('end of button Handler');
}
/**
* Call back from a specific JavaScript url to show a specific
* note.
* @param {number} noteKey
* @return {boolean}
*/
function showNote(noteKey) {
hideBlock('#list-note');
showBlock('#view-note .status-bar', 'Working...');
showBlock('#view-note');
cache.getValues('fullnote', [noteKey], renderFullNote);
return false;
}
/**
* Generates HTML for a note and display it.
* @param {Object} oneNote
* @param {boolean} complete True if the note has been provided or
* if an updated note is arriving. (Necessary for future support for
* sub-note updates.)
*/
function renderFullNote(oneNote, complete) {
google.logger('renderFullNote');
showBlock('.view-note-container', noteViewTemplate.process(oneNote));
if (complete) {
hideBlock('#view-note .status-bar');
}
currentNote = oneNote;
google.logger('done renderFullNote');
}
/**
* Generates HTML for a note list and display it.
* @param {Array.<Object>} noteHeaders
* @param {boolean} complete True if all in-existence notes that lie
* in the query range have been provided even if less than what was
* requested.
*/
function renderNoteList(noteHeaders, complete) {
google.logger('renderNoteList: ' + noteHeaders.length);
if (noteHeaders.length == 0) {
showBlock('.list-note-container-empty', 'No notes');
} else {
var notes = noteHeaders.map(function(n) {
return noteListTemplate.process(n);
});
showBlock('.list-note-container', notes.join());
}
if (complete) {
hideBlock('#list-note .status-bar');
}
google.logger('done renderNoteList');
}
/**
* Query for an updated note list.
*/
function showNoteList() {
google.logger('showNoteList');
cache.getValues('list', [0,20], renderNoteList);
currentNote = undefined;
google.logger('end of showNoteList');
}
/**
* A constructor for the controller. Sets up the UI and
* queries the cache for initial notelist contents.
*/
function createController() {
google.logger('createController:');
var buttons = document.querySelectorAll('.command-bar button');
Array.prototype.forEach.call(buttons, function(b) {
b.addEventListener('click', buttonHandler);
});
google.logger('controller is live');
showNoteList();
}
google.logger('about to fetch the template');
var currentNote = undefined;
var noteListTemplate = new google.wspl.simplenotes.Template(
document.querySelector('.list-note-container').innerHTML);
var noteViewTemplate = new google.wspl.simplenotes.Template(
document.querySelector('.view-note-container').innerHTML);
// var noteEditTemplate = new google.wspl.simplenotes.Template();
google.logger('created the template');
// debugging code... tear out...
// test the template construction...
// showBlock('.list-note-container', noteListTemplate.process(
// {noteKey: 123, subject: 'hi rob'}));
google.logger('loaded database');
var cache = new google.wspl.simplenotes.Cache();
cache.startCache(createController);
google.logger('finished loading page');
</script>
</html>

View file

@ -0,0 +1,503 @@
/*
Copyright 2009 Google 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.
*/
/**
* @fileoverview A concrete example of the cache pattern for building an offline
* webapplication: a cache for SimpleNotes.
*/
google.wspl.simplenotes = google.wspl.simplenotes || {};
google.logger('start simplenotes.js');
/**
* Status keys for the write buffer.
* @enum {number}
*/
google.wspl.simplenotes.WriteBufferStates = {
/**
* The update is in flight to the server.
*/
INFLIGHT: 1,
/**
* The update needs to be (re)sent to the server but is not in flight.
*/
SEND: 2,
/**
* The update needs to be applied to the cached notes.
*/
REAPPLY: 8
};
/**
* Creates a SimpleNotes cache wrapping a backing database.
* @constructor
*/
google.wspl.simplenotes.Cache = function() {
this.dbms_ = google.wspl.DatabaseFactory.createDatabase(
'simple-notes', 'http://yourdomain/dbworker.js');
/**
* Cache directory is a two-tuple over a range. (Holes
* must be allowed to support delection.)
* This is the lower (inclusive) bound of the cached range.
* @type {number}
* @private
*/
this.start_ = -1;
/**
*
* This is the upper (inclusive) bound of the cached range.
* @type {number}
* @private
*/
this.end_ = -1;
/**
* Start of range of notes known to exist on server at time of last
* response.
* @param {number}
*/
this.serverStart_ = -1;
/**
* End of range of notes known to exist on server at time of last
* response.
* @param {number}
*/
this.serverEnd_ = -1;
/**
* Time of last refresh.
* @type {number}
* @private
*/
this.lastRefresh_ = -1;
/**
* Last missing query.
* @type {Object}
* @private
*/
this.lastMiss_ = undefined;
};
/**
* Interval between refreshes in milliseconds.
* @type {number}
* @private
*/
google.wspl.simplenotes.Cache.TIME_BETWEEN_REFRESH_ = 2000;
google.wspl.simplenotes.Cache.CREATE_CACHED_NOTES_ =
new google.wspl.Statement(
'CREATE TABLE IF NOT EXISTS cached_notes (' +
'noteKey INTEGER UNIQUE PRIMARY KEY,' +
'subject TEXT,' +
'body TEXT' +
');'
);
google.wspl.simplenotes.Cache.CREATE_WRITE_BUFFER_ =
new google.wspl.Statement(
'CREATE TABLE IF NOT EXISTS write_buffer (' +
'sequence INTEGER UNIQUE PRIMARY KEY AUTOINCREMENT,' +
'noteKey INTEGER,' +
'status INTEGER,' +
'subject TEXT,' +
'body TEXT' +
');'
);
google.wspl.simplenotes.Cache.DETERMINE_MIN_KEY_ =
new google.wspl.Statement(
'SELECT MIN(noteKey) as minNoteKey FROM cached_notes;');
google.wspl.simplenotes.Cache.DETERMINE_MAX_KEY_ =
new google.wspl.Statement(
'SELECT MAX(noteKey) as maxNoteKey FROM cached_notes;');
/**
* Builds a cache and writebuffer combination for notes and then
* invokes the given callback.
* @param {function) callback.
*/
google.wspl.simplenotes.Cache.prototype.startCache = function(callback) {
google.logger('startCache');
var statc = 0;
var self = this;
var perStatCallback = function(tx, result) {
google.logger('perStatCallback');
if (statc == 4) {
self.start_ = (result.isValidRow()) ? result.getRow().minNoteKey : -1;
self.serverStart_ = self.start_; // Temporary. Remove when server exists.
} else if (statc == 5) {
self.end_ = (result.isValidRow()) ? result.getRow().maxNoteKey : -1;
self.serverEnd_ = self.end_; // Temporary. Remove when server exists.
}
statc++;
};
this.dbms_.executeAll([
google.wspl.simplenotes.Cache.CREATE_CACHED_NOTES_,
google.wspl.simplenotes.Cache.CREATE_WRITE_BUFFER_,
google.wspl.simplenotes.Cache.CREATE_UPDATE_TRIGGER_,
google.wspl.simplenotes.Cache.CREATE_REPLAY_TRIGGER_,
google.wspl.simplenotes.Cache.DETERMINE_MIN_KEY_,
google.wspl.simplenotes.Cache.DETERMINE_MAX_KEY_],
{onSuccess: perStatCallback, onFailure: this.logError_},
{onSuccess: callback, onFailure: this.logError_});
google.logger('finished startCache');
};
/**
* Stub function to be replaced with a server communication.
* @param {Array.<Object>} updates Payload to send to server.
*/
google.wspl.simplenotes.Cache.prototype.sendXhrPost = function(updates) {
google.logger('Should dispatch XHR to server now.');
};
/**
* @type {google.wspl.Statement}
* @private
*/
google.wspl.simplenotes.Cache.LIST_CACHED_NOTES_ =
new google.wspl.Statement(
'SELECT noteKey, subject from cached_notes WHERE ' +
'noteKey >= ? AND ' +
'noteKey <= ? ' +
';'
);
/**
* Tests if the given range is stored in the cache.
* Note that this mechanism requires extension to handle the
* creation of new notes.
* @param {number} start Lower bound (inclusive) on range.
* @param {number} end Uppder bound (inclusive) on range.
* @private
*/
google.wspl.simplenotes.Cache.prototype.isCacheHit_ = function(start, end) {
return start >= this.start_ && end <= this.end_
};
/**
* Logs a possibly useful error message.
* @param {Object} error An error descriptor.
* @private
*/
google.wspl.simplenotes.Cache.prototype.logError_ = function(error) {
google.logger('Simple Notes Cache is sad: ' + error);
};
/**
* Queries the cache for a list of note headers.
* @param {number} start The lower id in a range of notes.
* @param {number} end The higher id in a range of notes.
* @param {function(Array.<Object>)} valuesCallback A function to call
* with the result set after the transaction has completed.
* @private
*/
google.wspl.simplenotes.Cache.prototype.getNoteList_ = function(start, end,
valuesCallback) {
var notes = [];
var accumulateResults = function(tx, result) {
for(; result.isValidRow(); result.next()) {
notes.push(result.getRow());
google.logger('pushed...');
}
};
var inTransactionGetNotes = function(tx) {
tx.execute(google.wspl.simplenotes.Cache.LIST_CACHED_NOTES_.
createStatement([start, end]), {
onSuccess: accumulateResults,
onFailure: this.logError_});
};
var hit = this.isCacheHit_(start, end);
this.dbms_.createTransaction(inTransactionGetNotes, {onSuccess: function() {
valuesCallback(notes, hit);
}, onFailure: this.logError_});
if (hit) {
this.fetchFromServer(this.start_, this.end_); // Refresh
} else {
this.fetchFromServer(Math.min(this.start_, start), Math.max(this.end_, end));
this.lastMiss_ = {callback: valuesCallback, start: start, end: end};
}
};
/**
* @type {google.wspl.Statement}
* @private
*/
google.wspl.simplenotes.Cache.GET_ONE_NOTE_ =
new google.wspl.Statement(
'SELECT noteKey, subject, body from cached_notes WHERE ' +
'noteKey = ? ' +
';'
);
/**
* Queries the cache for a list of note headers.
* @param {number} noteId The note to get from the cache.
* @param {function(Array.<Object>)} valuesCallback A function to call
* with the result set after the transaction has completed.
* @private
*/
google.wspl.simplenotes.Cache.prototype.getOneNote_ = function(noteId,
callback) {
var note;
this.dbms_.execute(google.wspl.simplenotes.Cache.GET_ONE_NOTE_.
createStatement([noteId]),
{onSuccess: function(tx, result) { note = result.getRow(); },
onFailure: this.logError_},
{onSuccess: function() { callback(note, true); },
onFailure: this.logError_});
};
/**
* Queries the cache for either a list of notes or a single note including
* its body.
* @param {string} type The kind of values desired: 'list' or 'fullnote'.
* @param {Array.<number>} query The query for the values.
* @param {function(Array.<Object>)} valuesCallback A function to call
* with the result set after the transaction has completed.
*/
google.wspl.simplenotes.Cache.prototype.getValues = function(type,
query, valuesCallback) {
// Reduce any query to what would be available from the server
query[0] = Math.max(this.serverStart_, query[0]);
query[1] = Math.min(this.serverEnd_, query[1]);
if (type == 'list') {
this.getNoteList_(query[0], query[1], valuesCallback);
} else if (type == 'fullnote') {
this.getOneNote_(query[0], valuesCallback);
}
};
/**
* SQL trigger to insert a new change from write buffer to
* cache.
* @private
*/
google.wspl.simplenotes.Cache.CREATE_UPDATE_TRIGGER_ =
new google.wspl.Statement(
'CREATE TRIGGER IF NOT EXISTS updateTrigger ' +
'AFTER INSERT ON write_buffer ' +
'BEGIN ' +
' REPLACE INTO cached_notes ' +
' SELECT noteKey, subject, body ' +
' FROM write_buffer WHERE status & 8 = 8; ' +
' UPDATE write_buffer SET status = status & ~ 8; ' +
'END;'
);
/**
* SQL trigger to replay changes from the write buffer to
* the cache.
* @private
*/
google.wspl.simplenotes.Cache.CREATE_REPLAY_TRIGGER_ =
new google.wspl.Statement(
'CREATE TRIGGER IF NOT EXISTS replayTrigger ' +
'AFTER UPDATE ON write_buffer WHEN NEW.status & 8 = 8 ' +
'BEGIN ' +
' REPLACE INTO cached_notes ' +
' SELECT noteKey, subject, body ' +
' FROM write_buffer ' +
' WHERE noteKey = NEW.noteKey ' +
' ORDER BY sequence ASC;' +
' UPDATE write_buffer SET status = status & ~ 8; ' +
'END;'
);
/**
* SQL statement to mark actions for replay.
*/
google.wspl.simplenotes.Cache.MARK_FOR_REPLAY =
new google.wspl.Statement(
'UPDATE write_buffer SET status = status | 8;');
/**
* SQL statement to insert notes updates.
* @type {!google.wspl.Statement}
* @private
*/
google.wspl.simplenotes.Cache.INSERT_UI_UPDATE_ =
new google.wspl.Statement(
'INSERT INTO write_buffer (' +
'noteKey, status, subject, body' +
') ' + 'VALUES(?,?,?,?);');
/**
* Updates the given entry and write a new write buffer entry.
* @param {number} noteKey
* @param {string} subject
* @param {string} body
* @param {function(number)} ackCallback
*/
google.wspl.simplenotes.Cache.prototype.applyUiChange = function(noteKey,
subject, body, ackCallback) {
var self = this;
var update = [noteKey, 2 | 8, subject, body];
var stat = google.wspl.simplenotes.Cache.INSERT_UI_UPDATE_.createStatement(
update);
this.dbms_.execute(stat, null, {onSuccess: function() {
google.logger('applyUiChange cb');
ackCallback(noteKey);
}, onFailure: function (error) {
self.logError_(error);
ackCallback(-1);
}});
};
/**
* SQL statement to insert notes updates.
* @type {!google.wspl.Statement}
* @private
*/
google.wspl.simplenotes.Cache.INSERT_NOTE_ =
new google.wspl.Statement(
'REPLACE INTO cached_notes (noteKey, subject, body) ' +
'VALUES(?,?,?);' );
/**
* SQL statement to force replay of pending actions by setting a bit
* flag on each write-buffer row indicating that it should be reapplied
* to the contents of the cache.
* @type {!google.wspl.Statement}
* @private
*/
google.wspl.simplenotes.Cache.FORCE_REPLAY_ =
new google.wspl.Statement(
'UPDATE write_buffer SET status = status | 8;' );
/**
* SQL statement to delete notes no longer to be cached.
* @type {!google.wspl.Statement}
* @private
*/
google.wspl.simplenotes.Cache.EVICT_ =
new google.wspl.Statement(
'DELETE FROM cached_notes WHERE noteKey < ? OR noteKey > ?;');
/**
* Applies the changes delivered from the server by first inserting
* them into the cache and reapplying the write-buffer to the cache.
* @param {!Array.<Object>} notes An array of arrays.
*/
google.wspl.simplenotes.Cache.prototype.insertUpdate = function(notes) {
var self = this; var stats = [];
var start = notes[0].noteKey;
var end = notes[0].noteKey;
for (var i = 0; i < notes.length; i++) {
stats.push(google.wspl.simplenotes.Cache.INSERT_NOTE_.
createStatement([notes[i].noteKey, notes[i].subject, notes[i].body]));
start = Math.min(start, notes[0].noteKey);
end = Math.max(end, notes[0].noteKey);
}
stats.push(google.wspl.simplenotes.Cache.EVICT_.createStatement([start, end]));
stats.push(google.wspl.simplenotes.Cache.FORCE_REPLAY_);
var inTrans = function(tx) {
self.start_ = start;
self.end_ = end;
tx.executeAll(stats);
};
var afterInsert = function(tx) {
if (this.lastMiss_ &&
this.isCacheHit_(this.lastMiss_.start, this.lastMiss_.end)) {
this.lastMiss_.callback(notes);
this.lastMiss_ = undefined;
}
};
this.dbms_.createTransaction(inTrans, {onSuccess: afterInsert,
onError: this.logError_});
};
/**
* SQL statement to force replay of pending actions by setting a bit
* flag on each write-buffer row indicating that it should be reapplied
* to the contents of the cache.
* @type {!google.wspl.Statement}
* @private
*/
google.wspl.simplenotes.Cache.GET_UPDATES_TO_RESEND_ =
new google.wspl.Statement(
'SELECT noteKey, subject, body FROM write_buffer WHERE status & 2 = 2;');
/**
* SQL statement to mark write buffer statements as inflight.
* @type {!google.wspl.Statement}
* @private
*/
google.wspl.simplenotes.Cache.MARK_AS_INFLIGHT_ =
new google.wspl.Statement(
'UPDATE write_buffer SET status = status & ~2 | 1 WHERE status & 2 = 2;');
/**
* Fetches new material from the server as required.
* @param {number} start
* @param {number} end
* @param {function} opt_valueCallBack
*/
google.wspl.simplenotes.Cache.prototype.fetchFromServer = function(start,
end) {
google.logger('fetchFromServer');
var now = this.dbms_.getCurrentTime();
if (start >= this.start_ && end <= this.end_ &&
now - this.lastRefresh_ <
google.wspl.simplenotes.Cache.TIME_BETWEEN_REFRESH_) {
return;
}
var updates = []; var self = this; var flag = 1; var sql = []
sql.push(google.wspl.simplenotes.Cache.GET_UPDATES_TO_RESEND_);
sql.push(google.wspl.simplenotes.Cache.MARK_AS_INFLIGHT_);
var accumulateUpdates = function(tx, rs) {
if (flag == 1) {
for(; rs.isValidRow(); rs.next()) { updates.push(['u', rs.getRow()]); }
flag++;
}
};
var ackAndPost = function() {
updates.push(['q', {start: start, end: end}]);
self.sendXhrPost(updates);
};
this.dbms_.executeAll(sql,
{onSuccess: accumulateUpdates, onFailure: this.logError_},
{onSuccess: ackAndPost, onFailure: this.logError_});
};

View file

@ -0,0 +1,66 @@
/*
Copyright 2009 Google 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.
*/
body {
background-color: #efefef;
font: medium "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif;
}
.status-bar {
background-color: #ffd21d;
display: none;
-webkit-border-radius: 6.0px;
-moz-border-radius: 6.0px;
border-style: solid;
border-width: 1px;
padding: 3px;
text-align: center;
}
.screen {
border: thin solid #939393;
padding: 2px 2px 2px 2px;
}
.command-bar {
background-color: #275390;
}
.title-bar {
font-weight: bold;
background-color: #4b4b4b;
text-align: center;
padding: 3px;
color: #aaa;
}
textarea {
font-size: 16pt;
}
button {
text-align:center;
font-family: Helvetica;
font-weight: bold;
font-size: 20px;
text-decoration: none;
text-shadow: #fff 0px 2px 2px;
padding: 3px;
-webkit-border-radius: 6.0px;
-moz-border-radius: 6.0px;
margin-right: 4px;
margin-left: 4px;
}

View file

@ -0,0 +1,75 @@
/*
Copyright 2009 Google 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.
*/
/**
* @fileoverview template.js contains the implementation for a simple template
* scheme. Templates are fragments of HTML containing patterns into which
* arguments will be substituted.
*/
google.wspl.simplenotes = google.wspl.simplenotes || {};
/**
* Template class constructor. A template is an object which will
* substitute provided parameters into a marked-up string.
* @param {string} template
* @constructor
*/
google.wspl.simplenotes.Template = function(template) {
this.template_ = template;
this.res_ = null;
};
/**
* Returns the template expanded with the given args where args
* is an object (acting as an associative array) binding keys (found
* in the template wrapped with % symbols) to the associated
* values.
*
* Template substitution symbols without corresponding arguments
* will be passed through unchanged to the output.
*
* We assume that in typical use, the same template will be expanded
* repeatedly with different values. In this case, storing and re-using
* previously generated regular expressions will provide a performance
* improvement.
* @param {Object} args associates names with values
* @param {boolean} opt_rebuild set to true to force re-building the
* the regular epxression.
*/
google.wspl.simplenotes.Template.prototype.process = function(args,
opt_rebuild) {
var rebuild = opt_rebuild || false;
if (rebuild || this.res_ == null) {
var accumulatedRe = [];
this.res_ = null;
for (var a in args) {
accumulatedRe.push('%' + String(a) + '%');
}
if (accumulatedRe.length > 0) {
this.res_ = new RegExp(accumulatedRe.join('|'), 'g');
}
}
if (this.res_ != null) {
return this.template_.replace(this.res_, function(match) {
var keyName = match.slice(1,-1);
return args[keyName];
});
} else {
return this.template_;
}
};