diff --git a/dist/angular-spring-data-rest.js b/dist/angular-spring-data-rest.js index a58d291..8742e69 100644 --- a/dist/angular-spring-data-rest.js +++ b/dist/angular-spring-data-rest.js @@ -67,6 +67,12 @@ angular.module("spring-data-rest").provider("SpringDataRestAdapter", function () }, $get: ["$injector", function ($injector) { + /** + * Link map which contains the 'self' link as key and the list of already fetched link names as value. + * This is used to check that every link on each entity is only fetched once to avoid an infinite recursive loop. + * @type {{Object}} + */ + var linkMap = {}; /** * Returns the Angular $resource method which is configured with the given parameters. @@ -250,6 +256,12 @@ angular.module("spring-data-rest").provider("SpringDataRestAdapter", function () // if there are links to fetch, then process and fetch them if (fetchLinkNames != undefined) { + var self = data[config.linksKey][config.linksSelfLinkName][config.linksHrefKey]; + + // add the self link value as key and add an empty map to store all other links which are fetched for this entity + if (!linkMap[self]) { + linkMap[self] = []; + } // process all links angular.forEach(data[config.linksKey], function (linkValue, linkName) { @@ -258,15 +270,17 @@ angular.module("spring-data-rest").provider("SpringDataRestAdapter", function () if (linkName != config.linksSelfLinkName) { // check if: - // 1. the all link names key is given then fetch the link - // 2. the given key is equal - // 3. the given key is inside the array - if (fetchLinkNames == config.fetchAllKey || + // 1. the link was not fetched already + // 2. the all link names key is given then fetch the link + // 3. the given key is equal + // 4. the given key is inside the array + if (linkMap[self].indexOf(linkName) < 0 && + (fetchLinkNames == config.fetchAllKey || (typeof fetchLinkNames === "string" && linkName == fetchLinkNames) || - (fetchLinkNames instanceof Array && fetchLinkNames.indexOf(linkName) >= 0)) { + (fetchLinkNames instanceof Array && fetchLinkNames.indexOf(linkName) >= 0))) { promisesArray.push(fetchFunction(getProcessedUrl(data, linkName), linkName, processedData, fetchLinkNames, recursive)); - + linkMap[self].push(linkName); } } }); @@ -361,8 +375,14 @@ angular.module("spring-data-rest").provider("SpringDataRestAdapter", function () } }; + // empty the map and // return an object with the processData function - return {process: processData}; + return { + process: function(promiseOrData, fetchLinkNames, recursive) { + linkMap = {}; + return processData(promiseOrData, fetchLinkNames, recursive); + } + }; }] }; diff --git a/dist/angular-spring-data-rest.min.js b/dist/angular-spring-data-rest.min.js index 803bb18..4536916 100644 --- a/dist/angular-spring-data-rest.min.js +++ b/dist/angular-spring-data-rest.min.js @@ -3,4 +3,4 @@ * Copyright 2015 Guy Brand (@guy_labs) * https://github.com/guylabs/angular-spring-data-rest */ -!function(){"use strict";function a(b){return angular.forEach(arguments,function(c){c!==b&&angular.forEach(c,function(c,d){b[d]&&b[d].constructor&&b[d].constructor===Object?a(b[d],c):b[d]=c})}),angular.copy(b)}function b(a,b,c,d){var e=a[b];if(e){var f={};if(f[c]={},d===!0)angular.forEach(Object.keys(e),function(a){f[c][a]=e[a]});else{var g=Object.keys(e)[0];f[c]=e[g]}a=angular.extend(a,f),delete a[b]}return a}function c(a,b){return b&&(a=e(a)),a}function d(a,b,c){if(void 0==a||!a)throw new Error("The provided resource name '"+b+"' has no valid URL in the '"+c+"' property.");return a}function e(a){return a.replace(/{.*}/g,"")}function f(a){var b={},c=/{\?(.*)}/g,d=c.exec(a)[1].split(",");return angular.forEach(d,function(a){b[a]=""}),b}angular.module("spring-data-rest",["ngResource"]),angular.module("spring-data-rest").provider("SpringDataRestAdapter",function(){var e={linksKey:"_links",linksHrefKey:"href",linksSelfLinkName:"self",embeddedKey:"_embedded",embeddedNewKey:"_embeddedItems",embeddedNamedResources:!1,resourcesKey:"_resources",resourcesFunction:void 0,fetchFunction:void 0,fetchAllKey:"_allLinks"};return{config:function(b){if("undefined"!=typeof b){if(!angular.isObject(b))throw new Error("The given configuration '"+b+"' is not an object.");if(void 0!=b.resourcesFunction&&"function"!=typeof b.resourcesFunction)throw new Error("The given resource function '"+b.resourcesFunction+"' is not of type function.");if(void 0!=b.fetchFunction&&"function"!=typeof b.fetchFunction)throw new Error("The given fetch function '"+b.fetchFunction+"' is not of type function.");e=a(e,b)}return e},$get:["$injector",function(a){function g(b,c,d,f){return void 0==e.resourcesFunction?a.get("$resource")(b,c,d,f):e.resourcesFunction(b,c,d,f)}function h(b,c,d,f,g){if(void 0==e.fetchFunction){var h=[];return h.push(a.get("$http").get(b).then(function(a){return g?i(a.data,f,!0).then(function(a){d[c]=a}):i(a.data).then(function(a){d[c]=a})},function(b){return 404!=b.status?a.get("$q").reject(b):void 0})),a.get("$q").all(h)}return e.fetchFunction(b,c,d,f,g)}var i=function j(i,k,l){function m(a,b){var f=d(a[e.linksKey][b][e.linksHrefKey],b,e.linksHrefKey);return c(f,a[e.linksKey][b].templated)}function n(a){return"string"==typeof a&&a.indexOf("/")>0}function o(a){if(n(a)){var b=a.indexOf("/");return[a.substr(0,b),a.substr(b,a.length)]}}return a.get("$q").when(i).then(function(c){var d=function(a,b,d,h){var i=this[e.linksKey],j=b,k="";if(n(a)){var l=o(a);a=l[0],k=l[1]}if(angular.isObject(a)){if(!a.name)throw new Error("The provided resource object must contain a name property.");var p=a.parameters;return b&&angular.isObject(b)?j=p&&angular.isObject(p)?angular.extend(angular.copy(b),angular.copy(p)):angular.copy(b):p&&angular.isObject(p)&&(j=angular.copy(p)),angular.forEach(j,function(a,b){""===a&&delete j[b]}),g(m(c,a.name),j,d,h)}if(a in i)return g(m(c,a)+k,j,d,h);var q=[];return angular.forEach(i,function(a,b){if(a.templated){var c=f(a[e.linksHrefKey]);q.push({name:b,parameters:c})}else q.push({name:b})}),q};if(c&&c.data&&(c=c.data),!angular.isObject(c)||c instanceof Array)return a.get("$q").reject("Given data '"+c+"' is not of type object.");if(k&&!(k instanceof Array||"string"==typeof k))return a.get("$q").reject("Given fetch links '"+k+"' is not of type array or string.");var i=void 0,p=[];if(e.linksKey in c){var q={};q[e.resourcesKey]=d,i=angular.extend(angular.copy(c),q),void 0!=k&&angular.forEach(c[e.linksKey],function(a,b){b!=e.linksSelfLinkName&&(k==e.fetchAllKey||"string"==typeof k&&b==k||k instanceof Array&&k.indexOf(b)>=0)&&p.push(h(m(c,b),b,i,k,l))})}return e.embeddedKey in c&&(i||(i=angular.copy(c)),i=b(i,e.embeddedKey,e.embeddedNewKey,e.embeddedNamedResources),angular.forEach(i[e.embeddedNewKey],function(a,b){if(a instanceof Array&&a.length>0){var c,d=[];angular.forEach(a,function(a,b){angular.isObject(a)?(c=j({data:a},k,l).then(function(a){d[b]=a}),p.push(c)):d[b]=a}),c&&c.then(function(){i[e.embeddedNewKey][b]=d})}else angular.isObject(a)&&p.push(j({data:a},k,l).then(function(a){i[e.embeddedNewKey][b]=a}))})),a.get("$q").all(p).then(function(){return i?i:c})})};return{process:i}}]}}),angular.module("spring-data-rest").provider("SpringDataRestInterceptor",["$httpProvider","SpringDataRestAdapterProvider",function(a){return{apply:function(){a.interceptors.push("SpringDataRestInterceptor")},$get:["SpringDataRestAdapter","$q",function(a){return{response:function(b){return angular.isObject(b.data)?a.process(b.data).then(function(a){return b.data=a,b}):b}}}]}}])}(); \ No newline at end of file +!function(){"use strict";function a(b){return angular.forEach(arguments,function(c){c!==b&&angular.forEach(c,function(c,d){b[d]&&b[d].constructor&&b[d].constructor===Object?a(b[d],c):b[d]=c})}),angular.copy(b)}function b(a,b,c,d){var e=a[b];if(e){var f={};if(f[c]={},d===!0)angular.forEach(Object.keys(e),function(a){f[c][a]=e[a]});else{var g=Object.keys(e)[0];f[c]=e[g]}a=angular.extend(a,f),delete a[b]}return a}function c(a,b){return b&&(a=e(a)),a}function d(a,b,c){if(void 0==a||!a)throw new Error("The provided resource name '"+b+"' has no valid URL in the '"+c+"' property.");return a}function e(a){return a.replace(/{.*}/g,"")}function f(a){var b={},c=/{\?(.*)}/g,d=c.exec(a)[1].split(",");return angular.forEach(d,function(a){b[a]=""}),b}angular.module("spring-data-rest",["ngResource"]),angular.module("spring-data-rest").provider("SpringDataRestAdapter",function(){var e={linksKey:"_links",linksHrefKey:"href",linksSelfLinkName:"self",embeddedKey:"_embedded",embeddedNewKey:"_embeddedItems",embeddedNamedResources:!1,resourcesKey:"_resources",resourcesFunction:void 0,fetchFunction:void 0,fetchAllKey:"_allLinks"};return{config:function(b){if("undefined"!=typeof b){if(!angular.isObject(b))throw new Error("The given configuration '"+b+"' is not an object.");if(void 0!=b.resourcesFunction&&"function"!=typeof b.resourcesFunction)throw new Error("The given resource function '"+b.resourcesFunction+"' is not of type function.");if(void 0!=b.fetchFunction&&"function"!=typeof b.fetchFunction)throw new Error("The given fetch function '"+b.fetchFunction+"' is not of type function.");e=a(e,b)}return e},$get:["$injector",function(a){function g(b,c,d,f){return void 0==e.resourcesFunction?a.get("$resource")(b,c,d,f):e.resourcesFunction(b,c,d,f)}function h(b,c,d,f,g){if(void 0==e.fetchFunction){var h=[];return h.push(a.get("$http").get(b).then(function(a){return g?j(a.data,f,!0).then(function(a){d[c]=a}):j(a.data).then(function(a){d[c]=a})},function(b){return 404!=b.status?a.get("$q").reject(b):void 0})),a.get("$q").all(h)}return e.fetchFunction(b,c,d,f,g)}var i={},j=function k(j,l,m){function n(a,b){var f=d(a[e.linksKey][b][e.linksHrefKey],b,e.linksHrefKey);return c(f,a[e.linksKey][b].templated)}function o(a){return"string"==typeof a&&a.indexOf("/")>0}function p(a){if(o(a)){var b=a.indexOf("/");return[a.substr(0,b),a.substr(b,a.length)]}}return a.get("$q").when(j).then(function(c){var d=function(a,b,d,h){var i=this[e.linksKey],j=b,k="";if(o(a)){var l=p(a);a=l[0],k=l[1]}if(angular.isObject(a)){if(!a.name)throw new Error("The provided resource object must contain a name property.");var m=a.parameters;return b&&angular.isObject(b)?j=m&&angular.isObject(m)?angular.extend(angular.copy(b),angular.copy(m)):angular.copy(b):m&&angular.isObject(m)&&(j=angular.copy(m)),angular.forEach(j,function(a,b){""===a&&delete j[b]}),g(n(c,a.name),j,d,h)}if(a in i)return g(n(c,a)+k,j,d,h);var q=[];return angular.forEach(i,function(a,b){if(a.templated){var c=f(a[e.linksHrefKey]);q.push({name:b,parameters:c})}else q.push({name:b})}),q};if(c&&c.data&&(c=c.data),!angular.isObject(c)||c instanceof Array)return a.get("$q").reject("Given data '"+c+"' is not of type object.");if(l&&!(l instanceof Array||"string"==typeof l))return a.get("$q").reject("Given fetch links '"+l+"' is not of type array or string.");var j=void 0,q=[];if(e.linksKey in c){var r={};if(r[e.resourcesKey]=d,j=angular.extend(angular.copy(c),r),void 0!=l){var s=c[e.linksKey][e.linksSelfLinkName][e.linksHrefKey];i[s]||(i[s]=[]),angular.forEach(c[e.linksKey],function(a,b){b!=e.linksSelfLinkName&&i[s].indexOf(b)<0&&(l==e.fetchAllKey||"string"==typeof l&&b==l||l instanceof Array&&l.indexOf(b)>=0)&&(q.push(h(n(c,b),b,j,l,m)),i[s].push(b))})}}return e.embeddedKey in c&&(j||(j=angular.copy(c)),j=b(j,e.embeddedKey,e.embeddedNewKey,e.embeddedNamedResources),angular.forEach(j[e.embeddedNewKey],function(a,b){if(a instanceof Array&&a.length>0){var c,d=[];angular.forEach(a,function(a,b){angular.isObject(a)?(c=k({data:a},l,m).then(function(a){d[b]=a}),q.push(c)):d[b]=a}),c&&c.then(function(){j[e.embeddedNewKey][b]=d})}else angular.isObject(a)&&q.push(k({data:a},l,m).then(function(a){j[e.embeddedNewKey][b]=a}))})),a.get("$q").all(q).then(function(){return j?j:c})})};return{process:function(a,b,c){return i={},j(a,b,c)}}}]}}),angular.module("spring-data-rest").provider("SpringDataRestInterceptor",["$httpProvider","SpringDataRestAdapterProvider",function(a){return{apply:function(){a.interceptors.push("SpringDataRestInterceptor")},$get:["SpringDataRestAdapter","$q",function(a){return{response:function(b){return angular.isObject(b.data)?a.process(b.data).then(function(a){return b.data=a,b}):b}}}]}}])}(); \ No newline at end of file diff --git a/src/angular-spring-data-rest-provider.js b/src/angular-spring-data-rest-provider.js index aded52a..589d70f 100644 --- a/src/angular-spring-data-rest-provider.js +++ b/src/angular-spring-data-rest-provider.js @@ -55,6 +55,12 @@ angular.module("spring-data-rest").provider("SpringDataRestAdapter", function () }, $get: ["$injector", function ($injector) { + /** + * Link map which contains the 'self' link as key and the list of already fetched link names as value. + * This is used to check that every link on each entity is only fetched once to avoid an infinite recursive loop. + * @type {{Object}} + */ + var linkMap = {}; /** * Returns the Angular $resource method which is configured with the given parameters. @@ -238,6 +244,12 @@ angular.module("spring-data-rest").provider("SpringDataRestAdapter", function () // if there are links to fetch, then process and fetch them if (fetchLinkNames != undefined) { + var self = data[config.linksKey][config.linksSelfLinkName][config.linksHrefKey]; + + // add the self link value as key and add an empty map to store all other links which are fetched for this entity + if (!linkMap[self]) { + linkMap[self] = []; + } // process all links angular.forEach(data[config.linksKey], function (linkValue, linkName) { @@ -246,15 +258,17 @@ angular.module("spring-data-rest").provider("SpringDataRestAdapter", function () if (linkName != config.linksSelfLinkName) { // check if: - // 1. the all link names key is given then fetch the link - // 2. the given key is equal - // 3. the given key is inside the array - if (fetchLinkNames == config.fetchAllKey || + // 1. the link was not fetched already + // 2. the all link names key is given then fetch the link + // 3. the given key is equal + // 4. the given key is inside the array + if (linkMap[self].indexOf(linkName) < 0 && + (fetchLinkNames == config.fetchAllKey || (typeof fetchLinkNames === "string" && linkName == fetchLinkNames) || - (fetchLinkNames instanceof Array && fetchLinkNames.indexOf(linkName) >= 0)) { + (fetchLinkNames instanceof Array && fetchLinkNames.indexOf(linkName) >= 0))) { promisesArray.push(fetchFunction(getProcessedUrl(data, linkName), linkName, processedData, fetchLinkNames, recursive)); - + linkMap[self].push(linkName); } } }); @@ -349,8 +363,14 @@ angular.module("spring-data-rest").provider("SpringDataRestAdapter", function () } }; + // empty the map and // return an object with the processData function - return {process: processData}; + return { + process: function(promiseOrData, fetchLinkNames, recursive) { + linkMap = {}; + return processData(promiseOrData, fetchLinkNames, recursive); + } + }; }] }; diff --git a/test/angular-spring-data-rest-provider.spec.js b/test/angular-spring-data-rest-provider.spec.js index 08b672a..efc95e9 100644 --- a/test/angular-spring-data-rest-provider.spec.js +++ b/test/angular-spring-data-rest-provider.spec.js @@ -107,5 +107,51 @@ describe("the spring data rest adapter", function () { this.rootScope.$apply(); }); + it("must only fetch link once to avoid infinite loop", function () { + var allLinks = this.config.fetchAllKey; + var accidentHref = 'http://localhost:8080/api/reports/00001/accident'; + var reportHref = 'http://localhost:8080/api/accidents/00001/report'; + + this.httpBackend.whenGET(accidentHref).respond(200, mockDataAccident()); + this.httpBackend.expectGET(accidentHref); + + this.httpBackend.whenGET(reportHref).respond(200, mockDataReport()); + this.httpBackend.expectGET(reportHref); + + this.rawResponse = mockDataReport(); + SpringDataRestAdapter.process(this.rawResponse, allLinks, true).then(function (processedData) { + // expect that accident will not fetched twice + expect(processedData.accident).toBeDefined(); + expect(processedData.accident.report).toBeDefined(); + expect(processedData.accident.report.accident).not.toBeDefined(); + }); + + this.httpBackend.flush(); + this.rootScope.$apply(); + }); + + it("must only reinitialized the map when process called twice or more", function () { + var allLinks = this.config.fetchAllKey; + var accidentHref = 'http://localhost:8080/api/reports/00001/accident'; + var reportHref = 'http://localhost:8080/api/accidents/00001/report'; + + this.httpBackend.whenGET(accidentHref).respond(200, mockDataAccident()); + this.httpBackend.expectGET(accidentHref); + + this.httpBackend.whenGET(reportHref).respond(200, mockDataReport()); + this.httpBackend.expectGET(reportHref); + + this.rawResponse = mockDataReport(); + SpringDataRestAdapter.process(this.rawResponse, allLinks, true).then(function (processedData) { + SpringDataRestAdapter.process(mockDataReport(), allLinks, true).then(function (processedData2) { + // expect linkMap to be reinitialized after process method called twice + expect(JSON.stringify(processedData)).toEqual(JSON.stringify(processedData2)); + }); + }); + + this.httpBackend.flush(); + this.rootScope.$apply(); + }); + }); diff --git a/test/angular-spring-data-rest.helper.spec.js b/test/angular-spring-data-rest.helper.spec.js index fc15b5d..ca1b674 100644 --- a/test/angular-spring-data-rest.helper.spec.js +++ b/test/angular-spring-data-rest.helper.spec.js @@ -366,3 +366,30 @@ var mockWithRawEmbeddedValueTypes = function () { ); }; +var mockDataReport = function() { + return angular.copy({ + "reportNumber": "00001", + "_links": { + "self": { + "href": "http://localhost:8080/api/reports/00001" + }, + "accident": { + "href": "http://localhost:8080/api/reports/00001/accident" + } + } + }); +}; + +var mockDataAccident = function() { + return angular.copy({ + "accidentDate": "2015-07-05", + "_links": { + "self": { + "href": "http://localhost:8080/api/accidents/00001" + }, + "report": { + "href": "http://localhost:8080/api/accidents/00001/report" + } + } + }); +};