001/*
002 * (C) Copyright 2015-2016 Nuxeo SA (http://nuxeo.com/) and others.
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 *
016 * Contributors:
017 *     Kevin Leturc
018 */
019package org.nuxeo.ecm.liveconnect.box;
020
021import java.io.IOException;
022import java.io.InputStream;
023import java.net.URI;
024
025import javax.servlet.http.HttpServletRequest;
026
027import org.apache.http.HttpStatus;
028import org.nuxeo.ecm.core.blob.BlobManager.UsageHint;
029import org.nuxeo.ecm.core.blob.ManagedBlob;
030import org.nuxeo.ecm.core.model.Document;
031import org.nuxeo.ecm.liveconnect.core.AbstractLiveConnectBlobProvider;
032import org.nuxeo.ecm.liveconnect.core.LiveConnectFile;
033import org.nuxeo.ecm.liveconnect.core.LiveConnectFileInfo;
034import org.nuxeo.ecm.platform.oauth2.tokens.NuxeoOAuth2Token;
035
036import com.box.sdk.BoxAPIConnection;
037import com.box.sdk.BoxAPIException;
038import com.box.sdk.BoxFile;
039import com.box.sdk.BoxSharedLink;
040import com.box.sdk.BoxSharedLink.Access;
041import com.google.api.client.auth.oauth2.Credential;
042import com.google.api.client.http.GenericUrl;
043import com.google.api.client.http.HttpHeaders;
044import com.google.api.client.http.HttpRequest;
045import com.google.api.client.http.HttpRequestFactory;
046import com.google.api.client.http.HttpResponse;
047import com.google.api.client.http.HttpResponseException;
048import com.google.api.client.json.GenericJson;
049import com.google.api.client.util.ArrayMap;
050
051/**
052 * Provider for blobs getting information from Box.
053 *
054 * @since 8.1
055 */
056public class BoxBlobProvider extends AbstractLiveConnectBlobProvider<BoxOAuth2ServiceProvider> {
057
058    private static final String CACHE_NAME = "box";
059
060    private static final String BOX_DOCUMENT_TO_BE_UPDATED_PP = "box_document_to_be_updated";
061
062    private static final String BOX_URL = "https://api.box.com/2.0/";
063
064    private static final String DOWNLOAD_CONTENT_URL = BOX_URL + "files/%s/content";
065
066    private static final String THUMBNAIL_CONTENT_URL = BOX_URL + "files/%s/thumbnail.jpg?min_height=320&min_width=320";
067
068    private static final String EMBED_URL = BOX_URL + "files/%s?fields=expiring_embed_link";
069
070    @Override
071    protected String getCacheName() {
072        return CACHE_NAME;
073    }
074
075    @Override
076    public String getPageProviderNameForUpdate() {
077        return BOX_DOCUMENT_TO_BE_UPDATED_PP;
078    }
079
080    @Override
081    public URI getURI(ManagedBlob blob, UsageHint usage, HttpServletRequest servletRequest) throws IOException {
082        LiveConnectFileInfo fileInfo = toFileInfo(blob);
083        String url = null;
084        switch (usage) {
085        case STREAM:
086        case DOWNLOAD:
087            url = getDownloadUrl(fileInfo);
088            break;
089        case VIEW:
090            url = retrieveSharedLink(fileInfo).getURL();
091            break;
092        case EMBED:
093            url = getEmbedUrl(fileInfo);
094            break;
095        }
096        return url == null ? null : asURI(url);
097    }
098
099    @Override
100    public InputStream getStream(ManagedBlob blob) throws IOException {
101        URI uri = getURI(blob, UsageHint.STREAM, null);
102        return uri == null ? null : doGet(uri.toString()).getContent();
103    }
104
105    @Override
106    public InputStream getThumbnail(ManagedBlob blob) throws IOException {
107        // TODO update method when https://github.com/box/box-java-sdk/issues/54 will be done
108        LiveConnectFileInfo fileInfo = toFileInfo(blob);
109        GenericUrl url = new GenericUrl(String.format(THUMBNAIL_CONTENT_URL, fileInfo.getFileId()));
110
111        HttpResponse response = executeAuthenticate(fileInfo, url, false);
112        int statusCode = response.getStatusCode();
113        if (statusCode == HttpStatus.SC_OK) {
114            return response.getContent();
115        } else if (statusCode == HttpStatus.SC_MOVED_TEMPORARILY || statusCode == HttpStatus.SC_ACCEPTED) {
116            response.disconnect();
117            return doGet(response.getHeaders().getLocation()).getContent();
118        }
119        throw new HttpResponseException(response);
120    }
121
122    @Override
123    public ManagedBlob freezeVersion(ManagedBlob blob, Document doc) throws IOException {
124        LiveConnectFileInfo fileInfo = toFileInfo(blob);
125        if (fileInfo.getRevisionId().isPresent()) {
126            // already frozen
127            return null;
128        }
129        BoxFile.Info boxFileInfo = retrieveBoxFileInfo(fileInfo);
130        // put the latest file version in cache
131        putFileInCache(new BoxLiveConnectFile(fileInfo, boxFileInfo));
132        String revisionId = boxFileInfo.getVersion().getVersionID();
133
134        fileInfo = new LiveConnectFileInfo(fileInfo.getUser(), fileInfo.getFileId(), revisionId);
135        BoxLiveConnectFile file = new BoxLiveConnectFile(fileInfo, boxFileInfo);
136        return toBlob(file);
137    }
138
139    protected BoxAPIConnection getBoxClient(NuxeoOAuth2Token token) throws IOException {
140        return getBoxClient(getCredential(token));
141    }
142
143    protected BoxAPIConnection getBoxClient(Credential credential) throws IOException {
144        return new BoxAPIConnection(credential.getAccessToken());
145    }
146
147    @Override
148    protected LiveConnectFile retrieveFile(LiveConnectFileInfo fileInfo) throws IOException {
149        return new BoxLiveConnectFile(fileInfo, retrieveBoxFileInfo(fileInfo));
150    }
151
152    protected BoxFile.Info retrieveBoxFileInfo(LiveConnectFileInfo fileInfo) throws IOException {
153        try {
154            return prepareBoxFile(fileInfo).getInfo();
155        } catch (BoxAPIException e) {
156            throw new IOException("Failed to retrieve Box file metadata", e);
157        }
158    }
159
160    private BoxSharedLink retrieveSharedLink(LiveConnectFileInfo fileInfo) throws IOException {
161        try {
162            return prepareBoxFile(fileInfo).createSharedLink(Access.OPEN, null, null);
163        } catch (BoxAPIException e) {
164            throw new IOException("Failed to retrieve Box shared link", e);
165        }
166    }
167
168    private BoxFile prepareBoxFile(LiveConnectFileInfo fileInfo) throws IOException {
169        BoxAPIConnection boxClient = getBoxClient(getCredential(fileInfo));
170        return new BoxFile(boxClient, fileInfo.getFileId());
171    }
172
173    /**
174     * Returns the temporary download url for input file.
175     *
176     * @param fileInfo the file info
177     * @return the temporary download url for input file
178     */
179    private String getDownloadUrl(LiveConnectFileInfo fileInfo) throws IOException {
180        // TODO update method when https://github.com/box/box-java-sdk/issues/182 will be done
181        GenericUrl url = new GenericUrl(String.format(DOWNLOAD_CONTENT_URL, fileInfo.getFileId()));
182        fileInfo.getRevisionId().ifPresent(revId -> url.put("version", revId));
183
184        HttpResponse response = executeAuthenticate(fileInfo, url, false);
185        response.disconnect();
186        if (response.getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY) {
187            return response.getHeaders().getLocation();
188        }
189        throw new HttpResponseException(response);
190    }
191
192    /**
193     * Returns the temporary embed url for input file.
194     *
195     * @param fileInfo the file info
196     * @return the temporary embed url for input file
197     */
198    @SuppressWarnings("unchecked")
199    private String getEmbedUrl(LiveConnectFileInfo fileInfo) throws IOException {
200        // TODO change below by box client call when it'll be available
201        GenericUrl url = new GenericUrl(String.format(EMBED_URL, fileInfo.getFileId()));
202
203        try {
204            HttpResponse response = executeAuthenticate(fileInfo, url, true);
205            GenericJson boxInfo = response.parseAs(GenericJson.class);
206            return ((ArrayMap<String, Object>) boxInfo.get("expiring_embed_link")).get("url").toString();
207        } catch (HttpResponseException e) {
208            // Extension not supported
209            if (e.getStatusCode() == HttpStatus.SC_REQUEST_URI_TOO_LONG) {
210                return null;
211            }
212            throw e;
213        }
214    }
215
216    private HttpResponse executeAuthenticate(LiveConnectFileInfo fileInfo, GenericUrl url, boolean followRedirect)
217            throws IOException {
218        Credential credential = getCredential(fileInfo);
219
220        HttpRequest request = getOAuth2Provider().getRequestFactory().buildGetRequest(url);
221        request.setHeaders(new HttpHeaders().setAuthorization("Bearer " + credential.getAccessToken()));
222        request.setFollowRedirects(followRedirect);
223        request.setThrowExceptionOnExecuteError(followRedirect);
224
225        return request.execute();
226    }
227
228    /**
229     * Executes a GET request
230     */
231    private HttpResponse doGet(String url) throws IOException {
232        HttpRequestFactory requestFactory = getOAuth2Provider().getRequestFactory();
233        return requestFactory.buildGetRequest(new GenericUrl(url)).execute();
234    }
235
236}