diff --git a/front50-core/src/main/groovy/com/netflix/spinnaker/front50/model/application/ApplicationDAO.groovy b/front50-core/src/main/groovy/com/netflix/spinnaker/front50/model/application/ApplicationDAO.groovy index 99332beff..a2df8058a 100644 --- a/front50-core/src/main/groovy/com/netflix/spinnaker/front50/model/application/ApplicationDAO.groovy +++ b/front50-core/src/main/groovy/com/netflix/spinnaker/front50/model/application/ApplicationDAO.groovy @@ -29,7 +29,8 @@ public interface ApplicationDAO extends ItemDAO { Collection getApplicationHistory(String name, int limit) static class Searcher { - static Collection search(Collection searchableApplications, Map attributes) { + static Collection search(Collection searchableApplications, + Map attributes) { attributes = attributes.collect { k,v -> [k.toLowerCase(), v] }.collectEntries() if (attributes["accounts"]) { @@ -65,7 +66,35 @@ public interface ApplicationDAO extends ItemDAO { throw new NotFoundException("No Application found for search criteria $attributes") } - return items + return items.sort { Application a, Application b -> + return score(b, attributes) - score(a, attributes) + } + } + + static int score(Application application, Map attributes) { + return attributes.collect { key, value -> + return score(application, key, value) + }?.sum() as Integer ?: 0 + } + + static int score(Application application, String attributeName, String attributeValue) { + if (!application.hasProperty(attributeName)) { + return 0 + } + + def attribute = application[attributeName].toString().toLowerCase() + def indexOf = attribute.indexOf(attributeValue.toLowerCase()) + + // what percentage of the value matched + def coverage = ((double) attributeValue.length() / attribute.length()) * 100 + + // where did the match occur, bonus points for it occurring close to the start + def boost = attribute.length() - indexOf + + // scale boost based on coverage percentage + def scaledBoost = ((double) coverage / 100) * boost + + return indexOf < 0 ? 0 : coverage + scaledBoost } } } diff --git a/front50-core/src/test/groovy/com/netflix/spinnaker/front50/model/application/ApplicationDAOSpec.groovy b/front50-core/src/test/groovy/com/netflix/spinnaker/front50/model/application/ApplicationDAOSpec.groovy new file mode 100644 index 000000000..b3c65cac6 --- /dev/null +++ b/front50-core/src/test/groovy/com/netflix/spinnaker/front50/model/application/ApplicationDAOSpec.groovy @@ -0,0 +1,84 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.netflix.spinnaker.front50.model.application + +import spock.lang.Specification +import spock.lang.Unroll + +class ApplicationDAOSpec extends Specification { + def "should support sorting by a single attribute"() { + given: + def applications = [ + new Application(name: "YOUR APPLICATION"), + new Application(name: "APPLICATION"), + new Application(name: "MY APPLICATION"), + ] + + expect: + ApplicationDAO.Searcher.search(applications, ["name": "app"])*.name == [ + "APPLICATION", "MY APPLICATION", "YOUR APPLICATION" + ] + } + + def "should support sorting by multiple attributes"() { + given: + def applications = [ + new Application(name: "APPLICATION 123", description: "abcd"), + new Application(name: "APPLICATION 1", description: "bcda"), + new Application(name: "APPLICATION 12", description: "cdab"), + ] + + expect: + ApplicationDAO.Searcher.search(applications, ["name": "app", "description": "cd"])*.name == [ + "APPLICATION 1", "APPLICATION 12", "APPLICATION 123" + ] + } + + @Unroll + def "should calculate correct single attribute scores"() { + given: + def application = new Application(applicationAttributes) + + expect: + ApplicationDAO.Searcher.score(application, attributeName, attributeValue) == score + + where: + applicationAttributes | attributeName | attributeValue || score + ["name": "application"] | "name" | "application" || 111 + ["name": "application"] | "name" | "app" || 30 + ["name": "application"] | "name" | "ppl" || 29 + ["name": "application"] | "name" | "ion" || 28 + ["name": "application"] | "name" | "does_not_match" || 0 + } + + @Unroll + def "should calculate correct multiple attribute scores"() { + given: + def application = new Application(applicationAttributes) + + expect: + ApplicationDAO.Searcher.score(application, attributes) == score + + where: + applicationAttributes | attributes || score + ["name": "Netflix"] | ["name": "flix"] || 59 + ["name": "Netflix", description: "Spinnaker"] | ["name": "flix", description: "Spin"] || 107 + ["name": "Netflix", description: "Spinnaker"] | ["name": "flix", description: "ker"] || 93 + ["name": "Netflix", description: "Spinnaker"] | ["name": "flix", owner: "netflix"] || 59 + } +} diff --git a/front50-web/src/main/groovy/com/netflix/spinnaker/front50/controllers/v2/ApplicationsController.groovy b/front50-web/src/main/groovy/com/netflix/spinnaker/front50/controllers/v2/ApplicationsController.groovy index cc08ff65e..04ae67fed 100644 --- a/front50-web/src/main/groovy/com/netflix/spinnaker/front50/controllers/v2/ApplicationsController.groovy +++ b/front50-web/src/main/groovy/com/netflix/spinnaker/front50/controllers/v2/ApplicationsController.groovy @@ -56,9 +56,6 @@ public class ApplicationsController { @Autowired(required = false) List applicationEventListeners = [] - @Autowired - Registry registry - @PreAuthorize("@fiatPermissionEvaluator.storeWholePermission()") @PostFilter("hasPermission(filterObject.name, 'APPLICATION', 'READ')") @ApiOperation(value = "", notes = """Fetch all applications. @@ -67,12 +64,18 @@ public class ApplicationsController { - ?email=my@email.com - ?email=my@email.com&name=flex""") @RequestMapping(method = RequestMethod.GET) - Set applications(@RequestParam Map params) { + Set applications(@RequestParam(value = "pageSize", required = false) Integer pageSize, + @RequestParam Map params) { + params.remove("pageSize") + + def applications if (params.isEmpty()) { - return applicationDAO.all() + applications = applicationDAO.all().sort { it.name } + } else { + applications = applicationDAO.search(params) } - return applicationDAO.search(params) + return pageSize ? applications.asList().subList(0, Math.min(pageSize, applications.size())) : applications } // TODO(ttomsu): Think through application creation permissions. diff --git a/front50-web/src/test/groovy/com/netflix/spinnaker/front50/controllers/v2/ApplicationsControllerTck.groovy b/front50-web/src/test/groovy/com/netflix/spinnaker/front50/controllers/v2/ApplicationsControllerTck.groovy index 5acd3bd46..47ac76f0e 100644 --- a/front50-web/src/test/groovy/com/netflix/spinnaker/front50/controllers/v2/ApplicationsControllerTck.groovy +++ b/front50-web/src/test/groovy/com/netflix/spinnaker/front50/controllers/v2/ApplicationsControllerTck.groovy @@ -322,6 +322,24 @@ abstract class ApplicationsControllerTck extends Specification { then: response.andExpect status().isOk() response.andExpect content().string(new ObjectMapper().writeValueAsString([dao.findByName("SAMPLEAPP")])) + + when: + response = mockMvc.perform( + get("/v2/applications?name=sample&pageSize=9999") + ) + + then: + response.andExpect status().isOk() + response.andExpect content().string(new ObjectMapper().writeValueAsString([dao.findByName("SAMPLEAPP"), dao.findByName("SAMPLEAPP-2")])) + + when: + response = mockMvc.perform( + get("/v2/applications?name=sample&pageSize=1") + ) + + then: + response.andExpect status().isOk() + response.andExpect content().string(new ObjectMapper().writeValueAsString([dao.findByName("SAMPLEAPP")])) } private Map toMap(Application application) {