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.Optional.of;
025import static java.util.UUID.randomUUID;
026
027import java.net.URI;
028import java.time.Duration;
029import java.time.Instant;
030import java.util.Optional;
031import java.util.concurrent.ConcurrentHashMap;
032
033import javax.jcr.RepositoryException;
034import javax.jcr.Session;
035import javax.jcr.observation.ObservationManager;
036
037import com.fasterxml.jackson.core.JsonProcessingException;
038import com.fasterxml.jackson.databind.ObjectMapper;
039import com.fasterxml.jackson.databind.node.ObjectNode;
040import com.google.common.annotations.VisibleForTesting;
041import org.fcrepo.kernel.api.FedoraSession;
042import org.fcrepo.kernel.api.exception.AccessDeniedException;
043import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
044import org.fcrepo.kernel.modeshape.utils.FedoraSessionUserUtil;
045
046/**
047 * An implementation of the FedoraSession abstraction
048 * @author acoburn
049 */
050public class FedoraSessionImpl implements FedoraSession {
051    // The default timeout is 3 minutes
052    @VisibleForTesting
053    public static final String DEFAULT_TIMEOUT = Long.toString(ofMinutes(3).toMillis());
054
055    @VisibleForTesting
056    public static final String TIMEOUT_SYSTEM_PROPERTY = "fcrepo.session.timeout";
057
058    private final Session jcrSession;
059    private final String id;
060    private final Instant created;
061    private final ConcurrentHashMap<String, String> sessionData;
062    private Instant expires;
063
064    /**
065     * A key for looking up the transaction id in a session key-value pair
066     */
067    public static final String FCREPO_TX_ID = "fcrepo.tx.id";
068
069    private static final ObjectMapper mapper = new ObjectMapper();
070
071    /**
072     * Create a Fedora session with a JCR session
073     * @param session the JCR session
074     */
075    public FedoraSessionImpl(final Session session) {
076        this.jcrSession = session;
077
078        created = now();
079        id = randomUUID().toString();
080        expires = created.plus(operationTimeout());
081        sessionData = new ConcurrentHashMap<>();
082    }
083
084    @Override
085    public void commit() {
086        try {
087            if (jcrSession.isLive()) {
088                final ObservationManager obs = jcrSession.getWorkspace().getObservationManager();
089                final ObjectNode json = mapper.createObjectNode();
090                sessionData.forEach(json::put);
091                obs.setUserData(mapper.writeValueAsString(json));
092                jcrSession.save();
093            }
094        } catch (final javax.jcr.AccessDeniedException ex) {
095            throw new AccessDeniedException(ex);
096        } catch (final RepositoryException | JsonProcessingException ex) {
097            throw new RepositoryRuntimeException(ex);
098        }
099    }
100
101    @Override
102    public void expire() {
103        expires = now();
104        try {
105            if (jcrSession.isLive()) {
106                jcrSession.refresh(false);
107                jcrSession.logout();
108            }
109        } catch (final RepositoryException ex) {
110            throw new RepositoryRuntimeException(ex);
111        }
112    }
113
114    @Override
115    public Instant updateExpiry(final Duration amountToAdd) {
116        if (jcrSession.isLive()) {
117            expires = now().plus(amountToAdd);
118        }
119        return expires;
120    }
121
122    @Override
123    public Optional<Instant> getExpires() {
124        return of(expires);
125    }
126
127    @Override
128    public String getId() {
129        return id;
130    }
131
132    @Override
133    public URI getUserURI() {
134        return FedoraSessionUserUtil.getUserURI(jcrSession.getUserID());
135    }
136
137    /**
138     *  Add session data
139     *  @param key the data key
140     *  @param value the data value
141     *
142     *  Note: while the FedoraSession interface permits multi-valued
143     *  session data, this implementation constrains that to be single-valued.
144     *  That is, calling obj.addSessionData("key", "value1") followed by
145     *  obj.addSessionData("key", "value2") will result in only "value2" being associated
146     *  with the given key.
147     */
148    @Override
149    public void addSessionData(final String key, final String value) {
150        sessionData.put(key, value);
151    }
152
153    /**
154     * Get the internal JCR session
155     * @return the internal JCR session
156     */
157    public Session getJcrSession() {
158        return jcrSession;
159    }
160
161    /**
162     * Get the internal JCR session from an existing FedoraSession
163     * @param session the FedoraSession
164     * @return the JCR session
165     */
166    public static Session getJcrSession(final FedoraSession session) {
167        if (session instanceof FedoraSessionImpl) {
168            return ((FedoraSessionImpl)session).getJcrSession();
169        }
170        throw new ClassCastException("FedoraSession is not a " + FedoraSessionImpl.class.getCanonicalName());
171    }
172
173    /**
174     * Retrieve the default operation timeout value
175     * @return the default timeout value
176     */
177    public static Duration operationTimeout() {
178       return ofMillis(parseLong(System.getProperty(TIMEOUT_SYSTEM_PROPERTY, DEFAULT_TIMEOUT)));
179    }
180}