001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2010-2011 ApexIdentity Inc.
015 * Portions Copyright 2011-2015 ForgeRock AS.
016 */
017
018package org.forgerock.openig.filter;
019
020import static org.forgerock.openig.el.Bindings.bindings;
021import static org.forgerock.openig.util.JsonValues.asExpression;
022
023import java.io.IOException;
024import java.io.Reader;
025import java.nio.charset.Charset;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029
030import org.forgerock.http.Filter;
031import org.forgerock.http.Handler;
032import org.forgerock.http.protocol.Message;
033import org.forgerock.http.protocol.Request;
034import org.forgerock.http.protocol.Response;
035import org.forgerock.json.JsonValue;
036import org.forgerock.json.JsonValueException;
037import org.forgerock.openig.el.Bindings;
038import org.forgerock.openig.el.Expression;
039import org.forgerock.openig.heap.GenericHeapObject;
040import org.forgerock.openig.heap.GenericHeaplet;
041import org.forgerock.openig.heap.HeapException;
042import org.forgerock.openig.regex.PatternTemplate;
043import org.forgerock.openig.regex.StreamPatternExtractor;
044import org.forgerock.openig.util.MessageType;
045import org.forgerock.services.context.Context;
046import org.forgerock.util.promise.NeverThrowsException;
047import org.forgerock.util.promise.Promise;
048import org.forgerock.util.promise.ResultHandler;
049
050/**
051 * Extracts regular expression patterns from a message entity. Extraction occurs either
052 * before the request is handled if {@code messageType} is {@link MessageType#REQUEST}, or
053 * after the request is handled if it is {@link MessageType#RESPONSE}. Each pattern can have
054 * an associated template, which is applied to its match result.
055 * <p>
056 * The extraction results are contained in a {@link Map} object, whose location is specified
057 * by the {@code target} expression. For a given matched pattern, the value stored in the map
058 * is either the result of applying its associated pattern template (if specified) or the
059 * match result itself otherwise.
060 *
061 * @see StreamPatternExtractor
062 * @see PatternTemplate
063 */
064public class EntityExtractFilter extends GenericHeapObject implements Filter {
065
066    /** Extracts regular expression patterns from entities. */
067    private final StreamPatternExtractor extractor = new StreamPatternExtractor();
068
069    /** The message type to extract patterns from. */
070    private final MessageType messageType;
071
072    /** Overrides the character set encoding specified in message. If {@code null}, the message encoding is used. */
073    private final Charset charset;
074
075    /** Expression that yields the target object that will contain the mapped extraction results. */
076    private final Expression<?> target;
077
078    /**
079     * Builds an EntityExtractFilter that will act either on {@link MessageType#REQUEST} or {@link MessageType#RESPONSE}
080     * flow, extracting patterns into the given {@code target} {@link Expression}. The {@link Charset} used is the one
081     * of the message.
082     *
083     * @param type
084     *         Specifies the execution flow to be executed in
085     * @param target
086     *         Expression that yields the target object that will contain the mapped extraction results
087     */
088    public EntityExtractFilter(final MessageType type, final Expression<?> target) {
089        this(type, target, null);
090    }
091
092    /**
093     * Builds an EntityExtractFilter that will act either on {@link MessageType#REQUEST} or {@link MessageType#RESPONSE}
094     * flow, extracting patterns into the given {@code target} {@link Expression}. The {@link Charset} used is the one
095     * specified.
096     *
097     * @param type
098     *         Specifies the execution flow to be executed in
099     * @param target
100     *         Expression that yields the target object that will contain the mapped extraction results
101     * @param charset
102     *         Overrides the character set encoding specified in message. If {@code null}, the message encoding is used
103     */
104    public EntityExtractFilter(final MessageType type, final Expression<?> target, final Charset charset) {
105        this.messageType = type;
106        this.target = target;
107        this.charset = charset;
108    }
109
110    /**
111     * Returns the regular expression patterns extractor.
112     * @return the regular expression patterns extractor.
113     */
114    public StreamPatternExtractor getExtractor() {
115        return extractor;
116    }
117
118    private void process(Bindings bindings, Message message) {
119        HashMap<String, String> map = new HashMap<>();
120        if (message != null) {
121            try {
122                try (Reader reader = message.getEntity().newDecodedContentReader(charset)) {
123                    // get 'em all now
124                    for (Map.Entry<String, String> match : extractor.extract(reader)) {
125                        map.put(match.getKey(), match.getValue());
126                    }
127                }
128            } catch (IOException ioe) {
129                // may yield partial or unresolved attributes
130            }
131        }
132        target.set(bindings, map);
133    }
134
135    @Override
136    public Promise<Response, NeverThrowsException> filter(final Context context,
137                                                          final Request request,
138                                                          final Handler next) {
139
140        if (messageType == MessageType.REQUEST) {
141            process(bindings(context, request), request);
142        }
143        Promise<Response, NeverThrowsException> promise = next.handle(context, request);
144        if (messageType == MessageType.RESPONSE) {
145            return promise.thenOnResult(new ResultHandler<Response>() {
146                @Override
147                public void handleResult(final Response response) {
148                    process(bindings(context, request, response), response);
149                }
150            });
151        }
152        return promise;
153    }
154
155    /** Creates and initializes an entity extract handler in a heap environment. */
156    public static class Heaplet extends GenericHeaplet {
157        @Override
158        public Object create() throws HeapException {
159            EntityExtractFilter filter = new EntityExtractFilter(
160                    config.get("messageType").required().asEnum(MessageType.class),
161                    asExpression(config.get("target").required(), Object.class),
162                    config.get("charset").asCharset());
163
164            for (JsonValue jv : config.get("bindings").required().expect(List.class)) {
165                jv.required().expect(Map.class);
166                String key = jv.get("key").required().asString();
167                if (filter.extractor.getPatterns().containsKey(key)) {
168                    throw new JsonValueException(jv.get("key"), "Key already defined");
169                }
170                filter.extractor.getPatterns().put(key, jv.get("pattern").required().asPattern());
171                String template = jv.get("template").asString();
172                if (template != null) {
173                    filter.extractor.getTemplates().put(key, new PatternTemplate(template));
174                }
175            }
176            return filter;
177        }
178    }
179}