View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.syncope.core.rest.cxf.service;
20  
21  import java.util.ArrayList;
22  import java.util.List;
23  import javax.ws.rs.core.Context;
24  import javax.ws.rs.core.EntityTag;
25  import javax.ws.rs.core.MultivaluedMap;
26  import javax.ws.rs.core.Response;
27  import javax.ws.rs.core.UriBuilder;
28  import javax.ws.rs.core.UriInfo;
29  import org.apache.commons.lang3.BooleanUtils;
30  import org.apache.commons.lang3.StringUtils;
31  import org.apache.cxf.jaxrs.ext.MessageContext;
32  import org.apache.cxf.jaxrs.ext.search.SearchContext;
33  import org.apache.syncope.common.lib.BaseBean;
34  import org.apache.syncope.common.lib.SyncopeClientException;
35  import org.apache.syncope.common.lib.SyncopeConstants;
36  import org.apache.syncope.common.lib.to.PagedResult;
37  import org.apache.syncope.common.lib.to.ProvisioningResult;
38  import org.apache.syncope.common.lib.types.ClientExceptionType;
39  import org.apache.syncope.common.rest.api.Preference;
40  import org.apache.syncope.common.rest.api.RESTHeaders;
41  import org.apache.syncope.common.rest.api.service.JAXRSService;
42  import org.apache.syncope.core.persistence.api.dao.AnyDAO;
43  import org.apache.syncope.core.persistence.api.dao.GroupDAO;
44  import org.apache.syncope.core.persistence.api.dao.UserDAO;
45  import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
46  import org.slf4j.Logger;
47  import org.slf4j.LoggerFactory;
48  
49  public abstract class AbstractService implements JAXRSService {
50  
51      protected static final Logger LOG = LoggerFactory.getLogger(AbstractService.class);
52  
53      protected static final String OPTIONS_ALLOW = "GET,POST,OPTIONS,HEAD";
54  
55      @Context
56      protected UriInfo uriInfo;
57  
58      @Context
59      protected MessageContext messageContext;
60  
61      @Context
62      protected SearchContext searchContext;
63  
64      protected String findActualKey(final AnyDAO<?> dao, final String pretendingKey) {
65          String actualKey = pretendingKey;
66          if (uriInfo.getPathParameters(true).containsKey("key")) {
67              String keyInPath = uriInfo.getPathParameters(true).get("key").get(0);
68              if (actualKey == null) {
69                  actualKey = keyInPath;
70              } else if (!actualKey.equals(keyInPath)) {
71                  SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidRequest);
72                  sce.getElements().add("Key specified in request does not match key in the path");
73                  throw sce;
74              }
75          }
76          if (actualKey == null) {
77              SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidRequest);
78              sce.getElements().add("Key is null");
79              throw sce;
80          }
81          if (!SyncopeConstants.UUID_PATTERN.matcher(actualKey).matches()) {
82              actualKey = dao instanceof UserDAO
83                      ? ((UserDAO) dao).findKey(actualKey)
84                      : dao instanceof GroupDAO
85                              ? ((GroupDAO) dao).findKey(actualKey)
86                              : null;
87          }
88          return actualKey;
89      }
90  
91      protected boolean isNullPriorityAsync() {
92          return BooleanUtils.toBoolean(
93                  messageContext.getHttpServletRequest().getHeader(RESTHeaders.NULL_PRIORITY_ASYNC));
94      }
95  
96      /**
97       * Reads {@code Prefer} header from request and parses into a {@code Preference} instance.
98       *
99       * @return a {@code Preference} instance matching the passed {@code Prefer} header,
100      * or {@code Preference.NONE} if missing.
101      */
102     protected Preference getPreference() {
103         return Preference.fromString(messageContext.getHttpServletRequest().getHeader(RESTHeaders.PREFER));
104     }
105 
106     protected Response.ResponseBuilder applyPreference(
107             final ProvisioningResult<?> provisioningResult, final Response.ResponseBuilder builder) {
108 
109         switch (getPreference()) {
110             case RETURN_NO_CONTENT:
111                 break;
112 
113             case RETURN_CONTENT:
114             case NONE:
115             default:
116                 builder.entity(provisioningResult);
117                 break;
118 
119         }
120         if (getPreference() == Preference.RETURN_CONTENT || getPreference() == Preference.RETURN_NO_CONTENT) {
121             builder.header(RESTHeaders.PREFERENCE_APPLIED, getPreference().toString());
122         }
123 
124         return builder;
125     }
126 
127     /**
128      * Builds response to successful {@code create} request, taking into account any {@code Prefer} header.
129      *
130      * @param provisioningResult the entity just created
131      * @return response to successful {@code create} request
132      */
133     protected Response createResponse(final ProvisioningResult<?> provisioningResult) {
134         String entityId = provisioningResult.getEntity().getKey();
135         Response.ResponseBuilder builder = Response.
136                 created(uriInfo.getAbsolutePathBuilder().path(entityId).build()).
137                 header(RESTHeaders.RESOURCE_KEY, entityId);
138 
139         return applyPreference(provisioningResult, builder).build();
140     }
141 
142     /**
143      * Builds response to successful modification request, taking into account any {@code Prefer} header.
144      *
145      * @param entity the entity just modified
146      * @return response to successful modification request
147      */
148     protected Response modificationResponse(final Object entity) {
149         Response.ResponseBuilder builder;
150         switch (getPreference()) {
151             case RETURN_NO_CONTENT:
152                 builder = Response.noContent();
153                 break;
154 
155             case RETURN_CONTENT:
156             case NONE:
157             default:
158                 builder = Response.ok(entity);
159                 break;
160         }
161         if (getPreference() == Preference.RETURN_CONTENT || getPreference() == Preference.RETURN_NO_CONTENT) {
162             builder.header(RESTHeaders.PREFERENCE_APPLIED, getPreference().toString());
163         }
164 
165         return builder.build();
166     }
167 
168     protected void checkETag(final String etag) {
169         Response.ResponseBuilder builder = messageContext.getRequest().evaluatePreconditions(new EntityTag(etag));
170         if (builder != null) {
171             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.ConcurrentModification);
172             sce.getElements().add("Mismatching ETag value");
173             throw sce;
174         }
175     }
176 
177     protected List<OrderByClause> getOrderByClauses(final String orderBy) {
178         if (StringUtils.isBlank(orderBy)) {
179             return List.of();
180         }
181 
182         List<OrderByClause> result = new ArrayList<>();
183 
184         for (String clause : orderBy.split(",")) {
185             String[] elems = clause.trim().split(" ");
186 
187             if (elems.length > 0 && StringUtils.isNotBlank(elems[0])) {
188                 OrderByClause obc = new OrderByClause();
189                 obc.setField(elems[0].trim());
190                 if (elems.length > 1 && StringUtils.isNotBlank(elems[1])) {
191                     obc.setDirection(elems[1].trim().equalsIgnoreCase(OrderByClause.Direction.ASC.name())
192                             ? OrderByClause.Direction.ASC : OrderByClause.Direction.DESC);
193                 }
194                 result.add(obc);
195             }
196         }
197 
198         return result;
199     }
200 
201     /**
202      * Builds a paged result out of a list of items and additional information.
203      *
204      * @param <T> result type
205      * @param list bare list of items to be returned
206      * @param page current page
207      * @param size requested size
208      * @param totalCount total result size (not considering pagination)
209      * @return paged result
210      */
211     protected <T extends BaseBean> PagedResult<T> buildPagedResult(
212             final List<T> list, final int page, final int size, final int totalCount) {
213 
214         PagedResult<T> result = new PagedResult<>();
215         result.getResult().addAll(list);
216 
217         result.setPage(page);
218         result.setSize(result.getResult().size());
219         result.setTotalCount(totalCount);
220 
221         UriBuilder builder = uriInfo.getAbsolutePathBuilder();
222         MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
223         queryParams.forEach((key, value) -> builder.queryParam(key, value.toArray()));
224 
225         if (result.getPage() > 1) {
226             result.setPrev(builder.
227                     replaceQueryParam(PARAM_PAGE, result.getPage() - 1).
228                     replaceQueryParam(PARAM_SIZE, size).
229                     build());
230         }
231         if ((result.getPage() - 1) * size + result.getSize() < totalCount) {
232             result.setNext(builder.
233                     replaceQueryParam(PARAM_PAGE, result.getPage() + 1).
234                     replaceQueryParam(PARAM_SIZE, size).
235                     build());
236         }
237 
238         return result;
239     }
240 }