/**
* Copyright (C) 2011 BonitaSoft S.A.
* BonitaSoft, 32 rue Gustave Eiffel - 38000 Grenoble
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2.0 of the License, or
* (at your option) any later version.
* 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.bonitasoft.web.rest.server.framework;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpSession;
import org.bonitasoft.console.common.server.utils.UnauthorizedFolderException;
import org.bonitasoft.web.rest.server.framework.api.Datastore;
import org.bonitasoft.web.rest.server.framework.api.DatastoreHasAdd;
import org.bonitasoft.web.rest.server.framework.api.DatastoreHasDelete;
import org.bonitasoft.web.rest.server.framework.api.DatastoreHasGet;
import org.bonitasoft.web.rest.server.framework.api.DatastoreHasSearch;
import org.bonitasoft.web.rest.server.framework.api.DatastoreHasUpdate;
import org.bonitasoft.web.rest.server.framework.exception.APIFileUploadNotFoundException;
import org.bonitasoft.web.rest.server.framework.exception.ForbiddenAttributesException;
import org.bonitasoft.web.rest.server.framework.search.ItemSearchResult;
import org.bonitasoft.web.rest.server.framework.utils.FilePathBuilder;
import org.bonitasoft.web.toolkit.client.common.exception.api.APIException;
import org.bonitasoft.web.toolkit.client.common.exception.api.APIForbiddenException;
import org.bonitasoft.web.toolkit.client.common.exception.api.APIItemNotFoundException;
import org.bonitasoft.web.toolkit.client.common.exception.api.APIMethodNotAllowedException;
import org.bonitasoft.web.toolkit.client.common.util.MapUtil;
import org.bonitasoft.web.toolkit.client.data.APIID;
import org.bonitasoft.web.toolkit.client.data.item.DummyItem;
import org.bonitasoft.web.toolkit.client.data.item.IItem;
import org.bonitasoft.web.toolkit.client.data.item.ItemDefinition;
import org.bonitasoft.web.toolkit.client.data.item.attribute.ItemAttribute;
import org.bonitasoft.web.toolkit.client.data.item.template.ItemHasCreator;
import org.bonitasoft.web.toolkit.client.data.item.template.ItemHasLastUpdateDate;
import org.bonitasoft.web.toolkit.client.data.item.template.ItemHasLastUpdater;
import org.bonitasoft.web.toolkit.client.data.item.template.ItemHasUniqueId;
/**
* @author Séverin Moussel
*/
public abstract class API<ITEM extends IItem> {
protected ItemDefinition<ITEM> itemDefinition = null;
private final Map<String, Deployer> deployers = new HashMap<>();
private static Logger LOGGER = Logger.getLogger(API.class.getName());
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// CONSTRUCTORS
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public API() {
this.itemDefinition = defineItemDefinition();
}
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SETTERS AND GETTERS
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* The ServletCall responsible of this service.
*/
private APIServletCall caller = null;
/**
* Set the caller.
*
* @param caller
* The ServletCall responsible of this service.
*/
public final void setCaller(final APIServletCall caller) {
this.caller = caller;
}
/**
* Get the caller.
*/
protected final APIServletCall getCaller() {
return this.caller;
}
/**
* @return the itemDefinition
*/
public final ItemDefinition<ITEM> getItemDefinition() {
return this.itemDefinition;
}
/**
* Define the ItemDefinition for current class
*/
protected ItemDefinition<ITEM> defineItemDefinition() {
// FIXME [API V2] Make this method abstract after suppression of API V1
return null;
}
/**
* @see org.bonitasoft.web.toolkit.server.ServletCall#getHttpSession()
*/
protected final HttpSession getHttpSession() {
return this.caller.getHttpSession();
}
protected String getLocale() {
return this.caller.getLocale();
}
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// INPUT / OUTPUT
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
protected final String getParameterAsString(final String parameterName) {
return parameterName;
}
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ENTRY POINTS
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@SuppressWarnings("unchecked")
public ITEM runAdd(final IItem item) {
// FIXME Activate at end of APIs refactoring
// if (!(this instanceof APIHasAdd)) {
// throw new APIMethodNotAllowedException("POST method not allowed.");
// }
// Stop there if forbidden attributes are set
checkForbiddenAttributes(item.getAttributes());
// Run specific implementation
return add((ITEM) item);
}
@SuppressWarnings("unchecked")
public ITEM add(final ITEM item) {
final Datastore datastore = getDefaultDatastore();
if (datastore == null || !(datastore instanceof DatastoreHasAdd<?>)) {
throw new APIMethodNotAllowedException("POST method not allowed.");
}
return ((DatastoreHasAdd<ITEM>) datastore).add(item);
}
public ITEM runUpdate(final APIID id, final Map<String, String> attributes) {
// FIXME Activate at end of APIs refactoring
// if (!(this instanceof APIHasUpdate)) {
// throw new APIMethodNotAllowedException("PUT method not allowed.");
// }
id.setItemDefinition(getItemDefinition());
// Stop there if forbidden attributes are set
checkForbiddenAttributes(attributes);
// Run specific implementation
return this.update(id, attributes);
}
@SuppressWarnings("unchecked")
public ITEM update(final APIID id, final Map<String, String> attributes) {
final Datastore datastore = getDefaultDatastore();
if (datastore == null || !(datastore instanceof DatastoreHasUpdate<?>)) {
throw new APIMethodNotAllowedException("PUT method not allowed.");
}
return ((DatastoreHasUpdate<ITEM>) datastore).update(id, attributes);
}
public ITEM runGet(final APIID id, final List<String> deploys, final List<String> counters) {
// FIXME Activate at end of APIs refactoring
// if (!(this instanceof APIHasGet)) {
// throw new APIMethodNotAllowedException("GET method not allowed.");
// }
id.setItemDefinition(getItemDefinition());
final ITEM item = get(id);
if (item == null) {
throw new APIItemNotFoundException(getItemDefinition().getToken(), id);
}
fillDeploys(item, deploys != null ? deploys : new ArrayList<String>());
fillCounters(item, counters != null ? counters : new ArrayList<String>());
return item;
}
@SuppressWarnings("unchecked")
public ITEM get(final APIID id) {
final Datastore datastore = getDefaultDatastore();
if (datastore == null || !(datastore instanceof DatastoreHasGet<?>)) {
throw new APIMethodNotAllowedException("GET method not allowed.");
}
return ((DatastoreHasGet<ITEM>) datastore).get(id);
}
public ItemSearchResult<ITEM> runSearch(final int page, final int resultsByPage, final String search, final String orders,
final Map<String, String> filters, final List<String> deploys, final List<String> counters) {
// FIXME Activate at end of APIs refactoring
// if (!(this instanceof APIHasSearch)) {
// throw new APIMethodNotAllowedException("SEARCH method not allowed.");
// }
String realOrders = orders;
if (orders == null || orders.length() == 0) {
realOrders = defineDefaultSearchOrder();
// TODO remove this test and exception while the automated unit test over all APis
if (realOrders == null) {
throw new APIException("No default search order defined. Please, override the defineDefaultSearchOrder method in " + this.getClass().toString()
+ ".");
}
}
final ItemSearchResult<ITEM> searchResult = search(page, resultsByPage, search, realOrders, filters != null ? filters : new HashMap<String, String>());
for (final ITEM item : searchResult.getResults()) {
fillDeploys(item, deploys != null ? deploys : new ArrayList<String>());
fillCounters(item, counters != null ? counters : new ArrayList<String>());
}
return searchResult;
}
@SuppressWarnings("unchecked")
public ItemSearchResult<ITEM> search(final int page, final int resultsByPage, final String search, final String orders, final Map<String, String> filters) {
final Datastore datastore = getDefaultDatastore();
if (datastore == null || !(datastore instanceof DatastoreHasSearch<?>)) {
throw new APIMethodNotAllowedException("SEARCH method not allowed.");
}
return ((DatastoreHasSearch<ITEM>) datastore).search(page, resultsByPage, search, orders, filters);
}
/**
* Define the default search order.
*/
public String defineDefaultSearchOrder() {
return null;
}
public void runDelete(final List<APIID> ids) {
// FIXME Activate at end of APIs refactoring
// if (!(this instanceof APIHasDelete)) {
// throw new APIMethodNotAllowedException("DELETE method not allowed.");
// }
for (final APIID id : ids) {
id.setItemDefinition(getItemDefinition());
}
delete(ids);
}
public void delete(final List<APIID> ids) {
final Datastore datastore = getDefaultDatastore();
if (datastore == null || !(datastore instanceof DatastoreHasDelete)) {
throw new APIMethodNotAllowedException("DELETE method not allowed.");
}
((DatastoreHasDelete) datastore).delete(ids);
}
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// AUTOMATED CRUDS BASED ON INTERFACES
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
protected Datastore defineDefaultDatastore() {
return null;
}
public final Datastore getDefaultDatastore() {
return this.defineDefaultDatastore();
}
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// PARAMETERS TOOLS
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
protected final File getUploadedFile(final String attributeName, final String attributeValue) throws IOException {
if (attributeValue == null || attributeValue.isEmpty()) {
return null;
}
final String tmpIconPath = getCompleteTempFilePath(attributeValue);
final File file = new File(tmpIconPath);
if (!file.exists()) {
throw new APIFileUploadNotFoundException(attributeName, attributeValue);
}
return file;
}
abstract protected String getCompleteTempFilePath(String path) throws IOException;
/**
* Upload the file to the defined directory and rename it to make sure its filename is unique.<br>
* The original filename will be kept.
*
* @param attributeName
* The name of the attribute representing the file.
* @param attributeValue
* The value of the attribute representing the file.
* @param newDirectory
* The destination directory path.
* @return This method return the file in the destination directory.
*/
protected final File uploadAutoRename(final String attributeName, final String attributeValue, final String newDirectory) {
try {
final File destinationDirectory = new File(newDirectory);
String destinationFilename = getUploadedFile(attributeName, attributeValue).getName();
if (!destinationDirectory.exists()) {
destinationDirectory.mkdirs();
}
final String extension = this.getFileExtension(destinationFilename);
final File destinationFile = File.createTempFile("avatar", extension, destinationDirectory);
destinationFilename = destinationFile.getName().substring(0, destinationFile.getName().length() - extension.length());
return upload(attributeName, attributeValue, newDirectory, destinationFilename);
} catch (final UnauthorizedFolderException e) {
throw new APIForbiddenException(e.getMessage());
} catch (final IOException e) {
throw new APIException(e);
}
}
/**
* Rename and upload the file to the defined directory.
*
* @param attributeName
* The name of the attribute representing the file.
* @param attributeValue
* The value of the attribute representing the file.
* @param newDirectory
* The destination directory path.
* @param newName
* The name to set to the file without the extension (the original extension will be kept)
* @return This method return the file in the destination directory.
* @throws IOException
*/
protected final File upload(final String attributeName, final String attributeValue, final String newDirectory, final String newName) throws IOException {
// Check if the destination directory already exists. If not, creates it.
final File destinationDirectory = new File(newDirectory);
if (!destinationDirectory.exists()) {
destinationDirectory.mkdir();
}
// Construct the destination fileName
final File file = getUploadedFile(attributeName, attributeValue);
String destinationName = file.getName();
if (newName != null) {
destinationName = newName + getFileExtension(file.getName());
}
// Move the file
final File destinationFile = new File(destinationDirectory.getAbsolutePath() + File.separator + destinationName);
try {
destinationFile.delete();
Files.move(file.toPath(), destinationFile.toPath());
} catch (final Exception e) {
e.getMessage();
}
return destinationFile;
}
/**
* @param fileName
* @return
*/
private String getFileExtension(final String fileName) {
String extension = "";
final int dotPos = fileName.lastIndexOf('.');
if (dotPos >= 0) {
extension = fileName.substring(dotPos);
}
return extension;
}
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// DEPLOYS AND COUNTERS
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public void addDeployer(final Deployer deployer) {
deployers.put(deployer.getDeployedAttribute(), deployer);
}
public Map<String, Deployer> getDeployers() {
return Collections.unmodifiableMap(deployers);
}
protected void fillDeploys(final ITEM item, final List<String> deploys) {
for (final String attribute : deploys) {
deployAttribute(attribute, item);
}
}
private void deployAttribute(final String attribute, final ITEM item) {
if (deployers.containsKey(attribute)) {
try {
deployers.get(attribute).deployIn(item);
} catch (final Exception e) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, getFailedDeployMessage(attribute, item), e);
} else if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.log(Level.INFO, getFailedDeployMessage(attribute, item));
}
}
}
}
protected String getFailedDeployMessage(final String attribute, final ITEM item) {
return "Could not deploy attribute '" + attribute + "' on item " + item.toString();
}
protected void fillCounters(final ITEM item, final List<String> counters) {
// Do Nothing if not override
}
/**
* @param attributeName
* @param deploys
* @param item
*/
protected final boolean isDeployable(final String attributeName, final List<String> deploys, final IItem item) {
final String attributeValue = item.getAttributeValue(attributeName);
return deploys.contains(attributeName) && attributeValue != null && APIID.makeAPIID(attributeValue) != null;
}
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// UPLOADS
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private List<String> getFileAttributes() {
final List<String> results = new ArrayList<>();
for (final ItemAttribute attribute : getItemDefinition().getAttributes()) {
if (attribute.getType().equals(ItemAttribute.TYPE.IMAGE) || attribute.getType().equals(ItemAttribute.TYPE.FILE)) {
results.add(attribute.getName());
}
}
return results;
}
/**
* Upload and replace a file in an item<br />
* The resulted path will be "subFolder/filename.ext"
*
* @param attributeName
* The name of the attribute that contains a file to upload
* @param item
* The item containing the attribute to upload
* @param targetPath
* The path of the directory where the uploaded file will be stored
* @param prefix
* The specific folder under targetFolderPath
*/
protected final void uploadForAdd(final String attributeName, final IItem item, final String targetPath, final String prefix) {
if (item.getAttributeValue(attributeName) == null || item.getAttributeValue(attributeName).isEmpty()) {
return;
}
final String filename = uploadAutoRename(attributeName, item.getAttributeValue(attributeName), targetPath).getName();
item.setAttribute(attributeName, new FilePathBuilder(prefix).append(filename).toString());
}
/**
* @param attributeName
* @param item
* @param targetPath
* @param prefix
*/
private void deleteFile(final String attributeName, final IItem item, final String targetPath, final String prefix) {
if (item == null || item.getAttributeValue(attributeName) == null || item.getAttributeValue(attributeName).isEmpty()) {
return;
}
final String filePath = item.getAttributeValue(attributeName);
filePath.substring(prefix.length());
if (filePath != null && !filePath.isEmpty()) {
new File(new FilePathBuilder(targetPath).append(filePath).toString()).delete();
}
}
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// FORBIDDEN ATTRIBUTES
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Override this method to define attributes that are not allowed to be set manually during ADD or UPDATE.
*
* @return This method must returns a List of forbidden attributes' name.
*/
protected List<String> defineReadOnlyAttributes() {
return null;
}
private void checkForbiddenAttributes(final Map<String, String> attributes) {
// List forbidden attributes
final List<String> forbiddenAttributes = new ArrayList<>();
final List<String> definedForbiddenAttributes = defineReadOnlyAttributes();
if (definedForbiddenAttributes != null) {
forbiddenAttributes.addAll(definedForbiddenAttributes);
}
forbiddenAttributes.addAll(getForbiddenAttributesByInterfaces());
// No forbidden attributes defined, no need to go further in the check process.
if (forbiddenAttributes == null || forbiddenAttributes.size() == 0) {
return;
}
// List forbidden attributes found in the request
final List<String> errorAttributes = new ArrayList<>();
for (final String forbiddenAttribute : forbiddenAttributes) {
if (!MapUtil.isBlank(attributes, forbiddenAttribute)) {
errorAttributes.add(forbiddenAttribute);
}
}
// If at least one is found, throw a ForbiddenAttributesException
if (errorAttributes.size() > 0) {
throw new ForbiddenAttributesException(errorAttributes);
}
}
/**
* @return
*/
private List<String> getForbiddenAttributesByInterfaces() {
final List<String> forbiddenAttributes = new ArrayList<>();
@SuppressWarnings("unchecked")
// final T modelItem = (T) getItemDefinition().createItem();
final ITEM modelItem = (ITEM) new DummyItem();
if (modelItem instanceof ItemHasLastUpdateDate) {
forbiddenAttributes.add(ItemHasLastUpdateDate.ATTRIBUTE_LAST_UPDATE_DATE);
}
if (modelItem instanceof ItemHasLastUpdater) {
forbiddenAttributes.add(ItemHasLastUpdater.ATTRIBUTE_LAST_UPDATE_DATE);
forbiddenAttributes.add(ItemHasLastUpdater.ATTRIBUTE_LAST_UPDATE_USER_ID);
}
if (modelItem instanceof ItemHasCreator) {
forbiddenAttributes.add(ItemHasCreator.ATTRIBUTE_CREATED_BY_USER_ID);
forbiddenAttributes.add(ItemHasCreator.ATTRIBUTE_CREATION_DATE);
}
if (modelItem instanceof ItemHasUniqueId) {
forbiddenAttributes.add(ItemHasUniqueId.ATTRIBUTE_ID);
}
return forbiddenAttributes;
}
}