/*******************************************************************************
* Copyright (c) 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
package com.ibm.ws.lars.rest;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.ws.rs.core.UriInfo;
import com.ibm.ws.lars.rest.exceptions.AssetPersistenceException;
import com.ibm.ws.lars.rest.exceptions.InvalidJsonAssetException;
import com.ibm.ws.lars.rest.exceptions.NonExistentArtefactException;
import com.ibm.ws.lars.rest.exceptions.RepositoryException;
import com.ibm.ws.lars.rest.model.Asset;
import com.ibm.ws.lars.rest.model.AssetCursor;
import com.ibm.ws.lars.rest.model.Attachment;
import com.ibm.ws.lars.rest.model.AttachmentContentMetadata;
import com.ibm.ws.lars.rest.model.AttachmentContentResponse;
import com.ibm.ws.lars.rest.model.AttachmentList;
import com.ibm.ws.lars.rest.model.RepositoryResourceLifecycleException;
/**
* This needs to enforce:<br>
* - state transitions<br>
* -- no updates allowed to 'published' assets<br>
* -- time stamp updates<br>
* -- partial updates (allowed or not??)
*/
@ApplicationScoped
public class AssetServiceLayer {
@Inject
private Persistor persistenceBean;
@Inject
private Configuration configuration;
/**
* @see Persistor#retrieveAllAssets()
*/
public AssetCursor retrieveAllAssets() {
return persistenceBean.retrieveAllAssets();
}
/**
* @see Persistor#retrieveAllAssets(Collection,String, PaginationOptions, SortOptions)
*/
public AssetCursor retrieveAllAssets(Collection<AssetFilter> filters, String searchTerm, PaginationOptions pagination, SortOptions sortOptions) {
return persistenceBean.retrieveAllAssets(filters, searchTerm, pagination, sortOptions);
}
/**
* @see Persistor#countAllAssets(Collection, String)
*/
public int countAllAssets(Collection<AssetFilter> filters, String searchTerm) {
return persistenceBean.countAllAssets(filters, searchTerm);
}
/**
* Summarizes a list of fields from the assets matched by the given filters and search term.
* <p>
* For each field, the result is the list of unique values that are stored in that field, across
* all of the assets matched by the filters and searchTerm.
* <p>
* This result is put into a map of the following form:
*
* <pre>
* {
* "filterName": fieldName
* "filterValue": listOfDistinctValues
* }
* </pre>
* <p>
* Filters and searchTerm are treated the same as they are in
* {@link #retrieveAllAssets(Collection, String, PaginationOptions, SortOptions)}.
*
* @param fields a list of fields to summarize
* @param filters a list of filters, which may be empty
* @param searchTerm a term to search for, which may be null
* @return a list of result maps, one for each field
*/
public List<Map<String, Object>> summarizeAssets(List<String> fields, Collection<AssetFilter> filters, String searchTerm) {
List<Map<String, Object>> result = new ArrayList<>();
for (String field : fields) {
List<Object> values = persistenceBean.getDistinctValues(field, filters, searchTerm);
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("filterName", field);
resultMap.put("filterValue", values);
result.add(resultMap);
}
return result;
}
/**
* @param asset
* @param creatorName The name of the user who is creating the asset. Must not be null.
* @return
* @throws InvalidJsonAssetException
*/
public Asset createAsset(Asset asset, String creatorName) throws InvalidJsonAssetException {
Asset newAsset = new Asset(asset);
verifyNewAsset(newAsset);
String now = IsoDate.format(new Date());
newAsset.setCreatedOn(now);
newAsset.setLastUpdatedOn(now);
newAsset.setCreatedBy(creatorName);
newAsset.getProperties().put("state", Asset.State.DRAFT.getValue());
return persistenceBean.createAsset(newAsset);
}
/**
* @param assetId
* @return
* @throws NonExistentArtefactException
*/
public Asset retrieveAsset(String assetId, UriInfo uriInfo) throws NonExistentArtefactException {
Asset asset = persistenceBean.retrieveAsset(assetId);
AttachmentList attachments = persistenceBean.findAttachmentsForAsset(assetId);
for (Attachment attachment : attachments) {
computeAttachmentURL(attachment, uriInfo);
}
asset.setAttachments(attachments);
return asset;
}
/**
* @param assetId
* @param asset
* @return
* @throws InvalidJsonAssetException
* @throws NonExistentArtefactException
*/
public Asset updateAsset(String assetId, Asset asset) throws InvalidJsonAssetException, NonExistentArtefactException {
Asset existingAsset = persistenceBean.retrieveAsset(assetId);
if (existingAsset == null) {
throw new NonExistentArtefactException(assetId, RepositoryRESTResource.ArtefactType.ASSET);
}
return persistenceBean.updateAsset(assetId, asset);
}
/**
* Throws an exception if the state transition is invalid.
*
* @param action
* @param id
*
* @throws RepositoryResourceLifecycleException
*/
public void updateAssetState(Asset.StateAction action, String id) throws RepositoryResourceLifecycleException, NonExistentArtefactException {
Asset existingAsset = persistenceBean.retrieveAsset(id);
action.performAction(existingAsset);
existingAsset.setLastUpdatedOn(IsoDate.format(new Date()));
try {
persistenceBean.updateAsset(id, existingAsset);
} catch (InvalidJsonAssetException e) {
// This should never happen, as the asset was retrieved from the persistence layer,
// and the only changes were by us. Don't percolate the json exception, as that would
// make it look like user error.
throw new RepositoryException("JSON retrieved from asset store could not be save back again", e);
}
}
/**
* @param assetId
* @throws NonExistentArtefactException
*/
public void deleteAsset(String assetId) throws NonExistentArtefactException {
// Retrieve the asset to ensure it exists
persistenceBean.retrieveAsset(assetId);
// Delete all attachments belonging to the asset
for (Attachment attachment : persistenceBean.findAttachmentsForAsset(assetId)) {
deleteAttachment(attachment.get_id());
}
// Delete the asset itself
persistenceBean.deleteAsset(assetId);
}
private Attachment createAttachment(String assetId, String name, Attachment originalAttachmentMetadata, String contentType,
InputStream attachmentContentStream, UriInfo uriInfo) throws InvalidJsonAssetException, AssetPersistenceException, NonExistentArtefactException {
// Check that the parent exists
try {
persistenceBean.retrieveAsset(assetId);
} catch (NonExistentArtefactException e) {
// The message from the PersistenceLayer is unhelpful in this context, so send back a better one
throw new NonExistentArtefactException("The parent asset for this attachment (id="
+ assetId + ") does not exist in the repository.");
}
Attachment attachmentMetadata = new Attachment(originalAttachmentMetadata);
// Add necessary fields to the attachment (JSON) metadata
if (attachmentMetadata.get_id() == null) {
attachmentMetadata.set_id(persistenceBean.allocateNewId());
}
attachmentMetadata.setAssetId(assetId);
if (contentType != null) {
attachmentMetadata.setContentType(contentType);
}
attachmentMetadata.setName(name);
attachmentMetadata.setUploadOn(IsoDate.format(new Date()));
// Create the attachment content
if (attachmentContentStream != null) {
AttachmentContentMetadata contentMetadata = persistenceBean.createAttachmentContent(name, contentType, attachmentContentStream);
// TODO perhaps we should try to clean up after ourselves and delete the attachmentMetadata
// TODO seriously, this is one of the places where we reaslise that using a DB that doesn't
// support transactions means we don't get some of the guarantees that we might be used to.
attachmentMetadata.setGridFSId(contentMetadata.filename);
attachmentMetadata.setSize(contentMetadata.length);
}
Attachment returnedAttachment = persistenceBean.createAttachmentMetadata(attachmentMetadata);
computeAttachmentURL(returnedAttachment, uriInfo);
return returnedAttachment;
}
public Attachment createAttachmentWithContent(String assetId, String name, Attachment attachmentMetadata, String contentType,
InputStream attachmentContentStream, UriInfo uriInfo) throws InvalidJsonAssetException, AssetPersistenceException, NonExistentArtefactException {
// The attachment has content, so the URL must not be set, and the
// linkType must not be set (i.e. it must be null).
String url = attachmentMetadata.getUrl();
if (url != null) {
throw new InvalidJsonAssetException("An attachment should not have the URL set if it is created with content");
}
String stringType = attachmentMetadata.getLinkType();
if (stringType != null) {
throw new InvalidJsonAssetException("The link type must not be set for an attachment with content");
}
return createAttachment(assetId, name, attachmentMetadata, contentType, attachmentContentStream, uriInfo);
}
/**
* Create an attachment where the binary is stored elsewhere. This type of asset must supply the
* URL of the binary in the URL field of the supplied Attachment object. If this is not set,
* then an InvalidJsonAssetException will be thrown
*
* @param assetId
* @param name
* @param attachmentMetadata
* @return
* @throws InvalidJsonAssetException
* @throws AssetPersistenceException
* @throws NonExistentArtefactException
*/
public Attachment createAttachmentNoContent(String assetId, String name, Attachment attachmentMetadata, UriInfo uriInfo) throws InvalidJsonAssetException,
AssetPersistenceException, NonExistentArtefactException {
// There is no content, so an external URL must be set, and the link
// type must be DIRECT or WEB_PAGE
String url = attachmentMetadata.getUrl();
if (url == null) {
throw new InvalidJsonAssetException("The URL of the supplied attachment was null");
}
String stringType = attachmentMetadata.getLinkType();
if (stringType == null) {
throw new InvalidJsonAssetException("The link type for the attachment was not set.");
}
Attachment.LinkType linkType = Attachment.LinkType.forValue(stringType);
if (linkType == null || (linkType != Attachment.LinkType.DIRECT && linkType != Attachment.LinkType.WEB_PAGE)) {
throw new InvalidJsonAssetException("The link type for the attachment was set to an invalid value: " + stringType);
}
return createAttachment(assetId, name, attachmentMetadata, null, null, uriInfo);
}
public void deleteAttachment(String attachmentId) {
try {
Attachment attachment = persistenceBean.retrieveAttachmentMetadata(attachmentId);
if (attachment.getGridFSId() != null) {
persistenceBean.deleteAttachmentContent(attachment.getGridFSId());
}
persistenceBean.deleteAttachmentMetadata(attachmentId);
} catch (NonExistentArtefactException ex) {
// Do nothing if attachment does not exist
}
}
public Attachment retrieveAttachmentMetadata(String assetId, String attachmentId, UriInfo uriInfo) throws NonExistentArtefactException {
Attachment attachment = persistenceBean.retrieveAttachmentMetadata(attachmentId);
if (!Objects.equals(attachment.getAssetId(), assetId)) {
throw new NonExistentArtefactException("Asset " + assetId + " has no associated attachment with id " + attachmentId);
}
computeAttachmentURL(attachment, uriInfo);
return attachment;
}
public AttachmentContentResponse retrieveAttachmentContent(String assetId, String attachmentId, String name, UriInfo uriInfo) throws
NonExistentArtefactException {
Attachment attachmentMetadata = retrieveAttachmentMetadata(assetId, attachmentId, uriInfo);
if (!Objects.equals(name, attachmentMetadata.getName())) {
throw new NonExistentArtefactException("Attachment with id " + attachmentId + " and name " + name + " does not exist in the repository.");
}
String gridFSId = attachmentMetadata.getGridFSId();
return persistenceBean.retrieveAttachmentContent(gridFSId);
}
/**
* There are no required fields for an asset, all that needs to be checked is that there is no
* _id field. It is not allowed to specify an id in the JSON when an asset is being created.
* Allowing a user to do so is exposing a mongo implementation.
*/
void verifyNewAsset(Asset newAsset) throws InvalidJsonAssetException {
String id = newAsset.get_id();
if (id != null) {
throw new InvalidJsonAssetException("When creating a new asset, the _id field must be blank");
}
}
/**
* Computes and sets the URL for an attachment if the attachment's content is stored in lars.
* <p>
* If the attachment is stored externally, the URL is not changed.
* <p>
* The start of the URL is computed from the base URL of the request, unless it's overridden in
* the server.xml.
*
* @param attachment the attachment for which to update and set the URL
* @param uriInfo the UriInfo from the current request
*/
private void computeAttachmentURL(Attachment attachment, UriInfo uriInfo) {
// LinkType != null -> asset is not stored in LARS
// Therefore there should be an external URL in the attachment
if (attachment.getLinkType() != null) {
return;
}
// For assets stored in LARS, we need to compute the URL and store it in the attachment
String encodedName;
try {
encodedName = URLEncoder.encode(attachment.getName(), "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new AssertionError("This should never happen.", e);
}
String url = configuration.getRestBaseUri(uriInfo) + "assets/" + attachment.getAssetId() + "/attachments/" + attachment.get_id() + "/" + encodedName;
attachment.setUrl(url);
}
}