├── .gitignore ├── .travis.yml ├── README.md ├── pom.xml └── src └── main ├── java └── com │ └── todoapp │ ├── Bootstrap.java │ ├── JsonTransformer.java │ ├── Todo.java │ ├── TodoResource.java │ └── TodoService.java └── resources └── public ├── css ├── bootstrap.css └── main.css ├── index.html ├── js ├── angular-cookies.js ├── angular-resource.js ├── angular-route.js ├── angular-sanitize.js ├── angular.js └── jquery.js ├── scripts └── app.js └── views ├── create.html └── list.html /.gitignore: -------------------------------------------------------------------------------- 1 | .settings 2 | .idea 3 | *.iml 4 | target/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/shekhargulati/todoapp-spark.svg?branch=master)](https://travis-ci.org/shekhargulati/todoapp-spark) 2 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | todoapp 8 | todoapp1 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 1.8 13 | 1.8 14 | 15 | 16 | 17 | 18 | com.sparkjava 19 | spark-core 20 | 2.0.0 21 | 22 | 23 | org.slf4j 24 | slf4j-simple 25 | 1.7.5 26 | 27 | 28 | org.mongodb 29 | mongo-java-driver 30 | 2.11.3 31 | 32 | 33 | com.google.code.gson 34 | gson 35 | 2.2.4 36 | 37 | 38 | 39 | 40 | 41 | 42 | org.apache.maven.plugins 43 | maven-shade-plugin 44 | 2.3 45 | 46 | true 47 | 48 | 49 | *:* 50 | 51 | META-INF/*.SF 52 | META-INF/*.DSA 53 | META-INF/*.RSA 54 | 55 | 56 | 57 | 58 | 59 | 60 | package 61 | 62 | shade 63 | 64 | 65 | 66 | 68 | com.todoapp.Bootstrap 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/main/java/com/todoapp/Bootstrap.java: -------------------------------------------------------------------------------- 1 | package com.todoapp; 2 | 3 | import com.mongodb.*; 4 | 5 | import static spark.Spark.setIpAddress; 6 | import static spark.Spark.setPort; 7 | import static spark.SparkBase.staticFileLocation; 8 | 9 | /** 10 | * Created by shekhargulati on 09/06/14. 11 | */ 12 | public class Bootstrap { 13 | private static final String IP_ADDRESS = System.getenv("OPENSHIFT_DIY_IP") != null ? System.getenv("OPENSHIFT_DIY_IP") : "localhost"; 14 | private static final int PORT = System.getenv("OPENSHIFT_DIY_IP") != null ? Integer.parseInt(System.getenv("OPENSHIFT_DIY_IP")) : 8080; 15 | 16 | public static void main(String[] args) throws Exception { 17 | setIpAddress(IP_ADDRESS); 18 | setPort(PORT); 19 | staticFileLocation("/public"); 20 | new TodoResource(new TodoService(mongo())); 21 | } 22 | 23 | private static DB mongo() throws Exception { 24 | String host = System.getenv("OPENSHIFT_MONGODB_DB_HOST"); 25 | if (host == null) { 26 | MongoClient mongoClient = new MongoClient("localhost"); 27 | return mongoClient.getDB("todoapp"); 28 | } 29 | int port = Integer.parseInt(System.getenv("OPENSHIFT_MONGODB_DB_PORT")); 30 | String dbname = System.getenv("OPENSHIFT_APP_NAME"); 31 | String username = System.getenv("OPENSHIFT_MONGODB_DB_USERNAME"); 32 | String password = System.getenv("OPENSHIFT_MONGODB_DB_PASSWORD"); 33 | MongoClientOptions mongoClientOptions = MongoClientOptions.builder().connectionsPerHost(20).build(); 34 | MongoClient mongoClient = new MongoClient(new ServerAddress(host, port), mongoClientOptions); 35 | mongoClient.setWriteConcern(WriteConcern.SAFE); 36 | DB db = mongoClient.getDB(dbname); 37 | if (db.authenticate(username, password.toCharArray())) { 38 | return db; 39 | } else { 40 | throw new RuntimeException("Not able to authenticate with MongoDB"); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/todoapp/JsonTransformer.java: -------------------------------------------------------------------------------- 1 | package com.todoapp; 2 | 3 | import com.google.gson.Gson; 4 | import spark.Response; 5 | import spark.ResponseTransformer; 6 | 7 | import java.util.HashMap; 8 | 9 | public class JsonTransformer implements ResponseTransformer { 10 | 11 | private Gson gson = new Gson(); 12 | 13 | @Override 14 | public String render(Object model) { 15 | if (model instanceof Response) { 16 | return gson.toJson(new HashMap<>()); 17 | } 18 | return gson.toJson(model); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /src/main/java/com/todoapp/Todo.java: -------------------------------------------------------------------------------- 1 | package com.todoapp; 2 | 3 | import com.mongodb.BasicDBObject; 4 | import com.mongodb.DBObject; 5 | import org.bson.types.ObjectId; 6 | 7 | import java.util.Date; 8 | 9 | /** 10 | * Created by shekhargulati on 09/06/14. 11 | */ 12 | public class Todo { 13 | 14 | private String id; 15 | private String title; 16 | private boolean done; 17 | private Date createdOn = new Date(); 18 | 19 | public Todo(BasicDBObject dbObject) { 20 | this.id = ((ObjectId) dbObject.get("_id")).toString(); 21 | this.title = dbObject.getString("title"); 22 | this.done = dbObject.getBoolean("done"); 23 | this.createdOn = dbObject.getDate("createdOn"); 24 | } 25 | 26 | public String getTitle() { 27 | return title; 28 | } 29 | 30 | public boolean isDone() { 31 | return done; 32 | } 33 | 34 | public Date getCreatedOn() { 35 | return createdOn; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/todoapp/TodoResource.java: -------------------------------------------------------------------------------- 1 | package com.todoapp; 2 | 3 | import com.google.gson.Gson; 4 | import spark.Request; 5 | import spark.Response; 6 | import spark.Route; 7 | 8 | import java.util.HashMap; 9 | 10 | import static spark.Spark.get; 11 | import static spark.Spark.post; 12 | import static spark.Spark.put; 13 | 14 | /** 15 | * Created by shekhargulati on 09/06/14. 16 | */ 17 | public class TodoResource { 18 | 19 | private static final String API_CONTEXT = "/api/v1"; 20 | 21 | private final TodoService todoService; 22 | 23 | public TodoResource(TodoService todoService) { 24 | this.todoService = todoService; 25 | setupEndpoints(); 26 | } 27 | 28 | private void setupEndpoints() { 29 | post(API_CONTEXT + "/todos", "application/json", (request, response) -> { 30 | todoService.createNewTodo(request.body()); 31 | response.status(201); 32 | return response; 33 | }, new JsonTransformer()); 34 | 35 | get(API_CONTEXT + "/todos/:id", "application/json", (request, response) 36 | 37 | -> todoService.find(request.params(":id")), new JsonTransformer()); 38 | 39 | get(API_CONTEXT + "/todos", "application/json", (request, response) 40 | 41 | -> todoService.findAll(), new JsonTransformer()); 42 | 43 | put(API_CONTEXT + "/todos/:id", "application/json", (request, response) 44 | 45 | -> todoService.update(request.params(":id"), request.body()), new JsonTransformer()); 46 | } 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/todoapp/TodoService.java: -------------------------------------------------------------------------------- 1 | package com.todoapp; 2 | 3 | import com.google.gson.Gson; 4 | import com.mongodb.*; 5 | import org.bson.types.ObjectId; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Collections; 9 | import java.util.Date; 10 | import java.util.List; 11 | 12 | /** 13 | * Created by shekhargulati on 09/06/14. 14 | */ 15 | public class TodoService { 16 | 17 | private final DB db; 18 | private final DBCollection collection; 19 | 20 | public TodoService(DB db) { 21 | this.db = db; 22 | this.collection = db.getCollection("todos"); 23 | } 24 | 25 | public List findAll() { 26 | List todos = new ArrayList<>(); 27 | DBCursor dbObjects = collection.find(); 28 | while (dbObjects.hasNext()) { 29 | DBObject dbObject = dbObjects.next(); 30 | todos.add(new Todo((BasicDBObject) dbObject)); 31 | } 32 | return todos; 33 | } 34 | 35 | public void createNewTodo(String body) { 36 | Todo todo = new Gson().fromJson(body, Todo.class); 37 | collection.insert(new BasicDBObject("title", todo.getTitle()).append("done", todo.isDone()).append("createdOn", new Date())); 38 | } 39 | 40 | public Todo find(String id) { 41 | return new Todo((BasicDBObject) collection.findOne(new BasicDBObject("_id", new ObjectId(id)))); 42 | } 43 | 44 | public Todo update(String todoId, String body) { 45 | Todo todo = new Gson().fromJson(body, Todo.class); 46 | collection.update(new BasicDBObject("_id", new ObjectId(todoId)), new BasicDBObject("$set", new BasicDBObject("done", todo.isDone()))); 47 | return this.find(todoId); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/resources/public/css/main.css: -------------------------------------------------------------------------------- 1 | .success-true{ 2 | text-decoration: line-through; 3 | } -------------------------------------------------------------------------------- /src/main/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Todo App 5 | 6 | 7 | 8 | 9 | 22 | 23 |
24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/main/resources/public/js/angular-cookies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.2.15-build.2399+sha.ca4ddfa 3 | * (c) 2010-2014 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) {'use strict'; 7 | 8 | /** 9 | * @ngdoc module 10 | * @name ngCookies 11 | * @description 12 | * 13 | * # ngCookies 14 | * 15 | * The `ngCookies` module provides a convenient wrapper for reading and writing browser cookies. 16 | * 17 | * 18 | *
19 | * 20 | * See {@link ngCookies.$cookies `$cookies`} and 21 | * {@link ngCookies.$cookieStore `$cookieStore`} for usage. 22 | */ 23 | 24 | 25 | angular.module('ngCookies', ['ng']). 26 | /** 27 | * @ngdoc service 28 | * @name $cookies 29 | * 30 | * @description 31 | * Provides read/write access to browser's cookies. 32 | * 33 | * Only a simple Object is exposed and by adding or removing properties to/from 34 | * this object, new cookies are created/deleted at the end of current $eval. 35 | * 36 | * Requires the {@link ngCookies `ngCookies`} module to be installed. 37 | * 38 | * @example 39 | 40 | 41 | 49 | 50 | 51 | */ 52 | factory('$cookies', ['$rootScope', '$browser', function ($rootScope, $browser) { 53 | var cookies = {}, 54 | lastCookies = {}, 55 | lastBrowserCookies, 56 | runEval = false, 57 | copy = angular.copy, 58 | isUndefined = angular.isUndefined; 59 | 60 | //creates a poller fn that copies all cookies from the $browser to service & inits the service 61 | $browser.addPollFn(function() { 62 | var currentCookies = $browser.cookies(); 63 | if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl 64 | lastBrowserCookies = currentCookies; 65 | copy(currentCookies, lastCookies); 66 | copy(currentCookies, cookies); 67 | if (runEval) $rootScope.$apply(); 68 | } 69 | })(); 70 | 71 | runEval = true; 72 | 73 | //at the end of each eval, push cookies 74 | //TODO: this should happen before the "delayed" watches fire, because if some cookies are not 75 | // strings or browser refuses to store some cookies, we update the model in the push fn. 76 | $rootScope.$watch(push); 77 | 78 | return cookies; 79 | 80 | 81 | /** 82 | * Pushes all the cookies from the service to the browser and verifies if all cookies were 83 | * stored. 84 | */ 85 | function push() { 86 | var name, 87 | value, 88 | browserCookies, 89 | updated; 90 | 91 | //delete any cookies deleted in $cookies 92 | for (name in lastCookies) { 93 | if (isUndefined(cookies[name])) { 94 | $browser.cookies(name, undefined); 95 | } 96 | } 97 | 98 | //update all cookies updated in $cookies 99 | for(name in cookies) { 100 | value = cookies[name]; 101 | if (!angular.isString(value)) { 102 | if (angular.isDefined(lastCookies[name])) { 103 | cookies[name] = lastCookies[name]; 104 | } else { 105 | delete cookies[name]; 106 | } 107 | } else if (value !== lastCookies[name]) { 108 | $browser.cookies(name, value); 109 | updated = true; 110 | } 111 | } 112 | 113 | //verify what was actually stored 114 | if (updated){ 115 | updated = false; 116 | browserCookies = $browser.cookies(); 117 | 118 | for (name in cookies) { 119 | if (cookies[name] !== browserCookies[name]) { 120 | //delete or reset all cookies that the browser dropped from $cookies 121 | if (isUndefined(browserCookies[name])) { 122 | delete cookies[name]; 123 | } else { 124 | cookies[name] = browserCookies[name]; 125 | } 126 | updated = true; 127 | } 128 | } 129 | } 130 | } 131 | }]). 132 | 133 | 134 | /** 135 | * @ngdoc service 136 | * @name $cookieStore 137 | * @requires $cookies 138 | * 139 | * @description 140 | * Provides a key-value (string-object) storage, that is backed by session cookies. 141 | * Objects put or retrieved from this storage are automatically serialized or 142 | * deserialized by angular's toJson/fromJson. 143 | * 144 | * Requires the {@link ngCookies `ngCookies`} module to be installed. 145 | * 146 | * @example 147 | */ 148 | factory('$cookieStore', ['$cookies', function($cookies) { 149 | 150 | return { 151 | /** 152 | * @ngdoc method 153 | * @name $cookieStore#get 154 | * 155 | * @description 156 | * Returns the value of given cookie key 157 | * 158 | * @param {string} key Id to use for lookup. 159 | * @returns {Object} Deserialized cookie value. 160 | */ 161 | get: function(key) { 162 | var value = $cookies[key]; 163 | return value ? angular.fromJson(value) : value; 164 | }, 165 | 166 | /** 167 | * @ngdoc method 168 | * @name $cookieStore#put 169 | * 170 | * @description 171 | * Sets a value for given cookie key 172 | * 173 | * @param {string} key Id for the `value`. 174 | * @param {Object} value Value to be stored. 175 | */ 176 | put: function(key, value) { 177 | $cookies[key] = angular.toJson(value); 178 | }, 179 | 180 | /** 181 | * @ngdoc method 182 | * @name $cookieStore#remove 183 | * 184 | * @description 185 | * Remove given cookie 186 | * 187 | * @param {string} key Id of the key-value pair to delete. 188 | */ 189 | remove: function(key) { 190 | delete $cookies[key]; 191 | } 192 | }; 193 | 194 | }]); 195 | 196 | 197 | })(window, window.angular); 198 | -------------------------------------------------------------------------------- /src/main/resources/public/js/angular-resource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.2.15-build.2399+sha.ca4ddfa 3 | * (c) 2010-2014 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) {'use strict'; 7 | 8 | var $resourceMinErr = angular.$$minErr('$resource'); 9 | 10 | // Helper functions and regex to lookup a dotted path on an object 11 | // stopping at undefined/null. The path must be composed of ASCII 12 | // identifiers (just like $parse) 13 | var MEMBER_NAME_REGEX = /^(\.[a-zA-Z_$][0-9a-zA-Z_$]*)+$/; 14 | 15 | function isValidDottedPath(path) { 16 | return (path != null && path !== '' && path !== 'hasOwnProperty' && 17 | MEMBER_NAME_REGEX.test('.' + path)); 18 | } 19 | 20 | function lookupDottedPath(obj, path) { 21 | if (!isValidDottedPath(path)) { 22 | throw $resourceMinErr('badmember', 'Dotted member path "@{0}" is invalid.', path); 23 | } 24 | var keys = path.split('.'); 25 | for (var i = 0, ii = keys.length; i < ii && obj !== undefined; i++) { 26 | var key = keys[i]; 27 | obj = (obj !== null) ? obj[key] : undefined; 28 | } 29 | return obj; 30 | } 31 | 32 | /** 33 | * Create a shallow copy of an object and clear other fields from the destination 34 | */ 35 | function shallowClearAndCopy(src, dst) { 36 | dst = dst || {}; 37 | 38 | angular.forEach(dst, function(value, key){ 39 | delete dst[key]; 40 | }); 41 | 42 | for (var key in src) { 43 | if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { 44 | dst[key] = src[key]; 45 | } 46 | } 47 | 48 | return dst; 49 | } 50 | 51 | /** 52 | * @ngdoc module 53 | * @name ngResource 54 | * @description 55 | * 56 | * # ngResource 57 | * 58 | * The `ngResource` module provides interaction support with RESTful services 59 | * via the $resource service. 60 | * 61 | * 62 | *
63 | * 64 | * See {@link ngResource.$resource `$resource`} for usage. 65 | */ 66 | 67 | /** 68 | * @ngdoc service 69 | * @name $resource 70 | * @requires $http 71 | * 72 | * @description 73 | * A factory which creates a resource object that lets you interact with 74 | * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. 75 | * 76 | * The returned resource object has action methods which provide high-level behaviors without 77 | * the need to interact with the low level {@link ng.$http $http} service. 78 | * 79 | * Requires the {@link ngResource `ngResource`} module to be installed. 80 | * 81 | * @param {string} url A parametrized URL template with parameters prefixed by `:` as in 82 | * `/user/:username`. If you are using a URL with a port number (e.g. 83 | * `http://example.com:8080/api`), it will be respected. 84 | * 85 | * If you are using a url with a suffix, just add the suffix, like this: 86 | * `$resource('http://example.com/resource.json')` or `$resource('http://example.com/:id.json')` 87 | * or even `$resource('http://example.com/resource/:resource_id.:format')` 88 | * If the parameter before the suffix is empty, :resource_id in this case, then the `/.` will be 89 | * collapsed down to a single `.`. If you need this sequence to appear and not collapse then you 90 | * can escape it with `/\.`. 91 | * 92 | * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in 93 | * `actions` methods. If any of the parameter value is a function, it will be executed every time 94 | * when a param value needs to be obtained for a request (unless the param was overridden). 95 | * 96 | * Each key value in the parameter object is first bound to url template if present and then any 97 | * excess keys are appended to the url search query after the `?`. 98 | * 99 | * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in 100 | * URL `/path/greet?salutation=Hello`. 101 | * 102 | * If the parameter value is prefixed with `@` then the value of that parameter is extracted from 103 | * the data object (useful for non-GET operations). 104 | * 105 | * @param {Object.=} actions Hash with declaration of custom action that should extend 106 | * the default set of resource actions. The declaration should be created in the format of {@link 107 | * ng.$http#usage_parameters $http.config}: 108 | * 109 | * {action1: {method:?, params:?, isArray:?, headers:?, ...}, 110 | * action2: {method:?, params:?, isArray:?, headers:?, ...}, 111 | * ...} 112 | * 113 | * Where: 114 | * 115 | * - **`action`** – {string} – The name of action. This name becomes the name of the method on 116 | * your resource object. 117 | * - **`method`** – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, 118 | * `DELETE`, and `JSONP`. 119 | * - **`params`** – {Object=} – Optional set of pre-bound parameters for this action. If any of 120 | * the parameter value is a function, it will be executed every time when a param value needs to 121 | * be obtained for a request (unless the param was overridden). 122 | * - **`url`** – {string} – action specific `url` override. The url templating is supported just 123 | * like for the resource-level urls. 124 | * - **`isArray`** – {boolean=} – If true then the returned object for this action is an array, 125 | * see `returns` section. 126 | * - **`transformRequest`** – 127 | * `{function(data, headersGetter)|Array.}` – 128 | * transform function or an array of such functions. The transform function takes the http 129 | * request body and headers and returns its transformed (typically serialized) version. 130 | * - **`transformResponse`** – 131 | * `{function(data, headersGetter)|Array.}` – 132 | * transform function or an array of such functions. The transform function takes the http 133 | * response body and headers and returns its transformed (typically deserialized) version. 134 | * - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the 135 | * GET request, otherwise if a cache instance built with 136 | * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for 137 | * caching. 138 | * - **`timeout`** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} that 139 | * should abort the request when resolved. 140 | * - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the 141 | * XHR object. See 142 | * [requests with credentials](https://developer.mozilla.org/en/http_access_control#section_5) 143 | * for more information. 144 | * - **`responseType`** - `{string}` - see 145 | * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). 146 | * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods - 147 | * `response` and `responseError`. Both `response` and `responseError` interceptors get called 148 | * with `http response` object. See {@link ng.$http $http interceptors}. 149 | * 150 | * @returns {Object} A resource "class" object with methods for the default set of resource actions 151 | * optionally extended with custom `actions`. The default set contains these actions: 152 | * ```js 153 | * { 'get': {method:'GET'}, 154 | * 'save': {method:'POST'}, 155 | * 'query': {method:'GET', isArray:true}, 156 | * 'remove': {method:'DELETE'}, 157 | * 'delete': {method:'DELETE'} }; 158 | * ``` 159 | * 160 | * Calling these methods invoke an {@link ng.$http} with the specified http method, 161 | * destination and parameters. When the data is returned from the server then the object is an 162 | * instance of the resource class. The actions `save`, `remove` and `delete` are available on it 163 | * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create, 164 | * read, update, delete) on server-side data like this: 165 | * ```js 166 | * var User = $resource('/user/:userId', {userId:'@id'}); 167 | * var user = User.get({userId:123}, function() { 168 | * user.abc = true; 169 | * user.$save(); 170 | * }); 171 | * ``` 172 | * 173 | * It is important to realize that invoking a $resource object method immediately returns an 174 | * empty reference (object or array depending on `isArray`). Once the data is returned from the 175 | * server the existing reference is populated with the actual data. This is a useful trick since 176 | * usually the resource is assigned to a model which is then rendered by the view. Having an empty 177 | * object results in no rendering, once the data arrives from the server then the object is 178 | * populated with the data and the view automatically re-renders itself showing the new data. This 179 | * means that in most cases one never has to write a callback function for the action methods. 180 | * 181 | * The action methods on the class object or instance object can be invoked with the following 182 | * parameters: 183 | * 184 | * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` 185 | * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` 186 | * - non-GET instance actions: `instance.$action([parameters], [success], [error])` 187 | * 188 | * Success callback is called with (value, responseHeaders) arguments. Error callback is called 189 | * with (httpResponse) argument. 190 | * 191 | * Class actions return empty instance (with additional properties below). 192 | * Instance actions return promise of the action. 193 | * 194 | * The Resource instances and collection have these additional properties: 195 | * 196 | * - `$promise`: the {@link ng.$q promise} of the original server interaction that created this 197 | * instance or collection. 198 | * 199 | * On success, the promise is resolved with the same resource instance or collection object, 200 | * updated with data from server. This makes it easy to use in 201 | * {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view 202 | * rendering until the resource(s) are loaded. 203 | * 204 | * On failure, the promise is resolved with the {@link ng.$http http response} object, without 205 | * the `resource` property. 206 | * 207 | * - `$resolved`: `true` after first server interaction is completed (either with success or 208 | * rejection), `false` before that. Knowing if the Resource has been resolved is useful in 209 | * data-binding. 210 | * 211 | * @example 212 | * 213 | * # Credit card resource 214 | * 215 | * ```js 216 | // Define CreditCard class 217 | var CreditCard = $resource('/user/:userId/card/:cardId', 218 | {userId:123, cardId:'@id'}, { 219 | charge: {method:'POST', params:{charge:true}} 220 | }); 221 | 222 | // We can retrieve a collection from the server 223 | var cards = CreditCard.query(function() { 224 | // GET: /user/123/card 225 | // server returns: [ {id:456, number:'1234', name:'Smith'} ]; 226 | 227 | var card = cards[0]; 228 | // each item is an instance of CreditCard 229 | expect(card instanceof CreditCard).toEqual(true); 230 | card.name = "J. Smith"; 231 | // non GET methods are mapped onto the instances 232 | card.$save(); 233 | // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'} 234 | // server returns: {id:456, number:'1234', name: 'J. Smith'}; 235 | 236 | // our custom method is mapped as well. 237 | card.$charge({amount:9.99}); 238 | // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'} 239 | }); 240 | 241 | // we can create an instance as well 242 | var newCard = new CreditCard({number:'0123'}); 243 | newCard.name = "Mike Smith"; 244 | newCard.$save(); 245 | // POST: /user/123/card {number:'0123', name:'Mike Smith'} 246 | // server returns: {id:789, number:'0123', name: 'Mike Smith'}; 247 | expect(newCard.id).toEqual(789); 248 | * ``` 249 | * 250 | * The object returned from this function execution is a resource "class" which has "static" method 251 | * for each action in the definition. 252 | * 253 | * Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and 254 | * `headers`. 255 | * When the data is returned from the server then the object is an instance of the resource type and 256 | * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD 257 | * operations (create, read, update, delete) on server-side data. 258 | 259 | ```js 260 | var User = $resource('/user/:userId', {userId:'@id'}); 261 | var user = User.get({userId:123}, function() { 262 | user.abc = true; 263 | user.$save(); 264 | }); 265 | ``` 266 | * 267 | * It's worth noting that the success callback for `get`, `query` and other methods gets passed 268 | * in the response that came from the server as well as $http header getter function, so one 269 | * could rewrite the above example and get access to http headers as: 270 | * 271 | ```js 272 | var User = $resource('/user/:userId', {userId:'@id'}); 273 | User.get({userId:123}, function(u, getResponseHeaders){ 274 | u.abc = true; 275 | u.$save(function(u, putResponseHeaders) { 276 | //u => saved user object 277 | //putResponseHeaders => $http header getter 278 | }); 279 | }); 280 | ``` 281 | 282 | * # Creating a custom 'PUT' request 283 | * In this example we create a custom method on our resource to make a PUT request 284 | * ```js 285 | * var app = angular.module('app', ['ngResource', 'ngRoute']); 286 | * 287 | * // Some APIs expect a PUT request in the format URL/object/ID 288 | * // Here we are creating an 'update' method 289 | * app.factory('Notes', ['$resource', function($resource) { 290 | * return $resource('/notes/:id', null, 291 | * { 292 | * 'update': { method:'PUT' } 293 | * }); 294 | * }]); 295 | * 296 | * // In our controller we get the ID from the URL using ngRoute and $routeParams 297 | * // We pass in $routeParams and our Notes factory along with $scope 298 | * app.controller('NotesCtrl', ['$scope', '$routeParams', 'Notes', 299 | function($scope, $routeParams, Notes) { 300 | * // First get a note object from the factory 301 | * var note = Notes.get({ id:$routeParams.id }); 302 | * $id = note.id; 303 | * 304 | * // Now call update passing in the ID first then the object you are updating 305 | * Notes.update({ id:$id }, note); 306 | * 307 | * // This will PUT /notes/ID with the note object in the request payload 308 | * }]); 309 | * ``` 310 | */ 311 | angular.module('ngResource', ['ng']). 312 | factory('$resource', ['$http', '$q', function($http, $q) { 313 | 314 | var DEFAULT_ACTIONS = { 315 | 'get': {method:'GET'}, 316 | 'save': {method:'POST'}, 317 | 'query': {method:'GET', isArray:true}, 318 | 'remove': {method:'DELETE'}, 319 | 'delete': {method:'DELETE'} 320 | }; 321 | var noop = angular.noop, 322 | forEach = angular.forEach, 323 | extend = angular.extend, 324 | copy = angular.copy, 325 | isFunction = angular.isFunction; 326 | 327 | /** 328 | * We need our custom method because encodeURIComponent is too aggressive and doesn't follow 329 | * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path 330 | * segments: 331 | * segment = *pchar 332 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 333 | * pct-encoded = "%" HEXDIG HEXDIG 334 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 335 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 336 | * / "*" / "+" / "," / ";" / "=" 337 | */ 338 | function encodeUriSegment(val) { 339 | return encodeUriQuery(val, true). 340 | replace(/%26/gi, '&'). 341 | replace(/%3D/gi, '='). 342 | replace(/%2B/gi, '+'); 343 | } 344 | 345 | 346 | /** 347 | * This method is intended for encoding *key* or *value* parts of query component. We need a 348 | * custom method because encodeURIComponent is too aggressive and encodes stuff that doesn't 349 | * have to be encoded per http://tools.ietf.org/html/rfc3986: 350 | * query = *( pchar / "/" / "?" ) 351 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 352 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 353 | * pct-encoded = "%" HEXDIG HEXDIG 354 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 355 | * / "*" / "+" / "," / ";" / "=" 356 | */ 357 | function encodeUriQuery(val, pctEncodeSpaces) { 358 | return encodeURIComponent(val). 359 | replace(/%40/gi, '@'). 360 | replace(/%3A/gi, ':'). 361 | replace(/%24/g, '$'). 362 | replace(/%2C/gi, ','). 363 | replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); 364 | } 365 | 366 | function Route(template, defaults) { 367 | this.template = template; 368 | this.defaults = defaults || {}; 369 | this.urlParams = {}; 370 | } 371 | 372 | Route.prototype = { 373 | setUrlParams: function(config, params, actionUrl) { 374 | var self = this, 375 | url = actionUrl || self.template, 376 | val, 377 | encodedVal; 378 | 379 | var urlParams = self.urlParams = {}; 380 | forEach(url.split(/\W/), function(param){ 381 | if (param === 'hasOwnProperty') { 382 | throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name."); 383 | } 384 | if (!(new RegExp("^\\d+$").test(param)) && param && 385 | (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) { 386 | urlParams[param] = true; 387 | } 388 | }); 389 | url = url.replace(/\\:/g, ':'); 390 | 391 | params = params || {}; 392 | forEach(self.urlParams, function(_, urlParam){ 393 | val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; 394 | if (angular.isDefined(val) && val !== null) { 395 | encodedVal = encodeUriSegment(val); 396 | url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), function(match, p1) { 397 | return encodedVal + p1; 398 | }); 399 | } else { 400 | url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function(match, 401 | leadingSlashes, tail) { 402 | if (tail.charAt(0) == '/') { 403 | return tail; 404 | } else { 405 | return leadingSlashes + tail; 406 | } 407 | }); 408 | } 409 | }); 410 | 411 | // strip trailing slashes and set the url 412 | url = url.replace(/\/+$/, '') || '/'; 413 | // then replace collapse `/.` if found in the last URL path segment before the query 414 | // E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x` 415 | url = url.replace(/\/\.(?=\w+($|\?))/, '.'); 416 | // replace escaped `/\.` with `/.` 417 | config.url = url.replace(/\/\\\./, '/.'); 418 | 419 | 420 | // set params - delegate param encoding to $http 421 | forEach(params, function(value, key){ 422 | if (!self.urlParams[key]) { 423 | config.params = config.params || {}; 424 | config.params[key] = value; 425 | } 426 | }); 427 | } 428 | }; 429 | 430 | 431 | function resourceFactory(url, paramDefaults, actions) { 432 | var route = new Route(url); 433 | 434 | actions = extend({}, DEFAULT_ACTIONS, actions); 435 | 436 | function extractParams(data, actionParams){ 437 | var ids = {}; 438 | actionParams = extend({}, paramDefaults, actionParams); 439 | forEach(actionParams, function(value, key){ 440 | if (isFunction(value)) { value = value(); } 441 | ids[key] = value && value.charAt && value.charAt(0) == '@' ? 442 | lookupDottedPath(data, value.substr(1)) : value; 443 | }); 444 | return ids; 445 | } 446 | 447 | function defaultResponseInterceptor(response) { 448 | return response.resource; 449 | } 450 | 451 | function Resource(value){ 452 | shallowClearAndCopy(value || {}, this); 453 | } 454 | 455 | forEach(actions, function(action, name) { 456 | var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method); 457 | 458 | Resource[name] = function(a1, a2, a3, a4) { 459 | var params = {}, data, success, error; 460 | 461 | /* jshint -W086 */ /* (purposefully fall through case statements) */ 462 | switch(arguments.length) { 463 | case 4: 464 | error = a4; 465 | success = a3; 466 | //fallthrough 467 | case 3: 468 | case 2: 469 | if (isFunction(a2)) { 470 | if (isFunction(a1)) { 471 | success = a1; 472 | error = a2; 473 | break; 474 | } 475 | 476 | success = a2; 477 | error = a3; 478 | //fallthrough 479 | } else { 480 | params = a1; 481 | data = a2; 482 | success = a3; 483 | break; 484 | } 485 | case 1: 486 | if (isFunction(a1)) success = a1; 487 | else if (hasBody) data = a1; 488 | else params = a1; 489 | break; 490 | case 0: break; 491 | default: 492 | throw $resourceMinErr('badargs', 493 | "Expected up to 4 arguments [params, data, success, error], got {0} arguments", 494 | arguments.length); 495 | } 496 | /* jshint +W086 */ /* (purposefully fall through case statements) */ 497 | 498 | var isInstanceCall = this instanceof Resource; 499 | var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data)); 500 | var httpConfig = {}; 501 | var responseInterceptor = action.interceptor && action.interceptor.response || 502 | defaultResponseInterceptor; 503 | var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || 504 | undefined; 505 | 506 | forEach(action, function(value, key) { 507 | if (key != 'params' && key != 'isArray' && key != 'interceptor') { 508 | httpConfig[key] = copy(value); 509 | } 510 | }); 511 | 512 | if (hasBody) httpConfig.data = data; 513 | route.setUrlParams(httpConfig, 514 | extend({}, extractParams(data, action.params || {}), params), 515 | action.url); 516 | 517 | var promise = $http(httpConfig).then(function(response) { 518 | var data = response.data, 519 | promise = value.$promise; 520 | 521 | if (data) { 522 | // Need to convert action.isArray to boolean in case it is undefined 523 | // jshint -W018 524 | if (angular.isArray(data) !== (!!action.isArray)) { 525 | throw $resourceMinErr('badcfg', 'Error in resource configuration. Expected ' + 526 | 'response to contain an {0} but got an {1}', 527 | action.isArray?'array':'object', angular.isArray(data)?'array':'object'); 528 | } 529 | // jshint +W018 530 | if (action.isArray) { 531 | value.length = 0; 532 | forEach(data, function(item) { 533 | value.push(new Resource(item)); 534 | }); 535 | } else { 536 | shallowClearAndCopy(data, value); 537 | value.$promise = promise; 538 | } 539 | } 540 | 541 | value.$resolved = true; 542 | 543 | response.resource = value; 544 | 545 | return response; 546 | }, function(response) { 547 | value.$resolved = true; 548 | 549 | (error||noop)(response); 550 | 551 | return $q.reject(response); 552 | }); 553 | 554 | promise = promise.then( 555 | function(response) { 556 | var value = responseInterceptor(response); 557 | (success||noop)(value, response.headers); 558 | return value; 559 | }, 560 | responseErrorInterceptor); 561 | 562 | if (!isInstanceCall) { 563 | // we are creating instance / collection 564 | // - set the initial promise 565 | // - return the instance / collection 566 | value.$promise = promise; 567 | value.$resolved = false; 568 | 569 | return value; 570 | } 571 | 572 | // instance call 573 | return promise; 574 | }; 575 | 576 | 577 | Resource.prototype['$' + name] = function(params, success, error) { 578 | if (isFunction(params)) { 579 | error = success; success = params; params = {}; 580 | } 581 | var result = Resource[name].call(this, params, this, success, error); 582 | return result.$promise || result; 583 | }; 584 | }); 585 | 586 | Resource.bind = function(additionalParamDefaults){ 587 | return resourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); 588 | }; 589 | 590 | return Resource; 591 | } 592 | 593 | return resourceFactory; 594 | }]); 595 | 596 | 597 | })(window, window.angular); 598 | -------------------------------------------------------------------------------- /src/main/resources/public/js/angular-route.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.2.15-build.2399+sha.ca4ddfa 3 | * (c) 2010-2014 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) {'use strict'; 7 | 8 | /** 9 | * @ngdoc module 10 | * @name ngRoute 11 | * @description 12 | * 13 | * # ngRoute 14 | * 15 | * The `ngRoute` module provides routing and deeplinking services and directives for angular apps. 16 | * 17 | * ## Example 18 | * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. 19 | * 20 | * 21 | *
22 | */ 23 | /* global -ngRouteModule */ 24 | var ngRouteModule = angular.module('ngRoute', ['ng']). 25 | provider('$route', $RouteProvider); 26 | 27 | /** 28 | * @ngdoc provider 29 | * @name $routeProvider 30 | * @function 31 | * 32 | * @description 33 | * 34 | * Used for configuring routes. 35 | * 36 | * ## Example 37 | * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. 38 | * 39 | * ## Dependencies 40 | * Requires the {@link ngRoute `ngRoute`} module to be installed. 41 | */ 42 | function $RouteProvider(){ 43 | function inherit(parent, extra) { 44 | return angular.extend(new (angular.extend(function() {}, {prototype:parent}))(), extra); 45 | } 46 | 47 | var routes = {}; 48 | 49 | /** 50 | * @ngdoc method 51 | * @name $routeProvider#when 52 | * 53 | * @param {string} path Route path (matched against `$location.path`). If `$location.path` 54 | * contains redundant trailing slash or is missing one, the route will still match and the 55 | * `$location.path` will be updated to add or drop the trailing slash to exactly match the 56 | * route definition. 57 | * 58 | * * `path` can contain named groups starting with a colon: e.g. `:name`. All characters up 59 | * to the next slash are matched and stored in `$routeParams` under the given `name` 60 | * when the route matches. 61 | * * `path` can contain named groups starting with a colon and ending with a star: 62 | * e.g.`:name*`. All characters are eagerly stored in `$routeParams` under the given `name` 63 | * when the route matches. 64 | * * `path` can contain optional named groups with a question mark: e.g.`:name?`. 65 | * 66 | * For example, routes like `/color/:color/largecode/:largecode*\/edit` will match 67 | * `/color/brown/largecode/code/with/slashes/edit` and extract: 68 | * 69 | * * `color: brown` 70 | * * `largecode: code/with/slashes`. 71 | * 72 | * 73 | * @param {Object} route Mapping information to be assigned to `$route.current` on route 74 | * match. 75 | * 76 | * Object properties: 77 | * 78 | * - `controller` – `{(string|function()=}` – Controller fn that should be associated with 79 | * newly created scope or the name of a {@link angular.Module#controller registered 80 | * controller} if passed as a string. 81 | * - `controllerAs` – `{string=}` – A controller alias name. If present the controller will be 82 | * published to scope under the `controllerAs` name. 83 | * - `template` – `{string=|function()=}` – html template as a string or a function that 84 | * returns an html template as a string which should be used by {@link 85 | * ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives. 86 | * This property takes precedence over `templateUrl`. 87 | * 88 | * If `template` is a function, it will be called with the following parameters: 89 | * 90 | * - `{Array.<Object>}` - route parameters extracted from the current 91 | * `$location.path()` by applying the current route 92 | * 93 | * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html 94 | * template that should be used by {@link ngRoute.directive:ngView ngView}. 95 | * 96 | * If `templateUrl` is a function, it will be called with the following parameters: 97 | * 98 | * - `{Array.<Object>}` - route parameters extracted from the current 99 | * `$location.path()` by applying the current route 100 | * 101 | * - `resolve` - `{Object.=}` - An optional map of dependencies which should 102 | * be injected into the controller. If any of these dependencies are promises, the router 103 | * will wait for them all to be resolved or one to be rejected before the controller is 104 | * instantiated. 105 | * If all the promises are resolved successfully, the values of the resolved promises are 106 | * injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is 107 | * fired. If any of the promises are rejected the 108 | * {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. The map object 109 | * is: 110 | * 111 | * - `key` – `{string}`: a name of a dependency to be injected into the controller. 112 | * - `factory` - `{string|function}`: If `string` then it is an alias for a service. 113 | * Otherwise if function, then it is {@link auto.$injector#invoke injected} 114 | * and the return value is treated as the dependency. If the result is a promise, it is 115 | * resolved before its value is injected into the controller. Be aware that 116 | * `ngRoute.$routeParams` will still refer to the previous route within these resolve 117 | * functions. Use `$route.current.params` to access the new route parameters, instead. 118 | * 119 | * - `redirectTo` – {(string|function())=} – value to update 120 | * {@link ng.$location $location} path with and trigger route redirection. 121 | * 122 | * If `redirectTo` is a function, it will be called with the following parameters: 123 | * 124 | * - `{Object.}` - route parameters extracted from the current 125 | * `$location.path()` by applying the current route templateUrl. 126 | * - `{string}` - current `$location.path()` 127 | * - `{Object}` - current `$location.search()` 128 | * 129 | * The custom `redirectTo` function is expected to return a string which will be used 130 | * to update `$location.path()` and `$location.search()`. 131 | * 132 | * - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()` 133 | * or `$location.hash()` changes. 134 | * 135 | * If the option is set to `false` and url in the browser changes, then 136 | * `$routeUpdate` event is broadcasted on the root scope. 137 | * 138 | * - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive 139 | * 140 | * If the option is set to `true`, then the particular route can be matched without being 141 | * case sensitive 142 | * 143 | * @returns {Object} self 144 | * 145 | * @description 146 | * Adds a new route definition to the `$route` service. 147 | */ 148 | this.when = function(path, route) { 149 | routes[path] = angular.extend( 150 | {reloadOnSearch: true}, 151 | route, 152 | path && pathRegExp(path, route) 153 | ); 154 | 155 | // create redirection for trailing slashes 156 | if (path) { 157 | var redirectPath = (path[path.length-1] == '/') 158 | ? path.substr(0, path.length-1) 159 | : path +'/'; 160 | 161 | routes[redirectPath] = angular.extend( 162 | {redirectTo: path}, 163 | pathRegExp(redirectPath, route) 164 | ); 165 | } 166 | 167 | return this; 168 | }; 169 | 170 | /** 171 | * @param path {string} path 172 | * @param opts {Object} options 173 | * @return {?Object} 174 | * 175 | * @description 176 | * Normalizes the given path, returning a regular expression 177 | * and the original path. 178 | * 179 | * Inspired by pathRexp in visionmedia/express/lib/utils.js. 180 | */ 181 | function pathRegExp(path, opts) { 182 | var insensitive = opts.caseInsensitiveMatch, 183 | ret = { 184 | originalPath: path, 185 | regexp: path 186 | }, 187 | keys = ret.keys = []; 188 | 189 | path = path 190 | .replace(/([().])/g, '\\$1') 191 | .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option){ 192 | var optional = option === '?' ? option : null; 193 | var star = option === '*' ? option : null; 194 | keys.push({ name: key, optional: !!optional }); 195 | slash = slash || ''; 196 | return '' 197 | + (optional ? '' : slash) 198 | + '(?:' 199 | + (optional ? slash : '') 200 | + (star && '(.+?)' || '([^/]+)') 201 | + (optional || '') 202 | + ')' 203 | + (optional || ''); 204 | }) 205 | .replace(/([\/$\*])/g, '\\$1'); 206 | 207 | ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : ''); 208 | return ret; 209 | } 210 | 211 | /** 212 | * @ngdoc method 213 | * @name $routeProvider#otherwise 214 | * 215 | * @description 216 | * Sets route definition that will be used on route change when no other route definition 217 | * is matched. 218 | * 219 | * @param {Object} params Mapping information to be assigned to `$route.current`. 220 | * @returns {Object} self 221 | */ 222 | this.otherwise = function(params) { 223 | this.when(null, params); 224 | return this; 225 | }; 226 | 227 | 228 | this.$get = ['$rootScope', 229 | '$location', 230 | '$routeParams', 231 | '$q', 232 | '$injector', 233 | '$http', 234 | '$templateCache', 235 | '$sce', 236 | function($rootScope, $location, $routeParams, $q, $injector, $http, $templateCache, $sce) { 237 | 238 | /** 239 | * @ngdoc service 240 | * @name $route 241 | * @requires $location 242 | * @requires $routeParams 243 | * 244 | * @property {Object} current Reference to the current route definition. 245 | * The route definition contains: 246 | * 247 | * - `controller`: The controller constructor as define in route definition. 248 | * - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for 249 | * controller instantiation. The `locals` contain 250 | * the resolved values of the `resolve` map. Additionally the `locals` also contain: 251 | * 252 | * - `$scope` - The current route scope. 253 | * - `$template` - The current route template HTML. 254 | * 255 | * @property {Array.} routes Array of all configured routes. 256 | * 257 | * @description 258 | * `$route` is used for deep-linking URLs to controllers and views (HTML partials). 259 | * It watches `$location.url()` and tries to map the path to an existing route definition. 260 | * 261 | * Requires the {@link ngRoute `ngRoute`} module to be installed. 262 | * 263 | * You can define routes through {@link ngRoute.$routeProvider $routeProvider}'s API. 264 | * 265 | * The `$route` service is typically used in conjunction with the 266 | * {@link ngRoute.directive:ngView `ngView`} directive and the 267 | * {@link ngRoute.$routeParams `$routeParams`} service. 268 | * 269 | * @example 270 | * This example shows how changing the URL hash causes the `$route` to match a route against the 271 | * URL, and the `ngView` pulls in the partial. 272 | * 273 | * Note that this example is using {@link ng.directive:script inlined templates} 274 | * to get it working on jsfiddle as well. 275 | * 276 | * 278 | * 279 | *
280 | * Choose: 281 | * Moby | 282 | * Moby: Ch1 | 283 | * Gatsby | 284 | * Gatsby: Ch4 | 285 | * Scarlet Letter
286 | * 287 | *
288 | *
289 | * 290 | *
$location.path() = {{$location.path()}}
291 | *
$route.current.templateUrl = {{$route.current.templateUrl}}
292 | *
$route.current.params = {{$route.current.params}}
293 | *
$route.current.scope.name = {{$route.current.scope.name}}
294 | *
$routeParams = {{$routeParams}}
295 | *
296 | *
297 | * 298 | * 299 | * controller: {{name}}
300 | * Book Id: {{params.bookId}}
301 | *
302 | * 303 | * 304 | * controller: {{name}}
305 | * Book Id: {{params.bookId}}
306 | * Chapter Id: {{params.chapterId}} 307 | *
308 | * 309 | * 310 | * angular.module('ngRouteExample', ['ngRoute']) 311 | * 312 | * .config(function($routeProvider, $locationProvider) { 313 | * $routeProvider.when('/Book/:bookId', { 314 | * templateUrl: 'book.html', 315 | * controller: BookCntl, 316 | * resolve: { 317 | * // I will cause a 1 second delay 318 | * delay: function($q, $timeout) { 319 | * var delay = $q.defer(); 320 | * $timeout(delay.resolve, 1000); 321 | * return delay.promise; 322 | * } 323 | * } 324 | * }); 325 | * $routeProvider.when('/Book/:bookId/ch/:chapterId', { 326 | * templateUrl: 'chapter.html', 327 | * controller: ChapterCntl 328 | * }); 329 | * 330 | * // configure html5 to get links working on jsfiddle 331 | * $locationProvider.html5Mode(true); 332 | * }); 333 | * 334 | * function MainCntl($scope, $route, $routeParams, $location) { 335 | * $scope.$route = $route; 336 | * $scope.$location = $location; 337 | * $scope.$routeParams = $routeParams; 338 | * } 339 | * 340 | * function BookCntl($scope, $routeParams) { 341 | * $scope.name = "BookCntl"; 342 | * $scope.params = $routeParams; 343 | * } 344 | * 345 | * function ChapterCntl($scope, $routeParams) { 346 | * $scope.name = "ChapterCntl"; 347 | * $scope.params = $routeParams; 348 | * } 349 | * 350 | * 351 | * 352 | * it('should load and compile correct template', function() { 353 | * element(by.linkText('Moby: Ch1')).click(); 354 | * var content = element(by.css('[ng-view]')).getText(); 355 | * expect(content).toMatch(/controller\: ChapterCntl/); 356 | * expect(content).toMatch(/Book Id\: Moby/); 357 | * expect(content).toMatch(/Chapter Id\: 1/); 358 | * 359 | * element(by.partialLinkText('Scarlet')).click(); 360 | * 361 | * content = element(by.css('[ng-view]')).getText(); 362 | * expect(content).toMatch(/controller\: BookCntl/); 363 | * expect(content).toMatch(/Book Id\: Scarlet/); 364 | * }); 365 | * 366 | *
367 | */ 368 | 369 | /** 370 | * @ngdoc event 371 | * @name $route#$routeChangeStart 372 | * @eventType broadcast on root scope 373 | * @description 374 | * Broadcasted before a route change. At this point the route services starts 375 | * resolving all of the dependencies needed for the route change to occur. 376 | * Typically this involves fetching the view template as well as any dependencies 377 | * defined in `resolve` route property. Once all of the dependencies are resolved 378 | * `$routeChangeSuccess` is fired. 379 | * 380 | * @param {Object} angularEvent Synthetic event object. 381 | * @param {Route} next Future route information. 382 | * @param {Route} current Current route information. 383 | */ 384 | 385 | /** 386 | * @ngdoc event 387 | * @name $route#$routeChangeSuccess 388 | * @eventType broadcast on root scope 389 | * @description 390 | * Broadcasted after a route dependencies are resolved. 391 | * {@link ngRoute.directive:ngView ngView} listens for the directive 392 | * to instantiate the controller and render the view. 393 | * 394 | * @param {Object} angularEvent Synthetic event object. 395 | * @param {Route} current Current route information. 396 | * @param {Route|Undefined} previous Previous route information, or undefined if current is 397 | * first route entered. 398 | */ 399 | 400 | /** 401 | * @ngdoc event 402 | * @name $route#$routeChangeError 403 | * @eventType broadcast on root scope 404 | * @description 405 | * Broadcasted if any of the resolve promises are rejected. 406 | * 407 | * @param {Object} angularEvent Synthetic event object 408 | * @param {Route} current Current route information. 409 | * @param {Route} previous Previous route information. 410 | * @param {Route} rejection Rejection of the promise. Usually the error of the failed promise. 411 | */ 412 | 413 | /** 414 | * @ngdoc event 415 | * @name $route#$routeUpdate 416 | * @eventType broadcast on root scope 417 | * @description 418 | * 419 | * The `reloadOnSearch` property has been set to false, and we are reusing the same 420 | * instance of the Controller. 421 | */ 422 | 423 | var forceReload = false, 424 | $route = { 425 | routes: routes, 426 | 427 | /** 428 | * @ngdoc method 429 | * @name $route#reload 430 | * 431 | * @description 432 | * Causes `$route` service to reload the current route even if 433 | * {@link ng.$location $location} hasn't changed. 434 | * 435 | * As a result of that, {@link ngRoute.directive:ngView ngView} 436 | * creates new scope, reinstantiates the controller. 437 | */ 438 | reload: function() { 439 | forceReload = true; 440 | $rootScope.$evalAsync(updateRoute); 441 | } 442 | }; 443 | 444 | $rootScope.$on('$locationChangeSuccess', updateRoute); 445 | 446 | return $route; 447 | 448 | ///////////////////////////////////////////////////// 449 | 450 | /** 451 | * @param on {string} current url 452 | * @param route {Object} route regexp to match the url against 453 | * @return {?Object} 454 | * 455 | * @description 456 | * Check if the route matches the current url. 457 | * 458 | * Inspired by match in 459 | * visionmedia/express/lib/router/router.js. 460 | */ 461 | function switchRouteMatcher(on, route) { 462 | var keys = route.keys, 463 | params = {}; 464 | 465 | if (!route.regexp) return null; 466 | 467 | var m = route.regexp.exec(on); 468 | if (!m) return null; 469 | 470 | for (var i = 1, len = m.length; i < len; ++i) { 471 | var key = keys[i - 1]; 472 | 473 | var val = 'string' == typeof m[i] 474 | ? decodeURIComponent(m[i]) 475 | : m[i]; 476 | 477 | if (key && val) { 478 | params[key.name] = val; 479 | } 480 | } 481 | return params; 482 | } 483 | 484 | function updateRoute() { 485 | var next = parseRoute(), 486 | last = $route.current; 487 | 488 | if (next && last && next.$$route === last.$$route 489 | && angular.equals(next.pathParams, last.pathParams) 490 | && !next.reloadOnSearch && !forceReload) { 491 | last.params = next.params; 492 | angular.copy(last.params, $routeParams); 493 | $rootScope.$broadcast('$routeUpdate', last); 494 | } else if (next || last) { 495 | forceReload = false; 496 | $rootScope.$broadcast('$routeChangeStart', next, last); 497 | $route.current = next; 498 | if (next) { 499 | if (next.redirectTo) { 500 | if (angular.isString(next.redirectTo)) { 501 | $location.path(interpolate(next.redirectTo, next.params)).search(next.params) 502 | .replace(); 503 | } else { 504 | $location.url(next.redirectTo(next.pathParams, $location.path(), $location.search())) 505 | .replace(); 506 | } 507 | } 508 | } 509 | 510 | $q.when(next). 511 | then(function() { 512 | if (next) { 513 | var locals = angular.extend({}, next.resolve), 514 | template, templateUrl; 515 | 516 | angular.forEach(locals, function(value, key) { 517 | locals[key] = angular.isString(value) ? 518 | $injector.get(value) : $injector.invoke(value); 519 | }); 520 | 521 | if (angular.isDefined(template = next.template)) { 522 | if (angular.isFunction(template)) { 523 | template = template(next.params); 524 | } 525 | } else if (angular.isDefined(templateUrl = next.templateUrl)) { 526 | if (angular.isFunction(templateUrl)) { 527 | templateUrl = templateUrl(next.params); 528 | } 529 | templateUrl = $sce.getTrustedResourceUrl(templateUrl); 530 | if (angular.isDefined(templateUrl)) { 531 | next.loadedTemplateUrl = templateUrl; 532 | template = $http.get(templateUrl, {cache: $templateCache}). 533 | then(function(response) { return response.data; }); 534 | } 535 | } 536 | if (angular.isDefined(template)) { 537 | locals['$template'] = template; 538 | } 539 | return $q.all(locals); 540 | } 541 | }). 542 | // after route change 543 | then(function(locals) { 544 | if (next == $route.current) { 545 | if (next) { 546 | next.locals = locals; 547 | angular.copy(next.params, $routeParams); 548 | } 549 | $rootScope.$broadcast('$routeChangeSuccess', next, last); 550 | } 551 | }, function(error) { 552 | if (next == $route.current) { 553 | $rootScope.$broadcast('$routeChangeError', next, last, error); 554 | } 555 | }); 556 | } 557 | } 558 | 559 | 560 | /** 561 | * @returns {Object} the current active route, by matching it against the URL 562 | */ 563 | function parseRoute() { 564 | // Match a route 565 | var params, match; 566 | angular.forEach(routes, function(route, path) { 567 | if (!match && (params = switchRouteMatcher($location.path(), route))) { 568 | match = inherit(route, { 569 | params: angular.extend({}, $location.search(), params), 570 | pathParams: params}); 571 | match.$$route = route; 572 | } 573 | }); 574 | // No route matched; fallback to "otherwise" route 575 | return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}}); 576 | } 577 | 578 | /** 579 | * @returns {string} interpolation of the redirect path with the parameters 580 | */ 581 | function interpolate(string, params) { 582 | var result = []; 583 | angular.forEach((string||'').split(':'), function(segment, i) { 584 | if (i === 0) { 585 | result.push(segment); 586 | } else { 587 | var segmentMatch = segment.match(/(\w+)(.*)/); 588 | var key = segmentMatch[1]; 589 | result.push(params[key]); 590 | result.push(segmentMatch[2] || ''); 591 | delete params[key]; 592 | } 593 | }); 594 | return result.join(''); 595 | } 596 | }]; 597 | } 598 | 599 | ngRouteModule.provider('$routeParams', $RouteParamsProvider); 600 | 601 | 602 | /** 603 | * @ngdoc service 604 | * @name $routeParams 605 | * @requires $route 606 | * 607 | * @description 608 | * The `$routeParams` service allows you to retrieve the current set of route parameters. 609 | * 610 | * Requires the {@link ngRoute `ngRoute`} module to be installed. 611 | * 612 | * The route parameters are a combination of {@link ng.$location `$location`}'s 613 | * {@link ng.$location#search `search()`} and {@link ng.$location#path `path()`}. 614 | * The `path` parameters are extracted when the {@link ngRoute.$route `$route`} path is matched. 615 | * 616 | * In case of parameter name collision, `path` params take precedence over `search` params. 617 | * 618 | * The service guarantees that the identity of the `$routeParams` object will remain unchanged 619 | * (but its properties will likely change) even when a route change occurs. 620 | * 621 | * Note that the `$routeParams` are only updated *after* a route change completes successfully. 622 | * This means that you cannot rely on `$routeParams` being correct in route resolve functions. 623 | * Instead you can use `$route.current.params` to access the new route's parameters. 624 | * 625 | * @example 626 | * ```js 627 | * // Given: 628 | * // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby 629 | * // Route: /Chapter/:chapterId/Section/:sectionId 630 | * // 631 | * // Then 632 | * $routeParams ==> {chapterId:1, sectionId:2, search:'moby'} 633 | * ``` 634 | */ 635 | function $RouteParamsProvider() { 636 | this.$get = function() { return {}; }; 637 | } 638 | 639 | ngRouteModule.directive('ngView', ngViewFactory); 640 | ngRouteModule.directive('ngView', ngViewFillContentFactory); 641 | 642 | 643 | /** 644 | * @ngdoc directive 645 | * @name ngView 646 | * @restrict ECA 647 | * 648 | * @description 649 | * # Overview 650 | * `ngView` is a directive that complements the {@link ngRoute.$route $route} service by 651 | * including the rendered template of the current route into the main layout (`index.html`) file. 652 | * Every time the current route changes, the included view changes with it according to the 653 | * configuration of the `$route` service. 654 | * 655 | * Requires the {@link ngRoute `ngRoute`} module to be installed. 656 | * 657 | * @animations 658 | * enter - animation is used to bring new content into the browser. 659 | * leave - animation is used to animate existing content away. 660 | * 661 | * The enter and leave animation occur concurrently. 662 | * 663 | * @scope 664 | * @priority 400 665 | * @param {string=} onload Expression to evaluate whenever the view updates. 666 | * 667 | * @param {string=} autoscroll Whether `ngView` should call {@link ng.$anchorScroll 668 | * $anchorScroll} to scroll the viewport after the view is updated. 669 | * 670 | * - If the attribute is not set, disable scrolling. 671 | * - If the attribute is set without value, enable scrolling. 672 | * - Otherwise enable scrolling only if the `autoscroll` attribute value evaluated 673 | * as an expression yields a truthy value. 674 | * @example 675 | 678 | 679 |
680 | Choose: 681 | Moby | 682 | Moby: Ch1 | 683 | Gatsby | 684 | Gatsby: Ch4 | 685 | Scarlet Letter
686 | 687 |
688 |
689 |
690 |
691 | 692 |
$location.path() = {{main.$location.path()}}
693 |
$route.current.templateUrl = {{main.$route.current.templateUrl}}
694 |
$route.current.params = {{main.$route.current.params}}
695 |
$route.current.scope.name = {{main.$route.current.scope.name}}
696 |
$routeParams = {{main.$routeParams}}
697 |
698 |
699 | 700 | 701 |
702 | controller: {{book.name}}
703 | Book Id: {{book.params.bookId}}
704 |
705 |
706 | 707 | 708 |
709 | controller: {{chapter.name}}
710 | Book Id: {{chapter.params.bookId}}
711 | Chapter Id: {{chapter.params.chapterId}} 712 |
713 |
714 | 715 | 716 | .view-animate-container { 717 | position:relative; 718 | height:100px!important; 719 | position:relative; 720 | background:white; 721 | border:1px solid black; 722 | height:40px; 723 | overflow:hidden; 724 | } 725 | 726 | .view-animate { 727 | padding:10px; 728 | } 729 | 730 | .view-animate.ng-enter, .view-animate.ng-leave { 731 | -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; 732 | transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; 733 | 734 | display:block; 735 | width:100%; 736 | border-left:1px solid black; 737 | 738 | position:absolute; 739 | top:0; 740 | left:0; 741 | right:0; 742 | bottom:0; 743 | padding:10px; 744 | } 745 | 746 | .view-animate.ng-enter { 747 | left:100%; 748 | } 749 | .view-animate.ng-enter.ng-enter-active { 750 | left:0; 751 | } 752 | .view-animate.ng-leave.ng-leave-active { 753 | left:-100%; 754 | } 755 | 756 | 757 | 758 | angular.module('ngViewExample', ['ngRoute', 'ngAnimate'], 759 | function($routeProvider, $locationProvider) { 760 | $routeProvider.when('/Book/:bookId', { 761 | templateUrl: 'book.html', 762 | controller: BookCtrl, 763 | controllerAs: 'book' 764 | }); 765 | $routeProvider.when('/Book/:bookId/ch/:chapterId', { 766 | templateUrl: 'chapter.html', 767 | controller: ChapterCtrl, 768 | controllerAs: 'chapter' 769 | }); 770 | 771 | // configure html5 to get links working on jsfiddle 772 | $locationProvider.html5Mode(true); 773 | }); 774 | 775 | function MainCtrl($route, $routeParams, $location) { 776 | this.$route = $route; 777 | this.$location = $location; 778 | this.$routeParams = $routeParams; 779 | } 780 | 781 | function BookCtrl($routeParams) { 782 | this.name = "BookCtrl"; 783 | this.params = $routeParams; 784 | } 785 | 786 | function ChapterCtrl($routeParams) { 787 | this.name = "ChapterCtrl"; 788 | this.params = $routeParams; 789 | } 790 | 791 | 792 | 793 | it('should load and compile correct template', function() { 794 | element(by.linkText('Moby: Ch1')).click(); 795 | var content = element(by.css('[ng-view]')).getText(); 796 | expect(content).toMatch(/controller\: ChapterCtrl/); 797 | expect(content).toMatch(/Book Id\: Moby/); 798 | expect(content).toMatch(/Chapter Id\: 1/); 799 | 800 | element(by.partialLinkText('Scarlet')).click(); 801 | 802 | content = element(by.css('[ng-view]')).getText(); 803 | expect(content).toMatch(/controller\: BookCtrl/); 804 | expect(content).toMatch(/Book Id\: Scarlet/); 805 | }); 806 | 807 |
808 | */ 809 | 810 | 811 | /** 812 | * @ngdoc event 813 | * @name ngView#$viewContentLoaded 814 | * @eventType emit on the current ngView scope 815 | * @description 816 | * Emitted every time the ngView content is reloaded. 817 | */ 818 | ngViewFactory.$inject = ['$route', '$anchorScroll', '$animate']; 819 | function ngViewFactory( $route, $anchorScroll, $animate) { 820 | return { 821 | restrict: 'ECA', 822 | terminal: true, 823 | priority: 400, 824 | transclude: 'element', 825 | link: function(scope, $element, attr, ctrl, $transclude) { 826 | var currentScope, 827 | currentElement, 828 | previousElement, 829 | autoScrollExp = attr.autoscroll, 830 | onloadExp = attr.onload || ''; 831 | 832 | scope.$on('$routeChangeSuccess', update); 833 | update(); 834 | 835 | function cleanupLastView() { 836 | if(previousElement) { 837 | previousElement.remove(); 838 | previousElement = null; 839 | } 840 | if(currentScope) { 841 | currentScope.$destroy(); 842 | currentScope = null; 843 | } 844 | if(currentElement) { 845 | $animate.leave(currentElement, function() { 846 | previousElement = null; 847 | }); 848 | previousElement = currentElement; 849 | currentElement = null; 850 | } 851 | } 852 | 853 | function update() { 854 | var locals = $route.current && $route.current.locals, 855 | template = locals && locals.$template; 856 | 857 | if (angular.isDefined(template)) { 858 | var newScope = scope.$new(); 859 | var current = $route.current; 860 | 861 | // Note: This will also link all children of ng-view that were contained in the original 862 | // html. If that content contains controllers, ... they could pollute/change the scope. 863 | // However, using ng-view on an element with additional content does not make sense... 864 | // Note: We can't remove them in the cloneAttchFn of $transclude as that 865 | // function is called before linking the content, which would apply child 866 | // directives to non existing elements. 867 | var clone = $transclude(newScope, function(clone) { 868 | $animate.enter(clone, null, currentElement || $element, function onNgViewEnter () { 869 | if (angular.isDefined(autoScrollExp) 870 | && (!autoScrollExp || scope.$eval(autoScrollExp))) { 871 | $anchorScroll(); 872 | } 873 | }); 874 | cleanupLastView(); 875 | }); 876 | 877 | currentElement = clone; 878 | currentScope = current.scope = newScope; 879 | currentScope.$emit('$viewContentLoaded'); 880 | currentScope.$eval(onloadExp); 881 | } else { 882 | cleanupLastView(); 883 | } 884 | } 885 | } 886 | }; 887 | } 888 | 889 | // This directive is called during the $transclude call of the first `ngView` directive. 890 | // It will replace and compile the content of the element with the loaded template. 891 | // We need this directive so that the element content is already filled when 892 | // the link function of another directive on the same element as ngView 893 | // is called. 894 | ngViewFillContentFactory.$inject = ['$compile', '$controller', '$route']; 895 | function ngViewFillContentFactory($compile, $controller, $route) { 896 | return { 897 | restrict: 'ECA', 898 | priority: -400, 899 | link: function(scope, $element) { 900 | var current = $route.current, 901 | locals = current.locals; 902 | 903 | $element.html(locals.$template); 904 | 905 | var link = $compile($element.contents()); 906 | 907 | if (current.controller) { 908 | locals.$scope = scope; 909 | var controller = $controller(current.controller, locals); 910 | if (current.controllerAs) { 911 | scope[current.controllerAs] = controller; 912 | } 913 | $element.data('$ngControllerController', controller); 914 | $element.children().data('$ngControllerController', controller); 915 | } 916 | 917 | link(scope); 918 | } 919 | }; 920 | } 921 | 922 | 923 | })(window, window.angular); 924 | -------------------------------------------------------------------------------- /src/main/resources/public/js/angular-sanitize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.2.15-build.2399+sha.ca4ddfa 3 | * (c) 2010-2014 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) {'use strict'; 7 | 8 | var $sanitizeMinErr = angular.$$minErr('$sanitize'); 9 | 10 | /** 11 | * @ngdoc module 12 | * @name ngSanitize 13 | * @description 14 | * 15 | * # ngSanitize 16 | * 17 | * The `ngSanitize` module provides functionality to sanitize HTML. 18 | * 19 | * 20 | *
21 | * 22 | * See {@link ngSanitize.$sanitize `$sanitize`} for usage. 23 | */ 24 | 25 | /* 26 | * HTML Parser By Misko Hevery (misko@hevery.com) 27 | * based on: HTML Parser By John Resig (ejohn.org) 28 | * Original code by Erik Arvidsson, Mozilla Public License 29 | * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js 30 | * 31 | * // Use like so: 32 | * htmlParser(htmlString, { 33 | * start: function(tag, attrs, unary) {}, 34 | * end: function(tag) {}, 35 | * chars: function(text) {}, 36 | * comment: function(text) {} 37 | * }); 38 | * 39 | */ 40 | 41 | 42 | /** 43 | * @ngdoc service 44 | * @name $sanitize 45 | * @function 46 | * 47 | * @description 48 | * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are 49 | * then serialized back to properly escaped html string. This means that no unsafe input can make 50 | * it into the returned string, however, since our parser is more strict than a typical browser 51 | * parser, it's possible that some obscure input, which would be recognized as valid HTML by a 52 | * browser, won't make it through the sanitizer. 53 | * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and 54 | * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. 55 | * 56 | * @param {string} html Html input. 57 | * @returns {string} Sanitized html. 58 | * 59 | * @example 60 | 61 | 62 | 73 |
74 | Snippet: 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
DirectiveHowSourceRendered
ng-bind-htmlAutomatically uses $sanitize
<div ng-bind-html="snippet">
</div>
ng-bind-htmlBypass $sanitize by explicitly trusting the dangerous value 92 |
<div ng-bind-html="deliberatelyTrustDangerousSnippet()">
 93 | </div>
94 |
ng-bindAutomatically escapes
<div ng-bind="snippet">
</div>
104 |
105 |
106 | 107 | it('should sanitize the html snippet by default', function() { 108 | expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). 109 | toBe('

an html\nclick here\nsnippet

'); 110 | }); 111 | 112 | it('should inline raw snippet if bound to a trusted value', function() { 113 | expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()). 114 | toBe("

an html\n" + 115 | "click here\n" + 116 | "snippet

"); 117 | }); 118 | 119 | it('should escape snippet without any filter', function() { 120 | expect(element(by.css('#bind-default div')).getInnerHtml()). 121 | toBe("<p style=\"color:blue\">an html\n" + 122 | "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + 123 | "snippet</p>"); 124 | }); 125 | 126 | it('should update', function() { 127 | element(by.model('snippet')).clear(); 128 | element(by.model('snippet')).sendKeys('new text'); 129 | expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). 130 | toBe('new text'); 131 | expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe( 132 | 'new text'); 133 | expect(element(by.css('#bind-default div')).getInnerHtml()).toBe( 134 | "new <b onclick=\"alert(1)\">text</b>"); 135 | }); 136 |
137 |
138 | */ 139 | function $SanitizeProvider() { 140 | this.$get = ['$$sanitizeUri', function($$sanitizeUri) { 141 | return function(html) { 142 | var buf = []; 143 | htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { 144 | return !/^unsafe/.test($$sanitizeUri(uri, isImage)); 145 | })); 146 | return buf.join(''); 147 | }; 148 | }]; 149 | } 150 | 151 | function sanitizeText(chars) { 152 | var buf = []; 153 | var writer = htmlSanitizeWriter(buf, angular.noop); 154 | writer.chars(chars); 155 | return buf.join(''); 156 | } 157 | 158 | 159 | // Regular Expressions for parsing tags and attributes 160 | var START_TAG_REGEXP = 161 | /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/, 162 | END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/, 163 | ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, 164 | BEGIN_TAG_REGEXP = /^/g, 167 | DOCTYPE_REGEXP = /]*?)>/i, 168 | CDATA_REGEXP = //g, 169 | // Match everything outside of normal chars and " (quote character) 170 | NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; 171 | 172 | 173 | // Good source of info about elements and attributes 174 | // http://dev.w3.org/html5/spec/Overview.html#semantics 175 | // http://simon.html5.org/html-elements 176 | 177 | // Safe Void Elements - HTML5 178 | // http://dev.w3.org/html5/spec/Overview.html#void-elements 179 | var voidElements = makeMap("area,br,col,hr,img,wbr"); 180 | 181 | // Elements that you can, intentionally, leave open (and which close themselves) 182 | // http://dev.w3.org/html5/spec/Overview.html#optional-tags 183 | var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), 184 | optionalEndTagInlineElements = makeMap("rp,rt"), 185 | optionalEndTagElements = angular.extend({}, 186 | optionalEndTagInlineElements, 187 | optionalEndTagBlockElements); 188 | 189 | // Safe Block Elements - HTML5 190 | var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," + 191 | "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," + 192 | "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); 193 | 194 | // Inline Elements - HTML5 195 | var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," + 196 | "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," + 197 | "samp,small,span,strike,strong,sub,sup,time,tt,u,var")); 198 | 199 | 200 | // Special Elements (can contain anything) 201 | var specialElements = makeMap("script,style"); 202 | 203 | var validElements = angular.extend({}, 204 | voidElements, 205 | blockElements, 206 | inlineElements, 207 | optionalEndTagElements); 208 | 209 | //Attributes that have href and hence need to be sanitized 210 | var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap"); 211 | var validAttrs = angular.extend({}, uriAttrs, makeMap( 212 | 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+ 213 | 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+ 214 | 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+ 215 | 'scope,scrolling,shape,size,span,start,summary,target,title,type,'+ 216 | 'valign,value,vspace,width')); 217 | 218 | function makeMap(str) { 219 | var obj = {}, items = str.split(','), i; 220 | for (i = 0; i < items.length; i++) obj[items[i]] = true; 221 | return obj; 222 | } 223 | 224 | 225 | /** 226 | * @example 227 | * htmlParser(htmlString, { 228 | * start: function(tag, attrs, unary) {}, 229 | * end: function(tag) {}, 230 | * chars: function(text) {}, 231 | * comment: function(text) {} 232 | * }); 233 | * 234 | * @param {string} html string 235 | * @param {object} handler 236 | */ 237 | function htmlParser( html, handler ) { 238 | var index, chars, match, stack = [], last = html; 239 | stack.last = function() { return stack[ stack.length - 1 ]; }; 240 | 241 | while ( html ) { 242 | chars = true; 243 | 244 | // Make sure we're not in a script or style element 245 | if ( !stack.last() || !specialElements[ stack.last() ] ) { 246 | 247 | // Comment 248 | if ( html.indexOf("", index) === index) { 253 | if (handler.comment) handler.comment( html.substring( 4, index ) ); 254 | html = html.substring( index + 3 ); 255 | chars = false; 256 | } 257 | // DOCTYPE 258 | } else if ( DOCTYPE_REGEXP.test(html) ) { 259 | match = html.match( DOCTYPE_REGEXP ); 260 | 261 | if ( match ) { 262 | html = html.replace( match[0], ''); 263 | chars = false; 264 | } 265 | // end tag 266 | } else if ( BEGING_END_TAGE_REGEXP.test(html) ) { 267 | match = html.match( END_TAG_REGEXP ); 268 | 269 | if ( match ) { 270 | html = html.substring( match[0].length ); 271 | match[0].replace( END_TAG_REGEXP, parseEndTag ); 272 | chars = false; 273 | } 274 | 275 | // start tag 276 | } else if ( BEGIN_TAG_REGEXP.test(html) ) { 277 | match = html.match( START_TAG_REGEXP ); 278 | 279 | if ( match ) { 280 | html = html.substring( match[0].length ); 281 | match[0].replace( START_TAG_REGEXP, parseStartTag ); 282 | chars = false; 283 | } 284 | } 285 | 286 | if ( chars ) { 287 | index = html.indexOf("<"); 288 | 289 | var text = index < 0 ? html : html.substring( 0, index ); 290 | html = index < 0 ? "" : html.substring( index ); 291 | 292 | if (handler.chars) handler.chars( decodeEntities(text) ); 293 | } 294 | 295 | } else { 296 | html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), 297 | function(all, text){ 298 | text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1"); 299 | 300 | if (handler.chars) handler.chars( decodeEntities(text) ); 301 | 302 | return ""; 303 | }); 304 | 305 | parseEndTag( "", stack.last() ); 306 | } 307 | 308 | if ( html == last ) { 309 | throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " + 310 | "of html: {0}", html); 311 | } 312 | last = html; 313 | } 314 | 315 | // Clean up any remaining tags 316 | parseEndTag(); 317 | 318 | function parseStartTag( tag, tagName, rest, unary ) { 319 | tagName = angular.lowercase(tagName); 320 | if ( blockElements[ tagName ] ) { 321 | while ( stack.last() && inlineElements[ stack.last() ] ) { 322 | parseEndTag( "", stack.last() ); 323 | } 324 | } 325 | 326 | if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) { 327 | parseEndTag( "", tagName ); 328 | } 329 | 330 | unary = voidElements[ tagName ] || !!unary; 331 | 332 | if ( !unary ) 333 | stack.push( tagName ); 334 | 335 | var attrs = {}; 336 | 337 | rest.replace(ATTR_REGEXP, 338 | function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) { 339 | var value = doubleQuotedValue 340 | || singleQuotedValue 341 | || unquotedValue 342 | || ''; 343 | 344 | attrs[name] = decodeEntities(value); 345 | }); 346 | if (handler.start) handler.start( tagName, attrs, unary ); 347 | } 348 | 349 | function parseEndTag( tag, tagName ) { 350 | var pos = 0, i; 351 | tagName = angular.lowercase(tagName); 352 | if ( tagName ) 353 | // Find the closest opened tag of the same type 354 | for ( pos = stack.length - 1; pos >= 0; pos-- ) 355 | if ( stack[ pos ] == tagName ) 356 | break; 357 | 358 | if ( pos >= 0 ) { 359 | // Close all the open elements, up the stack 360 | for ( i = stack.length - 1; i >= pos; i-- ) 361 | if (handler.end) handler.end( stack[ i ] ); 362 | 363 | // Remove the open elements from the stack 364 | stack.length = pos; 365 | } 366 | } 367 | } 368 | 369 | var hiddenPre=document.createElement("pre"); 370 | var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/; 371 | /** 372 | * decodes all entities into regular string 373 | * @param value 374 | * @returns {string} A string with decoded entities. 375 | */ 376 | function decodeEntities(value) { 377 | if (!value) { return ''; } 378 | 379 | // Note: IE8 does not preserve spaces at the start/end of innerHTML 380 | // so we must capture them and reattach them afterward 381 | var parts = spaceRe.exec(value); 382 | var spaceBefore = parts[1]; 383 | var spaceAfter = parts[3]; 384 | var content = parts[2]; 385 | if (content) { 386 | hiddenPre.innerHTML=content.replace(//g, '>'); 412 | } 413 | 414 | /** 415 | * create an HTML/XML writer which writes to buffer 416 | * @param {Array} buf use buf.jain('') to get out sanitized html string 417 | * @returns {object} in the form of { 418 | * start: function(tag, attrs, unary) {}, 419 | * end: function(tag) {}, 420 | * chars: function(text) {}, 421 | * comment: function(text) {} 422 | * } 423 | */ 424 | function htmlSanitizeWriter(buf, uriValidator){ 425 | var ignore = false; 426 | var out = angular.bind(buf, buf.push); 427 | return { 428 | start: function(tag, attrs, unary){ 429 | tag = angular.lowercase(tag); 430 | if (!ignore && specialElements[tag]) { 431 | ignore = tag; 432 | } 433 | if (!ignore && validElements[tag] === true) { 434 | out('<'); 435 | out(tag); 436 | angular.forEach(attrs, function(value, key){ 437 | var lkey=angular.lowercase(key); 438 | var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); 439 | if (validAttrs[lkey] === true && 440 | (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { 441 | out(' '); 442 | out(key); 443 | out('="'); 444 | out(encodeEntities(value)); 445 | out('"'); 446 | } 447 | }); 448 | out(unary ? '/>' : '>'); 449 | } 450 | }, 451 | end: function(tag){ 452 | tag = angular.lowercase(tag); 453 | if (!ignore && validElements[tag] === true) { 454 | out(''); 457 | } 458 | if (tag == ignore) { 459 | ignore = false; 460 | } 461 | }, 462 | chars: function(chars){ 463 | if (!ignore) { 464 | out(encodeEntities(chars)); 465 | } 466 | } 467 | }; 468 | } 469 | 470 | 471 | // define ngSanitize module and register $sanitize service 472 | angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); 473 | 474 | /* global sanitizeText: false */ 475 | 476 | /** 477 | * @ngdoc filter 478 | * @name linky 479 | * @function 480 | * 481 | * @description 482 | * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and 483 | * plain email address links. 484 | * 485 | * Requires the {@link ngSanitize `ngSanitize`} module to be installed. 486 | * 487 | * @param {string} text Input text. 488 | * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in. 489 | * @returns {string} Html-linkified text. 490 | * 491 | * @usage 492 | 493 | * 494 | * @example 495 | 496 | 497 | 508 |
509 | Snippet: 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 521 | 524 | 525 | 526 | 527 | 530 | 533 | 534 | 535 | 536 | 537 | 538 | 539 |
FilterSourceRendered
linky filter 519 |
<div ng-bind-html="snippet | linky">
</div>
520 |
522 |
523 |
linky target 528 |
<div ng-bind-html="snippetWithTarget | linky:'_blank'">
</div>
529 |
531 |
532 |
no filter
<div ng-bind="snippet">
</div>
540 | 541 | 542 | it('should linkify the snippet with urls', function() { 543 | expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). 544 | toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' + 545 | 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); 546 | expect(element.all(by.css('#linky-filter a')).count()).toEqual(4); 547 | }); 548 | 549 | it('should not linkify snippet without the linky filter', function() { 550 | expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()). 551 | toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' + 552 | 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); 553 | expect(element.all(by.css('#escaped-html a')).count()).toEqual(0); 554 | }); 555 | 556 | it('should update', function() { 557 | element(by.model('snippet')).clear(); 558 | element(by.model('snippet')).sendKeys('new http://link.'); 559 | expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). 560 | toBe('new http://link.'); 561 | expect(element.all(by.css('#linky-filter a')).count()).toEqual(1); 562 | expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()) 563 | .toBe('new http://link.'); 564 | }); 565 | 566 | it('should work with the target property', function() { 567 | expect(element(by.id('linky-target')). 568 | element(by.binding("snippetWithTarget | linky:'_blank'")).getText()). 569 | toBe('http://angularjs.org/'); 570 | expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); 571 | }); 572 | 573 | 574 | */ 575 | angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { 576 | var LINKY_URL_REGEXP = 577 | /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/, 578 | MAILTO_REGEXP = /^mailto:/; 579 | 580 | return function(text, target) { 581 | if (!text) return text; 582 | var match; 583 | var raw = text; 584 | var html = []; 585 | var url; 586 | var i; 587 | while ((match = raw.match(LINKY_URL_REGEXP))) { 588 | // We can not end in these as they are sometimes found at the end of the sentence 589 | url = match[0]; 590 | // if we did not match ftp/http/mailto then assume mailto 591 | if (match[2] == match[3]) url = 'mailto:' + url; 592 | i = match.index; 593 | addText(raw.substr(0, i)); 594 | addLink(url, match[0].replace(MAILTO_REGEXP, '')); 595 | raw = raw.substring(i + match[0].length); 596 | } 597 | addText(raw); 598 | return $sanitize(html.join('')); 599 | 600 | function addText(text) { 601 | if (!text) { 602 | return; 603 | } 604 | html.push(sanitizeText(text)); 605 | } 606 | 607 | function addLink(url, text) { 608 | html.push(''); 617 | addText(text); 618 | html.push(''); 619 | } 620 | }; 621 | }]); 622 | 623 | 624 | })(window, window.angular); 625 | -------------------------------------------------------------------------------- /src/main/resources/public/scripts/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by shekhargulati on 10/06/14. 3 | */ 4 | 5 | var app = angular.module('todoapp', [ 6 | 'ngCookies', 7 | 'ngResource', 8 | 'ngSanitize', 9 | 'ngRoute' 10 | ]); 11 | 12 | app.config(function ($routeProvider) { 13 | $routeProvider.when('/', { 14 | templateUrl: 'views/list.html', 15 | controller: 'ListCtrl' 16 | }).when('/create', { 17 | templateUrl: 'views/create.html', 18 | controller: 'CreateCtrl' 19 | }).otherwise({ 20 | redirectTo: '/' 21 | }) 22 | }); 23 | 24 | app.controller('ListCtrl', function ($scope, $http) { 25 | $http.get('/api/v1/todos').success(function (data) { 26 | $scope.todos = data; 27 | }).error(function (data, status) { 28 | console.log('Error ' + data) 29 | }) 30 | 31 | $scope.todoStatusChanged = function (todo) { 32 | console.log(todo); 33 | $http.put('/api/v1/todos/' + todo.id, todo).success(function (data) { 34 | console.log('status changed'); 35 | }).error(function (data, status) { 36 | console.log('Error ' + data) 37 | }) 38 | } 39 | }); 40 | 41 | app.controller('CreateCtrl', function ($scope, $http, $location) { 42 | $scope.todo = { 43 | done: false 44 | }; 45 | 46 | $scope.createTodo = function () { 47 | console.log($scope.todo); 48 | $http.post('/api/v1/todos', $scope.todo).success(function (data) { 49 | $location.path('/'); 50 | }).error(function (data, status) { 51 | console.log('Error ' + data) 52 | }) 53 | } 54 | }); -------------------------------------------------------------------------------- /src/main/resources/public/views/create.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 | 7 |
8 |
9 |
10 |
11 | 12 |
13 |
14 |
-------------------------------------------------------------------------------- /src/main/resources/public/views/list.html: -------------------------------------------------------------------------------- 1 |

All Todos New Todo

3 | 4 | 5 | 6 | 9 | 12 | 15 | 16 | 17 | 18 | 19 | 22 | 25 | 28 | 29 |
7 | # 8 | 10 | Title 11 | 13 | Created On 14 | Done
{{$index+1}} 20 | {{todo.title}} 21 | 23 | {{todo.createdOn |date:'medium'}} 24 | 26 | 27 |
--------------------------------------------------------------------------------