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-2014 ForgeRock AS.
016 */
017
018package org.forgerock.openig.filter;
019
020import static org.forgerock.openig.util.Json.*;
021
022import java.io.File;
023import java.io.IOException;
024import java.util.Map;
025
026import org.forgerock.openig.el.Expression;
027import org.forgerock.openig.handler.Handler;
028import org.forgerock.openig.handler.HandlerException;
029import org.forgerock.openig.heap.GenericHeaplet;
030import org.forgerock.openig.heap.HeapException;
031import org.forgerock.openig.http.Exchange;
032import org.forgerock.openig.text.SeparatedValuesFile;
033import org.forgerock.openig.text.Separators;
034import org.forgerock.util.Factory;
035import org.forgerock.util.LazyMap;
036
037/**
038 * Retrieves and exposes a record from a delimier-separated file. Lookup of the record is
039 * performed using a specified key, whose value is derived from an exchange-scoped expression.
040 * The resulting record is exposed in a {@link Map} object, whose location is specified by the
041 * {@code target} expression. If a matching record cannot be found, then the resulting map
042 * will be empty.
043 * <p>
044 * The retrieval of the record is performed lazily; it does not occur until the first attempt
045 * to access a value in the target. This defers the overhead of file operations and text
046 * processing until a value is first required. This also means that the {@code value}
047 * expression will not be evaluated until the map is first accessed.
048 *
049 * @see SeparatedValuesFile
050 */
051public class FileAttributesFilter extends GenericFilter {
052
053    /** Expression that yields the target object that will contain the record. */
054    private final Expression target;
055
056    /** The file to read separated values from. */
057    private final SeparatedValuesFile file;
058
059    /** The name of the field in the file to perform the lookup on. */
060    private final String key;
061
062    /** Expression that yields the value to be looked-up within the file. */
063    private final Expression value;
064
065    /**
066     * Builds a new FileAttributesFilter extracting values from the given separated values file.
067     *
068     * @param file
069     *         The file to read separated values from ({@literal csv} file)
070     * @param key
071     *         The name of the field in the file to perform the lookup on
072     * @param value
073     *         Expression that yields the value to be looked-up within the file
074     * @param target
075     *         Expression that yields the target object that will contain the record
076     */
077    public FileAttributesFilter(final SeparatedValuesFile file,
078                                final String key,
079                                final Expression value,
080                                final Expression target) {
081        this.file = file;
082        this.key = key;
083        this.value = value;
084        this.target = target;
085    }
086
087    @Override
088    public void filter(final Exchange exchange, Handler next) throws HandlerException, IOException {
089        target.set(exchange, new LazyMap<String, String>(new Factory<Map<String, String>>() {
090            @Override
091            public Map<String, String> newInstance() {
092                try {
093                    return file.getRecord(key, value.eval(exchange).toString());
094                } catch (IOException ioe) {
095                    logger.warning(ioe);
096                    // results in an empty map
097                    return null;
098                }
099            }
100        }));
101        next.handle(exchange);
102    }
103
104    /** Creates and initializes a separated values file attribute provider in a heap environment. */
105    public static class Heaplet extends GenericHeaplet {
106        @Override
107        public Object create() throws HeapException {
108            SeparatedValuesFile sources = new SeparatedValuesFile(new File(evaluate(config.get("file").required())),
109                                                                  config.get("charset").defaultTo("UTF-8").asCharset(),
110                                                                  config.get("separator").defaultTo("COMMA")
111                                                                          .asEnum(Separators.class).getSeparator(),
112                                                                  config.get("header").defaultTo(true).asBoolean());
113
114            if (config.isDefined("fields")) {
115                sources.getFields().addAll(config.get("fields").asList(String.class));
116            }
117            return new FileAttributesFilter(sources,
118                                            config.get("key").required().asString(),
119                                            asExpression(config.get("value").required()),
120                                            asExpression(config.get("target").required()));
121        }
122    }
123}