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 java.lang.String.format;
021import static org.forgerock.openig.el.Bindings.bindings;
022import static org.forgerock.openig.util.JsonValues.asExpression;
023import static org.forgerock.openig.util.JsonValues.evaluate;
024
025import java.io.File;
026import java.io.IOException;
027import java.util.Collections;
028import java.util.Map;
029
030import org.forgerock.http.Filter;
031import org.forgerock.http.Handler;
032import org.forgerock.http.protocol.Request;
033import org.forgerock.http.protocol.Response;
034import org.forgerock.openig.el.Bindings;
035import org.forgerock.openig.el.Expression;
036import org.forgerock.openig.heap.GenericHeapObject;
037import org.forgerock.openig.heap.GenericHeaplet;
038import org.forgerock.openig.heap.HeapException;
039import org.forgerock.openig.text.SeparatedValuesFile;
040import org.forgerock.openig.text.Separators;
041import org.forgerock.services.context.Context;
042import org.forgerock.util.Factory;
043import org.forgerock.util.LazyMap;
044import org.forgerock.util.promise.NeverThrowsException;
045import org.forgerock.util.promise.Promise;
046
047/**
048 * Retrieves and exposes a record from a delimiter-separated file. Lookup of the record is
049 * performed using a specified key, whose value is derived from an expression.
050 * The resulting record is exposed in a {@link Map} object, whose location is specified by the
051 * {@code target} expression. If a matching record cannot be found, then the resulting map
052 * will be empty.
053 * <p>
054 * The retrieval of the record is performed lazily; it does not occur until the first attempt
055 * to access a value in the target. This defers the overhead of file operations and text
056 * processing until a value is first required. This also means that the {@code value}
057 * expression will not be evaluated until the map is first accessed.
058 *
059 * @see SeparatedValuesFile
060 */
061public class FileAttributesFilter extends GenericHeapObject implements Filter {
062
063    /** Expression that yields the target object that will contain the record. */
064    @SuppressWarnings("rawtypes") // Can't find the correct syntax to write Expression<Map<String, String>>
065    private final Expression<Map> target;
066
067    /** The file to read separated values from. */
068    private final SeparatedValuesFile file;
069
070    /** The name of the field in the file to perform the lookup on. */
071    private final String key;
072
073    /** Expression that yields the value to be looked-up within the file. */
074    private final Expression<String> value;
075
076    /**
077     * Builds a new FileAttributesFilter extracting values from the given separated values file.
078     *
079     * @param file
080     *         The file to read separated values from ({@literal csv} file)
081     * @param key
082     *         The name of the field in the file to perform the lookup on
083     * @param value
084     *         Expression that yields the value to be looked-up within the file
085     * @param target
086     *         Expression that yields the target object that will contain the record
087     */
088    public FileAttributesFilter(final SeparatedValuesFile file,
089                                final String key,
090                                final Expression<String> value,
091                                @SuppressWarnings("rawtypes") final Expression<Map> target) {
092        this.file = file;
093        this.key = key;
094        this.value = value;
095        this.target = target;
096    }
097
098    @Override
099    public Promise<Response, NeverThrowsException> filter(final Context context,
100                                                          final Request request,
101                                                          final Handler next) {
102        final Bindings bindings = bindings(context, request);
103        target.set(bindings, new LazyMap<>(new Factory<Map<String, String>>() {
104            @Override
105            public Map<String, String> newInstance() {
106                try {
107                    String eval = value.eval(bindings);
108                    Map<String, String> record = file.getRecord(key, eval);
109                    if (record == null) {
110                        logger.debug(format("Couldn't select a row where column %s value is equal to %s", key, eval));
111                        return Collections.emptyMap();
112                    } else {
113                        return record;
114                    }
115                } catch (IOException ioe) {
116                    logger.warning(ioe);
117                    // results in an empty map
118                    return Collections.emptyMap();
119                }
120            }
121        }));
122        return next.handle(context, request);
123    }
124
125    /** Creates and initializes a separated values file attribute provider in a heap environment. */
126    public static class Heaplet extends GenericHeaplet {
127        @Override
128        public Object create() throws HeapException {
129            String filename = evaluate(config.get("file").required());
130            SeparatedValuesFile sources = new SeparatedValuesFile(new File(filename),
131                                                                  config.get("charset").defaultTo("UTF-8").asCharset(),
132                                                                  config.get("separator").defaultTo("COMMA")
133                                                                          .asEnum(Separators.class).getSeparator(),
134                                                                  config.get("header").defaultTo(true).asBoolean());
135
136            if (config.isDefined("fields")) {
137                sources.getFields().addAll(config.get("fields").asList(String.class));
138            }
139            return new FileAttributesFilter(sources,
140                                            config.get("key").required().asString(),
141                                            asExpression(config.get("value").required(), String.class),
142                                            asExpression(config.get("target").required(), Map.class));
143        }
144    }
145}