# WopiFileResource.java

    /*
     * Copyright (C) 2000 - 2021 Silverpeas
     *
     * This program is free software: you can redistribute it and/or modify
     * it under the terms of the GNU Affero General Public License as
     * published by the Free Software Foundation, either version 3 of the
     * License, or (at your option) any later version.
     *
     * As a special exception to the terms and conditions of version 3.0 of
     * the GPL, you may redistribute this Program in connection with Free/Libre
     * Open Source Software ("FLOSS") applications as described in Silverpeas's
     * FLOSS exception.  You should have received a copy of the text describing
     * the FLOSS exception, and it is also available here:
     * "https://www.silverpeas.org/legal/floss_exception.html"
     *
     * This program is distributed in the hope that it will be useful,
     * but WITHOUT ANY WARRANTY; without even the implied warranty of
     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     * GNU Affero General Public License for more details.
     *
     * You should have received a copy of the GNU Affero General Public License
     * along with this program.  If not, see <http://www.gnu.org/licenses/>.
     */

    package org.silverpeas.wbe.wopi;

    import org.silverpeas.core.admin.user.model.User;
    import org.silverpeas.core.annotation.WebService;
    import org.silverpeas.core.util.JSONCodec;
    import org.silverpeas.core.wbe.WbeFile;
    import org.silverpeas.core.wbe.WbeUser;
    import org.silverpeas.core.web.rs.annotation.Authenticated;
    import org.silverpeas.core.webapi.wbe.AbstractWbeFileResource;
    import org.silverpeas.core.webapi.wbe.WbeFileEditionContext;
    import org.silverpeas.core.webapi.wbe.WbeFileWrapper;
    import org.silverpeas.core.webapi.wbe.WbeResponseError;
    import org.silverpeas.kernel.util.StringUtil;

    import javax.inject.Inject;
    import javax.servlet.http.HttpServletRequest;
    import javax.ws.rs.GET;
    import javax.ws.rs.POST;
    import javax.ws.rs.Path;
    import javax.ws.rs.WebApplicationException;
    import javax.ws.rs.core.MediaType;
    import javax.ws.rs.core.Response;
    import javax.ws.rs.core.StreamingOutput;
    import java.io.IOException;
    import java.time.OffsetDateTime;
    import java.util.Optional;
    import java.util.Set;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;

    import static java.text.MessageFormat.format;
    import static java.time.OffsetDateTime.parse;
    import static java.util.Optional.*;
    import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM;
    import static javax.ws.rs.core.Response.Status.CONFLICT;
    import static org.silverpeas.core.date.TemporalFormatter.toIso8601;
    import static org.silverpeas.core.util.URLUtil.getFullApplicationURL;
    import static org.silverpeas.core.util.URLUtil.getServerURL;
    import static org.silverpeas.core.util.file.FileServerUtils.getImageURL;
    import static org.silverpeas.core.wbe.WbeLogger.logger;
    import static org.silverpeas.core.wbe.WbeSettings.getWbeUserIdPrefix;
    import static org.silverpeas.kernel.util.StringUtil.*;
    import static org.silverpeas.wbe.wopi.util.WopiSettings.*;

    /**
     * @author silveryocha
     */
    @WebService
    @Path("wbe/wopi/files/{fileId}")
    @Authenticated
    public class WopiFileResource extends AbstractWbeFileResource {

      private static final String WOPI_OVERRIDE_HEADER = "X-WOPI-Override";
      private static final String WOPI_USER_IDS_HEADER = "X-WOPI-ViewUserIds";
      static final String WOPI_LOCK_HEADER = "X-WOPI-Lock";

      private static final String LAST_MODIFIED_TIME_FIELD = "LastModifiedTime";

      @Inject
      private org.silverpeas.wbe.wopi.WopiLockResponseManager lockManager;

      /**
       * @see
       * <a href="https://wopi.readthedocs.io/projects/wopirest/en/latest/endpoints.html#files-endpoint"> WOPI spec,
       * files endpoint</a>
       */
      @GET
      public Response sendFileData() {
        return process(() -> {
          final WbeFileEditionContext context = getEditionContext();
          final WbeFile file = context.getFile();
          final WbeUser user = context.getUser();
          final String json = JSONCodec.encodeObject(o -> {
            final User spUser = user.asSilverpeas();
            final boolean canBeModifiedBy = file.canBeModifiedBy(spUser);
            final boolean webViewOnly = false;
            final HttpServletRequest request = getSilverpeasContext().getRequest();
            return o
                // File
                .put("OwnerId", getWbeUserIdPrefix() + file.owner())
                .put("BaseFileName", file.name())
                .put("Size", file.size())
                .put("UserId", user.getId())
                .put("Version", file.version())
                .put(LAST_MODIFIED_TIME_FIELD, formatLastModifiedTime(file))
                // Host capabilities
                .put("SupportsContainers", false)
                .put("SupportsDeleteFile", false)
                .put("SupportsEcosystem", false)
                .put("SupportsExtendedLockLength", false)
                .put("SupportsFolders", false)
                .put("SupportsGetLock", lockManager.isEnabled())
                .put("SupportsLocks", lockManager.isEnabled())
                .put("SupportsRename", false)
                .put("SupportsUpdate", false)
                .put("SupportsUserInfo", false)
                // UI
                .put("PostMessageOrigin", getServerURL(request))
                .put("ClosePostMessage", true)
                // User
                .put("UserFriendlyName", spUser.getDisplayedName())
                .putJSONObject("UserExtraInfo", e ->
                    e.put("avatar", getFullApplicationURL(request) + getImageURL(spUser.getAvatar(), "30x")))
                // User Permissions
                .put("ReadOnly", !canBeModifiedBy)
                .put("DisablePrint", webViewOnly)
                .put("DisableExport", webViewOnly)
                .put("RestrictedWebViewOnly", webViewOnly)
                .put("UserCanAttend", false)
                .put("UserCanNotWriteRelative", true)
                .put("UserCanPresent", false)
                .put("UserCanRename", false)
                .put("UserCanWrite", canBeModifiedBy);
            }
          );
          return Response.ok().type(MediaType.APPLICATION_JSON).entity(json).build();
        });
      }

      /**
       * @see
       * <a href="https://wopi.readthedocs.io/projects/wopirest/en/latest/endpoints.html#files-endpoint"> WOPI spec,
       * files endpoint</a>
       */
      @POST
      public Response receiveFileData() {
        return process(() -> {
          final HttpServletRequest request = getSilverpeasContext().getRequest();
          final String action = defaultStringIfNotDefined(request.getHeader(WOPI_OVERRIDE_HEADER));
          final WbeFile file = getEditionContext().getFile();
          final Optional<Response> response;
          if ("SP_CURRENT_USERS".equals(action)) {
            final Set<String> userIds = Stream
                .of(ofNullable(request.getHeader(WOPI_USER_IDS_HEADER)).orElse("").split("[, ;]"))
                .filter(StringUtil::isDefined)
                .map(String::trim)
                .collect(Collectors.toSet());
            getHostManager().notifyEditionWith(file, userIds);
            response = of(Response.ok().build());
          } else if (action.contains("LOCK")) {
            response = lockManager.manage(request, action, file);
          } else {
            response = empty();
          }
          return response.orElseGet(() -> Response.status(Response.Status.NOT_IMPLEMENTED).build());
        });
      }

      /**
       * @see
       * <a href="https://wopi.readthedocs.io/projects/wopirest/en/latest/endpoints.html#file-contents-endpoint"> WOPI spec,
       * file contents endpoint</a>
       */
      @GET
      @Path("contents")
      public Response sendFileContentData() {
        return process(() -> {
          final WbeFileEditionContext context = getEditionContext();
          final StreamingOutput streamingOutput = o -> context.getFile().loadInto(o);
          return Response.ok(streamingOutput, APPLICATION_OCTET_STREAM).build();
        });
      }

      /**
       * @see
       * <a href="https://wopi.readthedocs.io/projects/wopirest/en/latest/endpoints.html#file-contents-endpoint"> WOPI spec,
       * file contents endpoint</a>
       */
      @POST
      @Path("contents")
      public Response receiveFileContentData() {
        return process(() -> {
          final HttpServletRequest request = getSilverpeasContext().getRequest();
          final WbeFileEditionContext context = getEditionContext();
          final WbeFile file = context.getFile();
          of(isLockCapabilityEnabled())
              .filter(b -> b)
              .map(b -> request.getHeader(WOPI_LOCK_HEADER))
              .filter(l -> (isDefined(l) && !file.lock().exists()) || !file.lock().id().equals(l))
              .ifPresent(l -> {
                logger().debug(() -> format("WRITE CONFLICT because of not corresponding LOCK {0} on file {1}", l, file));
                throw new WbeResponseError(Response.status(CONFLICT)
                    .header(WOPI_LOCK_HEADER, file.lock().id())
                    .build());
              });
          getTimestampVerificationElements().ifPresent(e -> {
            final String timestampHeaderValue = request.getHeader(e.getFirst());
            if (isDefined(timestampHeaderValue)) {
              final OffsetDateTime timestampToVerify = parse(timestampHeaderValue);
              logger().debug(() -> format("timestamp {0} verified on file {1}", timestampToVerify, file));
              if (!timestampToVerify.isEqual(file.lastModificationDate())) {
                logger().debug(() -> format("WRITE CONFLICT because of not corresponding timestamp {0} on file {1}", timestampToVerify, file));
                throw new WbeResponseError(Response.status(CONFLICT)
                    .type(MediaType.APPLICATION_JSON).entity(e.getSecond())
                    .build());
              }
            } else {
              logger().debug(() -> format("no timestamp verification on file {0}", file));
            }
          });
          try {
            file.updateFrom(getSilverpeasContext().getRequest().getInputStream());
          } catch (IOException e) {
            throw new WebApplicationException(e, Response.Status.NOT_FOUND);
          }
          getExitFieldNameDetection()
              .filter(f -> getBooleanValue(request.getHeader(f)))
              .ifPresent(f -> getHostManager().revokeFile(file));
          final String json = JSONCodec.encodeObject(
              o -> o.put(LAST_MODIFIED_TIME_FIELD, formatLastModifiedTime(file)));
          return Response.ok().type(MediaType.APPLICATION_JSON).entity(json).build();
        });
      }

      /**
       * Formats for 'LastModifiedTime' field which MUST contains the ISO8601 round-trip time format
       * indicating the new/updated file's modified time in storage after successful save.
       * <p>
       * WARNING six digits of nano part MUST exists event if there is no nano data.
       * </p>
       * @param file a {@link WbeFile} instance (which is indeed into the WOPI context a
       * {@link WbeFileWrapper} instance).
       * @return an ISO8601 formatted string.
       */
      private String formatLastModifiedTime(final WbeFile file) {
        return toIso8601(file.lastModificationDate(), true).replace("Z", ".000000Z");
      }

      @Override
      protected WbeFileWrapper wrapWbeFile(final WbeFile file) {
        return new org.silverpeas.wbe.wopi.WopiFileWrapper(file);
      }
    }