View Javadoc
1   /**
2    * This file Copyright (c) 2015-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.resteasy.client.command;
35  
36  import info.magnolia.commands.MgnlCommand;
37  import info.magnolia.context.Context;
38  import info.magnolia.registry.RegistrationException;
39  import info.magnolia.rest.client.registry.RestClientRegistry;
40  import info.magnolia.resteasy.client.RestEasyClient;
41  
42  import java.util.Map;
43  import java.util.concurrent.ConcurrentHashMap;
44  import java.util.concurrent.atomic.AtomicBoolean;
45  
46  import javax.inject.Inject;
47  import javax.ws.rs.NotAuthorizedException;
48  import javax.ws.rs.WebApplicationException;
49  import javax.ws.rs.core.MediaType;
50  
51  import com.fasterxml.jackson.databind.JsonNode;
52  
53  /**
54   * Abstract command which catches {@link WebApplicationException} to provide proper message from JSON response and also provides method to reauthenticate admin user.
55   *
56   * @param <S> Service class
57   */
58  public abstract class AbstractRestEasyCommand<S> extends MgnlCommand {
59  
60      private final RestClientRegistry restClientRegistry;
61      private final Class<S> serviceClass;
62      private final String restClientName;
63      private final String parameterResultName;
64      private static Map<Class, AtomicBoolean> relogingStatus = new ConcurrentHashMap<>();
65      private final Class<AbstractRestEasyCommand> clazz;
66  
67      @Inject
68      public AbstractRestEasyCommand(RestClientRegistry restClientRegistry, Class<S> serviceClass, String restClientName, String parameterResultName) {
69          this.restClientRegistry = restClientRegistry;
70          this.serviceClass = serviceClass;
71          this.restClientName = restClientName;
72          this.parameterResultName = parameterResultName;
73  
74          this.clazz = this.getClassToSynchronize();
75          if (relogingStatus.get(clazz) == null) {
76              relogingStatus.put(clazz, new AtomicBoolean(false));
77          }
78      }
79  
80      @Override
81      public boolean execute(Context ctx) throws RestEasyCommandException {
82          try {
83              Object result = this.onExecute(ctx);
84              ctx.put(parameterResultName, result);
85          } catch (NotAuthorizedException e) {
86              this.onUnauthorizedException(ctx, e);
87          } catch (WebApplicationException e) {
88              throw this.wrapException(e);
89          } catch (RegistrationException e) {
90              throw new RestEasyCommandException("Rest client not found: ", e);
91          }
92          return true;
93      }
94  
95      protected abstract JsonNode onExecute(Context ctx) throws RegistrationException;
96  
97      /**
98       * Handle authentication of admin user after e.g. session expiration.
99       *
100      * @return true if the login was successful, false otherwise
101      */
102     protected abstract boolean handleNotAuthorizedException(Context ctx, NotAuthorizedException e) throws RestEasyCommandException;
103 
104     protected S getService() throws RegistrationException {
105         return ((RestEasyClient) restClientRegistry.getRestClient(restClientName)).getClientService(serviceClass);
106     }
107 
108     protected RestEasyCommandException wrapException(WebApplicationException e) {
109         return new RestEasyCommandException(String.valueOf(this.getErrorCodeAndMessage(e)), e);
110     }
111 
112     /**
113      * Takes care of authentication which is triggered only for one of the requests running in parallel.
114      */
115     private void onUnauthorizedException(Context ctx, NotAuthorizedException e) throws RestEasyCommandException {
116 
117         AtomicBoolean reloging = relogingStatus.get(clazz);
118 
119         if (!reloging.get()) {
120             try {
121                 synchronized (clazz) {
122                     reloging.set(true);
123                     if (this.handleNotAuthorizedException(ctx, e)) {  //we should reexecute only if the admin login was successful
124                         Object result = this.onExecute(ctx);
125                         ctx.put(parameterResultName, result);
126                     } else {
127                         throw this.wrapException(e);
128                     }
129                 }
130             } catch (WebApplicationException e1) {
131                 throw this.wrapException(e1);
132             } catch (RegistrationException e1) {
133                 throw new RestEasyCommandException("Rest client not found: ", e);
134             } finally {
135                 reloging.set(false);
136             }
137         } else {
138             while (reloging.get()) ; //wait for another command to finish the authentication
139             // we don't know if the authentication was successful, but let's give it a try
140             try {
141                 Object result = this.onExecute(ctx);
142                 ctx.put(parameterResultName, result);
143             } catch (WebApplicationException e1) {
144                 throw this.wrapException(e1);
145             } catch (RegistrationException e1) {
146                 throw new RestEasyCommandException("Rest client not found: ", e);
147             }
148         }
149     }
150 
151     /**
152      * We want to synchronise all commands for certain service, not only the same class since services could implement more extended commands.
153      */
154     protected Class getClassToSynchronize() {
155         Class clazz = this.getClass();
156         while (!AbstractRestEasyCommand.class.equals(clazz.getSuperclass())) { //first descendant of AbstractRestEasyCommand
157             clazz = clazz.getSuperclass();
158         }
159         return clazz;
160     }
161 
162     private String getErrorCodeAndMessage(WebApplicationException e) {
163         String errorMessage = e.getMessage();
164         if (MediaType.APPLICATION_JSON_TYPE.isCompatible(e.getResponse().getMediaType()) && e.getResponse().hasEntity()) {
165             JsonNode node = e.getResponse().readEntity(JsonNode.class);
166             errorMessage = node.toString();
167             e.getResponse().close();
168         }
169         return errorMessage;
170     }
171 }