9d9aff45b6342fe24b42f6d1239df06461b8c665
[ccsdk/oran.git] /
1 /*-
2  * ========================LICENSE_START=================================
3  * ONAP : ccsdk oran
4  * ======================================================================
5  * Copyright (C) 2019-2020 Nordix Foundation. All rights reserved.
6  * ======================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ========================LICENSE_END===================================
19  */
20
21 package org.onap.ccsdk.oran.a1policymanagementservice.controllers.v2;
22
23 import com.google.gson.Gson;
24 import com.google.gson.GsonBuilder;
25
26 import io.swagger.annotations.Api;
27 import io.swagger.annotations.ApiOperation;
28 import io.swagger.annotations.ApiParam;
29 import io.swagger.annotations.ApiResponse;
30 import io.swagger.annotations.ApiResponses;
31
32 import java.lang.invoke.MethodHandles;
33 import java.time.Instant;
34 import java.util.ArrayList;
35 import java.util.Collection;
36 import java.util.List;
37
38 import lombok.Getter;
39
40 import org.onap.ccsdk.oran.a1policymanagementservice.clients.A1ClientFactory;
41 import org.onap.ccsdk.oran.a1policymanagementservice.controllers.VoidResponse;
42 import org.onap.ccsdk.oran.a1policymanagementservice.exceptions.ServiceException;
43 import org.onap.ccsdk.oran.a1policymanagementservice.repository.ImmutablePolicy;
44 import org.onap.ccsdk.oran.a1policymanagementservice.repository.Lock.LockType;
45 import org.onap.ccsdk.oran.a1policymanagementservice.repository.Policies;
46 import org.onap.ccsdk.oran.a1policymanagementservice.repository.Policy;
47 import org.onap.ccsdk.oran.a1policymanagementservice.repository.PolicyType;
48 import org.onap.ccsdk.oran.a1policymanagementservice.repository.PolicyTypes;
49 import org.onap.ccsdk.oran.a1policymanagementservice.repository.Ric;
50 import org.onap.ccsdk.oran.a1policymanagementservice.repository.Rics;
51 import org.onap.ccsdk.oran.a1policymanagementservice.repository.Service;
52 import org.onap.ccsdk.oran.a1policymanagementservice.repository.Services;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55 import org.springframework.beans.factory.annotation.Autowired;
56 import org.springframework.http.HttpStatus;
57 import org.springframework.http.MediaType;
58 import org.springframework.http.ResponseEntity;
59 import org.springframework.web.bind.annotation.DeleteMapping;
60 import org.springframework.web.bind.annotation.GetMapping;
61 import org.springframework.web.bind.annotation.PutMapping;
62 import org.springframework.web.bind.annotation.RequestBody;
63 import org.springframework.web.bind.annotation.RequestParam;
64 import org.springframework.web.bind.annotation.RestController;
65 import org.springframework.web.reactive.function.client.WebClientResponseException;
66 import reactor.core.publisher.Mono;
67
68 @RestController("PolicyControllerV2")
69 @Api(tags = {Consts.V2_API_NAME}, description = "Policy management")
70 public class PolicyController {
71
72     public static class RejectionException extends Exception {
73         private static final long serialVersionUID = 1L;
74
75         @Getter
76         private final HttpStatus status;
77
78         public RejectionException(String message, HttpStatus status) {
79             super(message);
80             this.status = status;
81         }
82     }
83
84     @Autowired
85     private Rics rics;
86     @Autowired
87     private PolicyTypes policyTypes;
88     @Autowired
89     private Policies policies;
90     @Autowired
91     private A1ClientFactory a1ClientFactory;
92     @Autowired
93     private Services services;
94
95     private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
96     private static Gson gson = new GsonBuilder() //
97         .serializeNulls() //
98         .create(); //
99
100     @GetMapping(path = Consts.V2_API_ROOT + "/policy-schemas", produces = MediaType.APPLICATION_JSON_VALUE)
101     @ApiOperation(value = "Returns policy type schema definitions")
102     @ApiResponses(
103         value = { //
104             @ApiResponse(code = 200, message = "Policy schemas", response = PolicySchemaList.class), //
105             @ApiResponse(code = 404, message = "Near-RT RIC is not found", response = ErrorResponse.ErrorInfo.class)})
106     public ResponseEntity<Object> getPolicySchemas( //
107         @ApiParam(
108             name = Consts.RIC_ID_PARAM,
109             required = false,
110             value = "The identity of the Near-RT RIC to get the definitions for.") //
111         @RequestParam(name = Consts.RIC_ID_PARAM, required = false) String ricId,
112         @ApiParam(
113             name = Consts.POLICY_TYPE_ID_PARAM,
114             required = true,
115             value = "The identity of the policy type to get the definition for. When this parameter is given, max one schema will be returned") //
116         @RequestParam(name = Consts.POLICY_TYPE_ID_PARAM, required = false) String policyTypeId) {
117         try {
118             if (ricId == null && policyTypeId == null) {
119                 Collection<PolicyType> types = this.policyTypes.getAll();
120                 return new ResponseEntity<>(toPolicyTypeSchemasJson(types), HttpStatus.OK);
121             } else if (ricId != null && policyTypeId != null) {
122                 Collection<PolicyType> types = new ArrayList<>();
123                 if (rics.getRic(ricId).isSupportingType(policyTypeId)) {
124                     types.add(policyTypes.getType(policyTypeId));
125                 }
126                 return new ResponseEntity<>(toPolicyTypeSchemasJson(types), HttpStatus.OK);
127             } else if (ricId != null) {
128                 Collection<PolicyType> types = rics.getRic(ricId).getSupportedPolicyTypes();
129                 return new ResponseEntity<>(toPolicyTypeSchemasJson(types), HttpStatus.OK);
130             } else {
131                 Collection<PolicyType> types = new ArrayList<>();
132                 types.add(policyTypes.getType(policyTypeId));
133                 return new ResponseEntity<>(toPolicyTypeSchemasJson(types), HttpStatus.OK);
134             }
135         } catch (ServiceException e) {
136             return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
137         }
138     }
139
140     @GetMapping(path = Consts.V2_API_ROOT + "/policy-types", produces = MediaType.APPLICATION_JSON_VALUE)
141     @ApiOperation(value = "Query policy type identities", produces = MediaType.APPLICATION_JSON_VALUE)
142     @ApiResponses(
143         value = {@ApiResponse(code = 200, message = "Policy type IDs", response = PolicyTypeIdList.class),
144             @ApiResponse(code = 404, message = "Near-RT RIC is not found", response = ErrorResponse.ErrorInfo.class)})
145     public ResponseEntity<Object> getPolicyTypes( //
146         @ApiParam(
147             name = Consts.RIC_ID_PARAM,
148             required = false,
149             value = "The identity of the Near-RT RIC to get types for.") //
150         @RequestParam(name = Consts.RIC_ID_PARAM, required = false) String ricId) {
151         if (ricId == null) {
152             Collection<PolicyType> types = this.policyTypes.getAll();
153             return new ResponseEntity<>(toPolicyTypeIdsJson(types), HttpStatus.OK);
154         } else {
155             try {
156                 Collection<PolicyType> types = rics.getRic(ricId).getSupportedPolicyTypes();
157                 return new ResponseEntity<>(toPolicyTypeIdsJson(types), HttpStatus.OK);
158             } catch (ServiceException e) {
159                 return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
160             }
161         }
162     }
163
164     @GetMapping(path = Consts.V2_API_ROOT + "/policy", produces = MediaType.APPLICATION_JSON_VALUE)
165     @ApiOperation(value = "Returns a policy configuration") //
166     @ApiResponses(
167         value = { //
168             @ApiResponse(code = 200, message = "Policy found", response = JsonObject.class), //
169             @ApiResponse(code = 404, message = "Policy is not found", response = ErrorResponse.ErrorInfo.class)} //
170     )
171     public ResponseEntity<Object> getPolicy( //
172         @ApiParam(name = Consts.POLICY_ID_PARAM, required = true, value = "The identity of the policy instance.") //
173         @RequestParam(name = Consts.POLICY_ID_PARAM, required = true) String id) {
174         try {
175             Policy p = policies.getPolicy(id);
176             return new ResponseEntity<>(p.json(), HttpStatus.OK);
177         } catch (ServiceException e) {
178             return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
179         }
180     }
181
182     @DeleteMapping(Consts.V2_API_ROOT + "/policy")
183     @ApiOperation(value = "Delete a policy")
184     @ApiResponses(
185         value = { //
186             @ApiResponse(code = 200, message = "Not used", response = VoidResponse.class),
187             @ApiResponse(code = 204, message = "Policy deleted", response = VoidResponse.class),
188             @ApiResponse(code = 404, message = "Policy is not found", response = ErrorResponse.ErrorInfo.class),
189             @ApiResponse(
190                 code = 423,
191                 message = "Near-RT RIC is not operational",
192                 response = ErrorResponse.ErrorInfo.class)})
193     public Mono<ResponseEntity<Object>> deletePolicy( //
194         @ApiParam(name = Consts.POLICY_ID_PARAM, required = true, value = "The identity of the policy instance.") //
195         @RequestParam(name = Consts.POLICY_ID_PARAM, required = true) String id) {
196         try {
197             Policy policy = policies.getPolicy(id);
198             keepServiceAlive(policy.ownerServiceId());
199             Ric ric = policy.ric();
200             return ric.getLock().lock(LockType.SHARED) //
201                 .flatMap(notUsed -> assertRicStateIdle(ric)) //
202                 .flatMap(notUsed -> a1ClientFactory.createA1Client(policy.ric())) //
203                 .doOnNext(notUsed -> policies.remove(policy)) //
204                 .flatMap(client -> client.deletePolicy(policy)) //
205                 .doOnNext(notUsed -> ric.getLock().unlockBlocking()) //
206                 .doOnError(notUsed -> ric.getLock().unlockBlocking()) //
207                 .flatMap(notUsed -> Mono.just(new ResponseEntity<>(HttpStatus.NO_CONTENT)))
208                 .onErrorResume(this::handleException);
209         } catch (ServiceException e) {
210             return ErrorResponse.createMono(e, HttpStatus.NOT_FOUND);
211         }
212     }
213
214     @PutMapping(path = Consts.V2_API_ROOT + "/policy", produces = MediaType.APPLICATION_JSON_VALUE)
215     @ApiOperation(value = "Create or update a policy")
216     @ApiResponses(
217         value = { //
218             @ApiResponse(code = 201, message = "Policy created", response = VoidResponse.class), //
219             @ApiResponse(code = 200, message = "Policy updated", response = VoidResponse.class), //
220             @ApiResponse(
221                 code = 423,
222                 message = "Near-RT RIC is not operational",
223                 response = ErrorResponse.ErrorInfo.class), //
224             @ApiResponse(
225                 code = 404,
226                 message = "Near-RT RIC or policy type is not found",
227                 response = ErrorResponse.ErrorInfo.class) //
228         })
229     public Mono<ResponseEntity<Object>> putPolicy( //
230         @ApiParam(name = Consts.POLICY_TYPE_ID_PARAM, required = false, value = "The identity of the policy type.") //
231         @RequestParam(name = Consts.POLICY_TYPE_ID_PARAM, required = false, defaultValue = "") String policyTypeId, //
232         @ApiParam(name = Consts.POLICY_ID_PARAM, required = true, value = "The identity of the policy instance.") //
233         @RequestParam(name = Consts.POLICY_ID_PARAM, required = true) String instanceId, //
234         @ApiParam(
235             name = Consts.RIC_ID_PARAM,
236             required = true,
237             value = "The identity of the Near-RT RIC where the policy will be " + //
238                 "created.") //
239         @RequestParam(name = Consts.RIC_ID_PARAM, required = true) String ricId, //
240         @ApiParam(
241             name = Consts.SERVICE_ID_PARAM,
242             required = true,
243             value = "The identity of the service creating the policy.") //
244         @RequestParam(name = Consts.SERVICE_ID_PARAM, required = true) String serviceId, //
245         @ApiParam(
246             name = Consts.TRANSIENT_PARAM,
247             required = false,
248             value = "If the policy is transient or not (boolean " + //
249                 "defaulted to false). A policy is transient if it will not be recreated in the Near-RT RIC " + //
250                 "when it has been lost (for instance due to a restart)") //
251         @RequestParam(name = Consts.TRANSIENT_PARAM, required = false, defaultValue = "false") boolean isTransient, //
252         @RequestBody Object jsonBody) {
253
254         String jsonString = gson.toJson(jsonBody);
255         Ric ric = rics.get(ricId);
256         PolicyType type = policyTypes.get(policyTypeId);
257         keepServiceAlive(serviceId);
258         if (ric == null || type == null) {
259             return ErrorResponse.createMono("Near-RT RIC or policy type not found", HttpStatus.NOT_FOUND);
260         }
261         Policy policy = ImmutablePolicy.builder() //
262             .id(instanceId) //
263             .json(jsonString) //
264             .type(type) //
265             .ric(ric) //
266             .ownerServiceId(serviceId) //
267             .lastModified(Instant.now()) //
268             .isTransient(isTransient) //
269             .build();
270
271         final boolean isCreate = this.policies.get(policy.id()) == null;
272
273         return ric.getLock().lock(LockType.SHARED) //
274             .flatMap(notUsed -> assertRicStateIdle(ric)) //
275             .flatMap(notUsed -> checkSupportedType(ric, type)) //
276             .flatMap(notUsed -> validateModifiedPolicy(policy)) //
277             .flatMap(notUsed -> a1ClientFactory.createA1Client(ric)) //
278             .flatMap(client -> client.putPolicy(policy)) //
279             .doOnNext(notUsed -> policies.put(policy)) //
280             .doOnNext(notUsed -> ric.getLock().unlockBlocking()) //
281             .doOnError(trowable -> ric.getLock().unlockBlocking()) //
282             .flatMap(notUsed -> Mono.just(new ResponseEntity<>(isCreate ? HttpStatus.CREATED : HttpStatus.OK))) //
283             .onErrorResume(this::handleException);
284     }
285
286     private Mono<ResponseEntity<Object>> handleException(Throwable throwable) {
287         if (throwable instanceof WebClientResponseException) {
288             WebClientResponseException e = (WebClientResponseException) throwable;
289             return ErrorResponse.createMono(e.getResponseBodyAsString(), e.getStatusCode());
290         } else if (throwable instanceof RejectionException) {
291             RejectionException e = (RejectionException) throwable;
292             return ErrorResponse.createMono(e.getMessage(), e.getStatus());
293         } else {
294             return ErrorResponse.createMono(throwable.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
295         }
296     }
297
298     private Mono<Object> validateModifiedPolicy(Policy policy) {
299         // Check that ric is not updated
300         Policy current = this.policies.get(policy.id());
301         if (current != null && !current.ric().id().equals(policy.ric().id())) {
302             RejectionException e = new RejectionException("Policy cannot change RIC, policyId: " + current.id() + //
303                 ", RIC ID: " + current.ric().id() + //
304                 ", new ID: " + policy.ric().id(), HttpStatus.CONFLICT);
305             logger.debug("Request rejected, {}", e.getMessage());
306             return Mono.error(e);
307         }
308         return Mono.just("{}");
309     }
310
311     private Mono<Object> checkSupportedType(Ric ric, PolicyType type) {
312         if (!ric.isSupportingType(type.id())) {
313             logger.debug("Request rejected, type not supported, RIC: {}", ric);
314             RejectionException e = new RejectionException("Type: " + type.id() + " not supported by RIC: " + ric.id(),
315                 HttpStatus.NOT_FOUND);
316             return Mono.error(e);
317         }
318         return Mono.just("{}");
319     }
320
321     private Mono<Object> assertRicStateIdle(Ric ric) {
322         if (ric.getState() == Ric.RicState.AVAILABLE) {
323             return Mono.just("{}");
324         } else {
325             logger.debug("Request rejected RIC not IDLE, ric: {}", ric);
326             RejectionException e = new RejectionException(
327                 "Ric is not operational, RIC name: " + ric.id() + ", state: " + ric.getState(), HttpStatus.LOCKED);
328             return Mono.error(e);
329         }
330     }
331
332     static final String GET_POLICIES_QUERY_DETAILS =
333         "Returns a list of A1 policies matching given search criteria. <br>" //
334             + "If several query parameters are defined, the policies matching all conditions are returned.";
335
336     @GetMapping(path = Consts.V2_API_ROOT + "/policies", produces = MediaType.APPLICATION_JSON_VALUE)
337     @ApiOperation(value = "Query for existing A1 policies", notes = GET_POLICIES_QUERY_DETAILS)
338     @ApiResponses(
339         value = { //
340             @ApiResponse(code = 200, message = "Policies", response = PolicyInfoList.class),
341             @ApiResponse(
342                 code = 404,
343                 message = "Near-RT RIC, policy type or service not found",
344                 response = ErrorResponse.ErrorInfo.class)})
345     public ResponseEntity<Object> getPolicies( //
346         @ApiParam(
347             name = Consts.POLICY_TYPE_ID_PARAM,
348             required = false,
349             value = "The identity of the policy type to get policies for.") //
350         @RequestParam(name = Consts.POLICY_TYPE_ID_PARAM, required = false) String type, //
351         @ApiParam(
352             name = Consts.RIC_ID_PARAM,
353             required = false,
354             value = "The identity of the Near-RT RIC to get policies for.") //
355         @RequestParam(name = Consts.RIC_ID_PARAM, required = false) String ric, //
356         @ApiParam(
357             name = Consts.SERVICE_ID_PARAM,
358             required = false,
359             value = "The identity of the service to get policies for.") //
360         @RequestParam(name = Consts.SERVICE_ID_PARAM, required = false) String service) //
361     {
362         if ((type != null && this.policyTypes.get(type) == null)) {
363             return ErrorResponse.create("Policy type not found", HttpStatus.NOT_FOUND);
364         }
365         if ((ric != null && this.rics.get(ric) == null)) {
366             return ErrorResponse.create("Near-RT RIC not found", HttpStatus.NOT_FOUND);
367         }
368
369         String filteredPolicies = policiesToJson(filter(type, ric, service));
370         return new ResponseEntity<>(filteredPolicies, HttpStatus.OK);
371     }
372
373     @GetMapping(path = Consts.V2_API_ROOT + "/policy-ids", produces = MediaType.APPLICATION_JSON_VALUE)
374     @ApiOperation(value = "Query policies, only policy identities are returned", notes = GET_POLICIES_QUERY_DETAILS)
375     @ApiResponses(
376         value = { //
377             @ApiResponse(code = 200, message = "Policy identities", response = PolicyIdList.class),
378             @ApiResponse(
379                 code = 404,
380                 message = "Near-RT RIC or type not found",
381                 response = ErrorResponse.ErrorInfo.class)})
382     public ResponseEntity<Object> getPolicyIds( //
383         @ApiParam(
384             name = Consts.POLICY_TYPE_ID_PARAM,
385             required = false,
386             value = "The identity of the policy type to get policies for.") //
387         @RequestParam(name = Consts.POLICY_TYPE_ID_PARAM, required = false) String policyTypeId, //
388         @ApiParam(
389             name = Consts.RIC_ID_PARAM,
390             required = false,
391             value = "The identity of the Near-RT RIC to get policies for.") //
392         @RequestParam(name = Consts.RIC_ID_PARAM, required = false) String ricId, //
393         @ApiParam(
394             name = Consts.SERVICE_ID_PARAM,
395             required = false,
396             value = "The identity of the service to get policies for.") //
397         @RequestParam(name = Consts.SERVICE_ID_PARAM, required = false) String serviceId) //
398     {
399         if ((policyTypeId != null && this.policyTypes.get(policyTypeId) == null)) {
400             return ErrorResponse.create("Policy type not found", HttpStatus.NOT_FOUND);
401         }
402         if ((ricId != null && this.rics.get(ricId) == null)) {
403             return ErrorResponse.create("Near-RT RIC not found", HttpStatus.NOT_FOUND);
404         }
405
406         String policyIdsJson = toPolicyIdsJson(filter(policyTypeId, ricId, serviceId));
407         return new ResponseEntity<>(policyIdsJson, HttpStatus.OK);
408     }
409
410     @GetMapping(path = Consts.V2_API_ROOT + "/policy-status", produces = MediaType.APPLICATION_JSON_VALUE)
411     @ApiOperation(value = "Returns a policy status") //
412     @ApiResponses(
413         value = { //
414             @ApiResponse(code = 200, message = "Policy status", response = JsonObject.class), //
415             @ApiResponse(code = 404, message = "Policy is not found", response = ErrorResponse.ErrorInfo.class)} //
416     )
417     public Mono<ResponseEntity<Object>> getPolicyStatus( //
418         @ApiParam(name = Consts.POLICY_ID_PARAM, required = true, value = "The identity of the policy.") //
419         @RequestParam(name = Consts.POLICY_ID_PARAM, required = true) String policyId) {
420         try {
421             Policy policy = policies.getPolicy(policyId);
422
423             return a1ClientFactory.createA1Client(policy.ric()) //
424                 .flatMap(client -> client.getPolicyStatus(policy)) //
425                 .flatMap(status -> Mono.just(new ResponseEntity<>((Object) status, HttpStatus.OK)))
426                 .onErrorResume(this::handleException);
427         } catch (ServiceException e) {
428             return ErrorResponse.createMono(e, HttpStatus.NOT_FOUND);
429         }
430     }
431
432     private void keepServiceAlive(String name) {
433         Service s = this.services.get(name);
434         if (s != null) {
435             s.keepAlive();
436         }
437     }
438
439     private boolean include(String filter, String value) {
440         return filter == null || value.equals(filter);
441     }
442
443     private Collection<Policy> filter(Collection<Policy> collection, String type, String ric, String service) {
444         if (type == null && ric == null && service == null) {
445             return collection;
446         }
447         List<Policy> filtered = new ArrayList<>();
448         for (Policy p : collection) {
449             if (include(type, p.type().id()) && include(ric, p.ric().id()) && include(service, p.ownerServiceId())) {
450                 filtered.add(p);
451             }
452         }
453         return filtered;
454     }
455
456     private Collection<Policy> filter(String type, String ric, String service) {
457         if (type != null) {
458             return filter(policies.getForType(type), null, ric, service);
459         } else if (service != null) {
460             return filter(policies.getForService(service), type, ric, null);
461         } else if (ric != null) {
462             return filter(policies.getForRic(ric), type, null, service);
463         } else {
464             return policies.getAll();
465         }
466     }
467
468     private String policiesToJson(Collection<Policy> policies) {
469         List<PolicyInfo> v = new ArrayList<>(policies.size());
470         for (Policy p : policies) {
471             PolicyInfo policyInfo = new PolicyInfo();
472             policyInfo.policyId = p.id();
473             policyInfo.policyData = fromJson(p.json());
474             policyInfo.ricId = p.ric().id();
475             policyInfo.policyTypeId = p.type().id();
476             policyInfo.serviceId = p.ownerServiceId();
477             policyInfo.lastModified = p.lastModified().toString();
478             if (!policyInfo.validate()) {
479                 logger.error("BUG, all fields must be set");
480             }
481             v.add(policyInfo);
482         }
483         PolicyInfoList list = new PolicyInfoList(v);
484         return gson.toJson(list);
485     }
486
487     private Object fromJson(String jsonStr) {
488         return gson.fromJson(jsonStr, Object.class);
489     }
490
491     private String toPolicyTypeSchemasJson(Collection<PolicyType> types) {
492
493         Collection<String> schemas = new ArrayList<>();
494         for (PolicyType t : types) {
495             schemas.add(t.schema());
496         }
497         PolicySchemaList res = new PolicySchemaList(schemas);
498         return gson.toJson(res);
499     }
500
501     private String toPolicyTypeIdsJson(Collection<PolicyType> types) {
502         List<String> v = new ArrayList<>(types.size());
503         for (PolicyType t : types) {
504             v.add(t.id());
505         }
506         PolicyTypeIdList ids = new PolicyTypeIdList(v);
507         return gson.toJson(ids);
508     }
509
510     private String toPolicyIdsJson(Collection<Policy> policies) {
511         List<String> v = new ArrayList<>(policies.size());
512         for (Policy p : policies) {
513             v.add(p.id());
514         }
515         return gson.toJson(new PolicyIdList(v));
516     }
517
518 }