001/**
002 * Copyright 2015 DuraSpace, Inc.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.fcrepo.transform.transformations;
017
018import com.google.common.base.Function;
019import com.google.common.collect.ImmutableList;
020import com.hp.hpl.jena.rdf.model.RDFNode;
021import com.hp.hpl.jena.rdf.model.Resource;
022
023import org.apache.marmotta.ldpath.LDPath;
024import org.apache.marmotta.ldpath.backend.jena.GenericJenaBackend;
025import org.apache.marmotta.ldpath.exception.LDPathParseException;
026import org.fcrepo.kernel.utils.iterators.RdfStream;
027import org.fcrepo.transform.Transformation;
028import org.slf4j.Logger;
029
030import javax.jcr.Node;
031import javax.jcr.RepositoryException;
032import javax.jcr.nodetype.NodeType;
033import javax.ws.rs.WebApplicationException;
034
035import java.io.InputStream;
036import java.io.InputStreamReader;
037import java.util.Collection;
038import java.util.Comparator;
039import java.util.List;
040import java.util.Map;
041import java.util.Objects;
042import java.util.Set;
043
044import static com.google.common.collect.Collections2.transform;
045import static com.google.common.collect.ImmutableSortedSet.orderedBy;
046import static com.google.common.collect.Maps.transformValues;
047import static com.hp.hpl.jena.rdf.model.ResourceFactory.createResource;
048import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
049import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT;
050import static org.modeshape.jcr.api.JcrConstants.JCR_DATA;
051import static org.slf4j.LoggerFactory.getLogger;
052
053/**
054 * Utilities for working with LDPath
055 *
056 * @author cbeer
057 */
058public class LDPathTransform implements Transformation<List<Map<String, Collection<Object>>>>  {
059
060    public static final String CONFIGURATION_FOLDER = "/fedora:system/fedora:transform/fedora:ldpath/";
061
062
063    // TODO: this mime type was made up
064    public static final String APPLICATION_RDF_LDPATH = "application/rdf+ldpath";
065    private final InputStream query;
066
067    private static final Logger LOGGER = getLogger(LDPathTransform.class);
068
069    private static final Comparator<NodeType> nodeTypeComp = new Comparator<NodeType>() {
070
071        @Override
072        public int compare(final NodeType o1, final NodeType o2) {
073            return o1.getName().compareTo(o2.getName());
074
075        }
076    };
077
078    /**
079     * Construct a new Transform from the InputStream
080     * @param query
081     */
082    public LDPathTransform(final InputStream query) {
083        this.query = query;
084    }
085
086    /**
087     * Pull a node-type specific transform out of JCR
088     * @param node
089     * @param key
090     * @return node-type specific transform
091     * @throws RepositoryException
092     */
093    public static LDPathTransform getNodeTypeTransform(final Node node,
094        final String key) throws RepositoryException {
095
096        final Node programNode = node.getSession().getNode(CONFIGURATION_FOLDER + key);
097
098        LOGGER.debug("Found program node: {}", programNode.getPath());
099
100        final NodeType primaryNodeType = node.getPrimaryNodeType();
101
102        final Set<NodeType> supertypes =
103            orderedBy(nodeTypeComp).add(primaryNodeType.getSupertypes())
104                    .build();
105        final Set<NodeType> mixinTypes =
106            orderedBy(nodeTypeComp).add(node.getMixinNodeTypes()).build();
107
108        // start with mixins, primary type, and supertypes of primary type
109        final ImmutableList.Builder<NodeType> nodeTypesB =
110            new ImmutableList.Builder<NodeType>().addAll(mixinTypes).add(
111                    primaryNodeType).addAll(supertypes);
112
113        // add supertypes of mixins
114        for (final NodeType mixin : mixinTypes) {
115            nodeTypesB.addAll(orderedBy(nodeTypeComp).add(
116                    mixin.getDeclaredSupertypes()).build());
117        }
118
119        final List<NodeType> nodeTypes = nodeTypesB.build();
120
121        LOGGER.debug("Discovered node types: {}", nodeTypes);
122
123        for (final NodeType nodeType : nodeTypes) {
124            if (programNode.hasNode(nodeType.toString())) {
125                return new LDPathTransform(programNode.getNode(nodeType.toString())
126                                               .getNode(JCR_CONTENT)
127                                               .getProperty(JCR_DATA)
128                                               .getBinary().getStream());
129            }
130        }
131
132        throw new WebApplicationException(new Exception(
133                "Couldn't find transformation for " + node.getPath()
134                        + " and transformation key " + key), SC_BAD_REQUEST);
135    }
136
137    @Override
138    public List<Map<String, Collection<Object>>> apply(final RdfStream stream) {
139        try {
140            final LDPath<RDFNode> ldpathForResource =
141                getLdpathResource(stream);
142
143            final Resource context = createResource(stream.topic().getURI());
144
145            final Map<String, Collection<?>> wildcardCollection =
146                ldpathForResource.programQuery(context, new InputStreamReader(
147                        query));
148
149            return ImmutableList.of(transformLdpathOutputToSomethingSerializable(wildcardCollection));
150        } catch (final LDPathParseException e) {
151            throw new RuntimeException(e);
152        }
153    }
154
155    @Override
156    public InputStream getQuery() {
157        return query;
158    }
159
160    @Override
161    public boolean equals(final Object other) {
162        return other instanceof LDPathTransform &&
163                   query.equals(((LDPathTransform)other).getQuery());
164    }
165
166    @Override
167    public int hashCode() {
168        return Objects.hashCode(getQuery());
169    }
170
171    /**
172     * Get the LDPath resource for an object
173     * @param rdfStream
174     * @return the LDPath resource for the given object
175     */
176    private static LDPath<RDFNode> getLdpathResource(final RdfStream rdfStream) {
177
178        return new LDPath<>(new GenericJenaBackend(rdfStream.asModel()));
179
180    }
181
182    /**
183     * In order for the JAX-RS serialization magic to work, we have to turn the map into
184     * a non-wildcard type.
185     * @param collectionMap
186     * @return map of the LDPath
187     */
188    private static Map<String, Collection<Object>> transformLdpathOutputToSomethingSerializable(
189        final Map<String, Collection<?>> collectionMap) {
190
191        return transformValues(collectionMap,
192                WILDCARD_COLLECTION_TO_OBJECT_COLLECTION);
193    }
194
195    private static final Function<Collection<?>, Collection<Object>> WILDCARD_COLLECTION_TO_OBJECT_COLLECTION =
196        new Function<Collection<?>, Collection<Object>>() {
197
198            @Override
199            public Collection<Object> apply(final Collection<?> input) {
200                return transform(input, ANYTHING_TO_OBJECT_FUNCTION);
201            }
202        };
203
204    private static final Function<Object, Object> ANYTHING_TO_OBJECT_FUNCTION =
205        new Function<Object, Object>() {
206
207            @Override
208            public Object apply(final Object input) {
209                return input;
210            }
211        };
212
213    @Override
214    public LDPathTransform newTransform(final InputStream query) {
215        return new LDPathTransform(query);
216    }
217}