001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.fcrepo.kernel.modeshape;
019
020import static java.lang.Long.parseLong;
021import static java.time.Duration.ofMillis;
022import static java.time.Duration.ofMinutes;
023import static java.time.Instant.now;
024import static java.util.Collections.singleton;
025import static java.util.Optional.of;
026import static java.util.UUID.randomUUID;
027
028import java.time.Duration;
029import java.time.Instant;
030import java.util.Collection;
031import java.util.Map;
032import java.util.Optional;
033import java.util.concurrent.ConcurrentHashMap;
034
035import javax.jcr.RepositoryException;
036import javax.jcr.Session;
037import javax.jcr.observation.ObservationManager;
038
039import com.fasterxml.jackson.core.JsonProcessingException;
040import com.fasterxml.jackson.databind.ObjectMapper;
041import com.fasterxml.jackson.databind.node.ObjectNode;
042import com.google.common.annotations.VisibleForTesting;
043import org.fcrepo.kernel.api.FedoraSession;
044import org.fcrepo.kernel.api.exception.AccessDeniedException;
045import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
046import org.fcrepo.kernel.modeshape.utils.NamespaceTools;
047
048/**
049 * An implementation of the FedoraSession abstraction
050 * @author acoburn
051 */
052public class FedoraSessionImpl implements FedoraSession {
053
054    // The default timeout is 3 minutes
055    @VisibleForTesting
056    public static final String DEFAULT_TIMEOUT = Long.toString(ofMinutes(3).toMillis());
057
058    @VisibleForTesting
059    public static final String TIMEOUT_SYSTEM_PROPERTY = "fcrepo.session.timeout";
060
061    private final Session jcrSession;
062    private final String id;
063    private final Instant created;
064    private final ConcurrentHashMap<String, String> sessionData;
065    private Instant expires;
066
067    /**
068     * A key for looking up the transaction id in a session key-value pair
069     */
070    public static final String FCREPO_TX_ID = "fcrepo.tx.id";
071
072    private static final ObjectMapper mapper = new ObjectMapper();
073
074    /**
075     * Create a Fedora session with a JCR session
076     * @param session the JCR session
077     */
078    public FedoraSessionImpl(final Session session) {
079        this.jcrSession = session;
080
081        created = now();
082        id = randomUUID().toString();
083        expires = created.plus(operationTimeout());
084        sessionData = new ConcurrentHashMap<>();
085    }
086
087    @Override
088    public void commit() {
089        try {
090            if (jcrSession.isLive()) {
091                final ObservationManager obs = jcrSession.getWorkspace().getObservationManager();
092                final ObjectNode json = mapper.createObjectNode();
093                sessionData.forEach(json::put);
094                obs.setUserData(mapper.writeValueAsString(json));
095                jcrSession.save();
096            }
097        } catch (final javax.jcr.AccessDeniedException ex) {
098            throw new AccessDeniedException(ex);
099        } catch (final RepositoryException | JsonProcessingException ex) {
100            throw new RepositoryRuntimeException(ex);
101        }
102    }
103
104    @Override
105    public void expire() {
106        expires = now();
107        try {
108            if (jcrSession.isLive()) {
109                jcrSession.refresh(false);
110                jcrSession.logout();
111            }
112        } catch (final RepositoryException ex) {
113            throw new RepositoryRuntimeException(ex);
114        }
115    }
116
117    @Override
118    public Instant updateExpiry(final Duration amountToAdd) {
119        if (jcrSession.isLive()) {
120            expires = now().plus(amountToAdd);
121        }
122        return expires;
123    }
124
125    @Override
126    public Instant getCreated() {
127        return created;
128    }
129
130    @Override
131    public Optional<Instant> getExpires() {
132        return of(expires);
133    }
134
135    @Override
136    public String getId() {
137        return id;
138    }
139
140    @Override
141    public String getUserId() {
142        return jcrSession.getUserID();
143    }
144
145    @Override
146    public Map<String, String> getNamespaces() {
147        return NamespaceTools.getNamespaces(jcrSession);
148    }
149
150    /**
151     *  Add session data
152     *  @param key the data key
153     *  @param value the data value
154     *
155     *  Note: while the FedoraSession interface permits multi-valued
156     *  session data, this implementation constrains that to be single-valued.
157     *  That is, calling obj.addSessionData("key", "value1") followed by
158     *  obj.addSessionData("key", "value2") will result in only "value2" being associated
159     *  with the given key.
160     */
161    @Override
162    public void addSessionData(final String key, final String value) {
163        sessionData.put(key, value);
164    }
165
166    @Override
167    public Collection<String> getSessionData(final String key) {
168        return singleton(sessionData.get(key));
169    }
170
171    @Override
172    public void removeSessionData(final String key, final String value) {
173        sessionData.remove(key, value);
174    }
175
176    @Override
177    public void removeSessionData(final String key) {
178        sessionData.remove(key);
179    }
180
181    /**
182     * Get the internal JCR session
183     * @return the internal JCR session
184     */
185    public Session getJcrSession() {
186        return jcrSession;
187    }
188
189    /**
190     * Get the internal JCR session from an existing FedoraSession
191     * @param session the FedoraSession
192     * @return the JCR session
193     */
194    public static Session getJcrSession(final FedoraSession session) {
195        if (session instanceof FedoraSessionImpl) {
196            return ((FedoraSessionImpl)session).getJcrSession();
197        }
198        throw new ClassCastException("FedoraSession is not a " + FedoraSessionImpl.class.getCanonicalName());
199    }
200
201    /**
202     * Retrieve the default operation timeout value
203     * @return the default timeout value
204     */
205    public static Duration operationTimeout() {
206       return ofMillis(parseLong(System.getProperty(TIMEOUT_SYSTEM_PROPERTY, DEFAULT_TIMEOUT)));
207    }
208}