View Javadoc
1   /*
2    * Copyright (c) 2023. Roland T. Lichti, Kaiserpfalz EDV-Service.
3    *
4    * This program is free software: you can redistribute it and/or modify
5    * it under the terms of the GNU General Public License as published by
6    * the Free Software Foundation, either version 3 of the License, or
7    * (at your option) any later version.
8    *
9    * This program is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU General Public License for more details.
13   *
14   * You should have received a copy of the GNU General Public License
15   * along with this program.  If not, see <https://www.gnu.org/licenses/>.
16   */
17  
18  package de.kaiserpfalzedv.commons.rest.workflow;
19  
20  import java.io.IOException;
21  import java.time.Duration;
22  import java.time.OffsetDateTime;
23  import java.time.ZoneId;
24  import java.time.format.DateTimeParseException;
25  import java.time.temporal.ChronoUnit;
26  import java.time.temporal.TemporalAmount;
27  
28  import org.slf4j.MDC;
29  
30  import de.kaiserpfalzedv.commons.core.workflow.WorkflowDetailInfoImpl;
31  import de.kaiserpfalzedv.commons.core.workflow.WorkflowInfoImpl;
32  import jakarta.inject.Inject;
33  import jakarta.inject.Singleton;
34  import jakarta.servlet.Filter;
35  import jakarta.servlet.FilterChain;
36  import jakarta.servlet.ServletException;
37  import jakarta.servlet.ServletRequest;
38  import jakarta.servlet.ServletResponse;
39  import jakarta.servlet.http.HttpServletRequest;
40  import jakarta.servlet.http.HttpServletResponse;
41  import lombok.RequiredArgsConstructor;
42  import lombok.extern.slf4j.Slf4j;
43  
44  /**
45   * <p>WorkflowFilter -- Handles the WorkflowInfo for REST requests.</p>
46   *
47   * @author klenkes74 {@literal <rlichti@kaiserpfalz-edv.de>}
48   * @since 2.0.0  2022-01-04
49   */
50  @RequiredArgsConstructor(onConstructor = @__(@Inject))
51  @Singleton
52  @Slf4j
53  public class WorkflowFilter implements Filter {
54      public static final String WORKFLOW_DATA = "de.kaiserpfalzedv.commons.core.workflow.data";
55  
56      private static final String WORKFLOW_USER = "X-wf-user";
57  
58      private static final String NAME = "name";
59      private static final String ID = "id";
60      private static final String CREATED = "created";
61      private static final String TTL = "ttl";
62      private static final String RESPONSE = "response";
63  
64      private static final String WORKFLOW_PREFIX = "X-wf-";
65      private static final String ACTION_PREFIX = "X-wf-action-";
66      private static final String CALL_PREFIX = "X-wf-call-";
67  
68      private final WorkflowProvider provider;
69  
70      @Override
71      public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
72          final HttpServletRequest req = this.checkForHttpServletRequestOrThrowException(request);
73          final HttpServletResponse res = this.checkForHttpServletResponseOrThrowException(response);
74  
75          this.filter(req);
76          chain.doFilter(request, response);
77          this.filter(req, res);
78      }
79  
80      private HttpServletRequest checkForHttpServletRequestOrThrowException(final ServletRequest request) throws ServletException {
81          if (! (request instanceof HttpServletRequest)) {
82              throw new ServletException("Wrong servlet type. This filter only works on HTTP servlet.");
83          }
84  
85          return (HttpServletRequest) request;
86      }
87  
88      private HttpServletResponse checkForHttpServletResponseOrThrowException(final ServletResponse response) throws ServletException {
89          if (! (response instanceof HttpServletResponse)) {
90              throw new ServletException("Wrong servlet type. This filter only works on HTTP servlet.");
91          }
92  
93          return (HttpServletResponse) response;
94      }
95  
96      private void filter(final HttpServletRequest context) {
97          final WorkflowInfoImpl info = this.getWorkflowInfo(context);
98          this.prepareMDC(info);
99  
100         context.setAttribute(WORKFLOW_DATA, info);
101         this.provider.registerWorkflowInfo(info);
102 
103         log.trace(
104                 "Created the workflow info. workflow='{}', action='{}', call='{}', user='{}'",
105                 info.getWorkflow().getId(),
106                 info.getAction().getId(),
107                 info.getCall().getId(),
108                 info.getUser()
109         );
110     }
111 
112     private WorkflowInfoImpl getWorkflowInfo(final HttpServletRequest context) {
113         return WorkflowInfoImpl.builder()
114                 .user(this.checkValidHeader(context.getHeader(WORKFLOW_USER)))
115                 .workflow(this.getWorkflowInfoDetail(context, WORKFLOW_PREFIX))
116                 .action(this.getWorkflowInfoDetail(context, ACTION_PREFIX))
117                 .call(this.getWorkflowInfoDetail(context, CALL_PREFIX))
118                 .build();
119     }
120 
121     private WorkflowDetailInfoImpl getWorkflowInfoDetail(final HttpServletRequest context, final String prefix) {
122         final String name = this.checkValidHeader(context.getHeader(prefix + NAME));
123         final String id = this.checkValidHeader(context.getHeader(prefix + ID));
124         final String response = this.checkValidHeader(context.getHeader(prefix + RESPONSE));
125 
126         final WorkflowDetailInfoImpl.WorkflowDetailInfoImplBuilder result = WorkflowDetailInfoImpl.builder()
127                 .created(this.checkValidTimeHeader(context.getHeader(prefix + CREATED), Duration.ofMillis(0)))
128                 .ttl(this.checkValidTimeHeader(context.getHeader(prefix + TTL), Duration.of(10, ChronoUnit.YEARS)));
129 
130         if (name != null) result.name(name);
131         if (id != null) result.id(id);
132         if (response != null) result.responseChannel(response);
133 
134         return result.build();
135     }
136 
137     private String checkValidHeader(final String value) {
138         if (value == null || "".equals(value)) {
139             return null;
140         }
141 
142         return value;
143     }
144 
145     private OffsetDateTime checkValidTimeHeader(final String value, final TemporalAmount futureOffset) {
146         try {
147             return OffsetDateTime.parse(value);
148         } catch (final DateTimeParseException e) {
149             return OffsetDateTime.now(ZoneId.of("UTC")).plus(futureOffset);
150         }
151     }
152 
153 
154     private void prepareMDC(final WorkflowInfoImpl info) {
155         MDC.put(WORKFLOW_USER, info.getUser());
156 
157         this.putMDC(info.getWorkflow(), WORKFLOW_PREFIX);
158         this.putMDC(info.getAction(), ACTION_PREFIX);
159         this.putMDC(info.getCall(), CALL_PREFIX);
160     }
161 
162     private void putMDC(final WorkflowDetailInfoImpl info, final String workflowPrefix) {
163         MDC.put(workflowPrefix + NAME, info.getName());
164         MDC.put(workflowPrefix + ID, info.getId());
165         MDC.put(workflowPrefix + CREATED, info.getCreated().toString());
166         MDC.put(workflowPrefix + TTL, info.getTtl().toString());
167         MDC.put(workflowPrefix + RESPONSE, info.getResponseChannel());
168     }
169 
170 
171     private void filter(final HttpServletRequest request, final HttpServletResponse context) {
172         this.provider.getWorkflowInfo().ifPresentOrElse(
173             info -> {
174                 log.trace(
175                     "Removing workflow data from request. workflow='{}', action='{}', call='{}', user='{}'",
176                     info.getWorkflow().getId(),
177                     info.getAction().getId(),
178                     info.getCall().getId(),
179                     info.getUser()
180                 );
181 
182                 this.unsetWorkflowInfoInContext(request);
183                 this.provider.unregisterWorkflowInfo();
184 
185                 this.removeMDC();
186             },
187             () -> {
188                 log.trace("No workflow data to remove from request.");
189             }
190         );
191     }
192 
193     private void unsetWorkflowInfoInContext(final HttpServletRequest requestContext) {
194         requestContext.setAttribute(WORKFLOW_DATA, null);
195     }
196 
197     private void removeMDC() {
198         MDC.remove(WORKFLOW_USER);
199         this.removeMDC(WORKFLOW_PREFIX);
200         this.removeMDC(ACTION_PREFIX);
201         this.removeMDC(CALL_PREFIX);
202     }
203 
204     private void removeMDC(final String prefix) {
205         MDC.remove(prefix + NAME);
206         MDC.remove(prefix + ID);
207         MDC.remove(prefix + CREATED);
208         MDC.remove(prefix + TTL);
209         MDC.remove(prefix + RESPONSE);
210     }
211 }