/**
* Abiquo community edition
* cloud management application for hybrid clouds
* Copyright (C) 2008-2010 - Abiquo Holdings S.L.
*
* This application is free software; you can redistribute it and/or
* modify it under the terms of the GNU LESSER GENERAL PUBLIC
* LICENSE as published by the Free Software Foundation under
* version 3 of the License
*
* This software 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
* LESSER GENERAL PUBLIC LICENSE v.3 for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
package com.abiquo.am.resources;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Providers;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.wink.common.annotations.Parent;
import org.apache.wink.common.model.multipart.InMultiPart;
import org.apache.wink.common.model.multipart.InPart;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import com.abiquo.am.exceptions.AMError;
import com.abiquo.am.services.ErepoFactory;
import com.abiquo.am.services.TemplateConventions;
import com.abiquo.am.services.TemplateService;
import com.abiquo.am.services.notify.AMNotifier;
import com.abiquo.appliancemanager.exceptions.AMException;
import com.abiquo.appliancemanager.exceptions.EventException;
import com.abiquo.appliancemanager.transport.TemplateDto;
import com.abiquo.appliancemanager.transport.TemplateIdDto;
import com.abiquo.appliancemanager.transport.TemplateIdsDto;
import com.abiquo.appliancemanager.transport.TemplateStateDto;
import com.abiquo.appliancemanager.transport.TemplateStatusEnumType;
import com.abiquo.appliancemanager.transport.TemplatesStateDto;
@Parent(EnterpriseRepositoryResource.class)
@Path(TemplatesResource.OVFPI_PATH)
@Controller
public class TemplatesResource
{
private final static Logger LOG = LoggerFactory.getLogger(TemplatesResource.class);
public static final String OVFPI_PATH = ApplianceManagerPaths.TEMPLATE_PATH;
// public static final String GET_IDS_ACTION = "action/getstates";
public static final String QUERY_PRAM_STATE = "state";
@Autowired
private AMNotifier notifier;
@Autowired
private TemplateService templateService;
/**
* include bundles <br>
* XXX do not include DOWNLOADING or ERROR status
*/
@GET
public TemplatesStateDto getTemplateStatus(
@PathParam(EnterpriseRepositoryResource.ENTERPRISE_REPOSITORY) final String idEnterprise,
@QueryParam(QUERY_PRAM_STATE) final TemplateStatusEnumType state)
{
TemplatesStateDto list = new TemplatesStateDto();
for (TemplateStateDto stt : ErepoFactory.getRepo(idEnterprise).getTemplateStates())
{
if (state == null || stt.getStatus().equals(state))
{
list.getCollection().add(stt);
}
}
return list;
}
@POST
@Consumes(MediaType.APPLICATION_XML)
public TemplatesStateDto getTemplatesStatus(
@PathParam(EnterpriseRepositoryResource.ENTERPRISE_REPOSITORY) final String idEnterprise,
final TemplateIdsDto ids)
{
TemplatesStateDto list = new TemplatesStateDto();
for (TemplateIdDto templateId : ids.getCollection())
{
try
{
list.getCollection().add(
templateService.getTemplateStatusIncludeProgress(templateId.getOvfId(),
idEnterprise));
}
catch (Exception e)
{
list.getCollection().add(errorRetrievingState(e, templateId.getOvfId()));
}
}
return list;
}
private TemplateStateDto errorRetrievingState(final Exception error, final String ovfid)
{
LOG.error("Can't get state of {}", ovfid, error);
TemplateStateDto state = new TemplateStateDto();
state.setOvfId(ovfid);
state.setStatus(TemplateStatusEnumType.ERROR);
state.setErrorCause(error.getMessage());
return state;
}
/**
* Never return error. Use GET_STATUS to see errors
*/
@POST
@Consumes(MediaType.TEXT_PLAIN)
public void downloadTemplate(
@PathParam(EnterpriseRepositoryResource.ENTERPRISE_REPOSITORY) final String erId,
final String ovfId)
{
LOG.debug("[deploy] {}", ovfId);
if (!TemplateConventions.isValidOVFLocation(ovfId))
{
throw new AMException(AMError.TEMPLATE_INVALID_LOCATION);
}
switch (templateService.getTemplateStatusIncludeProgress(ovfId, erId).getStatus())
{
case DOWNLOADING:
case DOWNLOAD:
throw new AMException(AMError.TEMPLATE_INSTALL_ALREADY);
case ERROR:
templateService.delete(erId, ovfId);
break;
default:
break;
}
if (ovfId.startsWith("upload"))
{
throw new AMException(AMError.TEMPLATE_UPLOAD, String.format(
"Can not deply an uploaded package %s", ovfId));
}
try
{
templateService.startDownload(erId, ovfId);
}
catch (AMException e)
{
notifier.setTemplateStatusError(erId, ovfId, e.toString());
throw e;
// XXX the request ends successfully but the ovf package status is ERROR
}
}
@POST
@Consumes("multipart/form-data")
public Response uploadTemplate(@Context final HttpHeaders headers,
@PathParam(EnterpriseRepositoryResource.ENTERPRISE_REPOSITORY) final String erId,
final InMultiPart mp, @Context final Providers providers) throws IOException,
EventException
{
TemplateDto diskInfo = null;
String errorMsg = null;
try
{
InPart diskInfoPart = mp.next();
diskInfo = readTemplateDtoFromMultipart(diskInfoPart, headers, providers);
}
catch (Exception e)
{
if (!StringUtils.isBlank(e.getMessage()))
{
errorMsg = e.getMessage();
}
else
{
errorMsg = "Error uploading the image";
}
diskInfo.setDiskFilePath(TemplateConventions.TEMPLATE_STATUS_ERROR_MARK);
}
// XXX notify DOWNLOADING
diskInfo.setUrl(decodedUrl(diskInfo.getUrl()));
final String ovfId = diskInfo.getUrl();
if (templateService.getTemplateStatusIncludeProgress(ovfId, erId).getStatus() == TemplateStatusEnumType.ERROR)
{
templateService.delete(erId, ovfId);
}
InPart diskFilePart = mp.next();
InputStream isDiskFile = diskFilePart.getBody(InputStream.class, null);
File diskFile = new File("/tmp/" + diskInfo.getDiskFilePath());
copy(isDiskFile, diskFile);
/**
* TODO check OVFid is in this hostname ..
*/
diskInfo.setDiskFileSize(diskFile.length());
diskInfo.setEnterpriseRepositoryId(Integer.valueOf(erId));
templateService.upload(diskInfo, diskFile, errorMsg);
return Response.created(URI.create(diskInfo.getUrl())).build();
}
/**
* Check each part of the url is properly encoded (uploading a template name with blanks)
*/
private String decodedUrl(final String url) throws UnsupportedEncodingException
{
String[] parts = url.replaceFirst("http://", "").split("/");
StringBuffer sb = new StringBuffer();
sb.append("http:/");
for (String part : parts)
{
sb.append("/").append(java.net.URLEncoder.encode(part, "UTF-8"));
}
return sb.toString();
}
private TemplateDto readTemplateDtoFromMultipart(final InPart diskInfoPart,
final HttpHeaders headers, final Providers providers) throws Exception
{
fixMediaType(diskInfoPart);
String json = diskInfoPart.getBody(String.class, null);
// we replace the \ with / because a fail parsing strings with \ followed by a char that
// might resemble a control char. (C:\f... ends up as C:[ctrl-L]...)
String json2 = removeFakePath(removeControlChar(json));
json2 = temporalJsonNameHack(json2);
return providers.getMessageBodyReader(TemplateDto.class, null, null,
MediaType.APPLICATION_JSON_TYPE).readFrom(TemplateDto.class, null, null,
MediaType.APPLICATION_JSON_TYPE, headers.getRequestHeaders(),
new ByteArrayInputStream(json2.getBytes()));
// return diskInfoPart.getBody(OVFPackageInstanceDto.class, null);
// return providers.getMessageBodyReader(OVFPackageInstanceDto.class, null, null,
// MediaType.APPLICATION_JSON_TYPE).readFrom(OVFPackageInstanceDto.class, null, null,
// MediaType.APPLICATION_JSON_TYPE, headers.getRequestHeaders(),
// diskInfoPart.getInputStream());
}
/**
* Duet the flex client now sends 'OVFPackageInstanceDto' instead of 'ovfInstance', this will be
* removed before the 2.0 release
*/
@Deprecated
private String temporalJsonNameHack(final String jsonin)
{
return jsonin.replaceAll("ovfPackageInstanceDto", "template").replaceAll("ovfUrl", "url");
}
/**
* This Function is needed as long as the HTML 5 states:
* http://people.w3.org/mike/diffs/html5/spec/Overview.diff.html#common-input-element-apis
* browsers prepend C:\fakepath\
*
* @param in string.
* @return with no '\' characters.
*/
private String removeFakePath(final String in)
{
// TODO this is a hack as the server adds the fake path somehow
return in.replace("C:\\fakepath\\", "").replace("\\", "/");
}
/**
* The parse fails if any.
*
* @param in a String that might contain control caracters.
* @return same String that does not contains any control caracters.
*/
private String removeControlChar(final String in)
{
StringBuilder sb = new StringBuilder();
for (char c : in.toCharArray())
{
if (!Character.isISOControl(c))
{
sb.append(c);
}
}
return sb.toString();
}
// CaseInsensitiveMultivaluedMap [map=[Content-Disposition=form-data; name="diskInfo";
// filename="diskInfo.json",Content-Type=application/json]]
private void fixMediaType(final InPart diskInfoPart)
{
if (diskInfoPart.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE) == null)
{
diskInfoPart.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
}
}
private void copy(final InputStream fin, final File destFile) throws IOException
{
OutputStream fout = new FileOutputStream(destFile);
try
{
IOUtils.copy(fin, fout);
}
finally
{
fout.close();
}
}
}