package io.fathom.cloud.image;
import io.fathom.cloud.Clock;
import io.fathom.cloud.CloudException;
import io.fathom.cloud.blobs.BlobData;
import io.fathom.cloud.image.api.os.resources.ImageFilter;
import io.fathom.cloud.protobuf.CloudCommons.Attributes;
import io.fathom.cloud.protobuf.CloudCommons.KeyValueData;
import io.fathom.cloud.protobuf.ImageModel.ImageData;
import io.fathom.cloud.protobuf.ImageModel.ImageLocation;
import io.fathom.cloud.protobuf.ImageModel.ImageState;
import io.fathom.cloud.server.model.Project;
import io.fathom.cloud.server.resources.Urls;
import io.fathom.cloud.services.ImageKey;
import io.fathom.cloud.services.ImageService;
import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response.Status;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.BaseEncoding;
import com.google.inject.persist.Transactional;
import com.google.protobuf.Descriptors.FieldDescriptor;
@Singleton
@Transactional
public class ImageServiceImpl implements ImageService {
private static final Logger log = LoggerFactory.getLogger(ImageServiceImpl.class);
private static final Comparator<Image> ORDER_BY_NAME = new Comparator<Image>() {
@Override
public int compare(Image o1, Image o2) {
String s1 = o1.getName();
String s2 = o2.getName();
if (s1 == null) {
return s2 == null ? 0 : -1;
}
if (s2 == null) {
return 1;
}
return s1.compareTo(s2);
//
//
// long v1 = o1.getId();
// long v2 = o2.getId();
// return Long.compare(v1, v2);
}
};
@Inject
ImageRepository imageRepository;
@Inject
ImageDataService imageDataService;
public List<Image> listImages(Project project, ImageFilter filter) throws CloudException {
List<Image> matching = Lists.newArrayList();
for (ImageData image : imageRepository.getImages().list()) {
if (!isVisible(project, image)) {
continue;
}
if (!filter.matches(image)) {
continue;
}
matching.add(new ImageImpl(image));
}
Collections.sort(matching, ORDER_BY_NAME);
return matching;
}
@Override
public List<Image> listImages(Project project) throws CloudException {
ImageFilter filter = new ImageFilter();
return listImages(project, filter);
}
@Override
public ImageImpl findImage(Project project, long imageId) throws CloudException {
ImageData image = imageRepository.getImages().find(imageId);
if (image != null) {
if (!isVisible(project, image)) {
image = null;
}
}
if (image == null) {
return null;
}
return new ImageImpl(image);
}
private boolean isVisible(Project project, ImageData image) {
if (image.hasImageState()) {
switch (image.getImageState()) {
case DELETED:
return false;
default:
// Not blocked
break;
}
}
if (image.getIsPublic()) {
return true;
}
if (image.getOwnerProject() == project.getId()) {
return true;
}
return false;
}
public ImageData deleteImage(Project project, long id) throws CloudException {
ImageImpl i = findImage(project, id);
if (i == null) {
// We check because it might not be visible!
return null;
}
ImageData.Builder b = ImageData.newBuilder(i.getData());
b.setImageState(ImageState.DELETED);
b.setDeletedAt(Clock.getTimestamp());
// TODO: Clean up old deleted images?
// TODO: Remove from storage?
return imageRepository.getImages().update(b);
}
public ImageData changeTags(Project project, int id, List<String> addTags, List<String> removeTags)
throws CloudException {
ImageImpl i = findImage(project, id);
if (i == null) {
throw new WebApplicationException(Status.NOT_FOUND);
}
ImageData.Builder b = ImageData.newBuilder(i.getData());
List<String> tags = Lists.newArrayList(b.getTagList());
for (String addTag : addTags) {
if (tags.contains(addTag)) {
continue;
}
tags.add(addTag);
}
tags.removeAll(removeTags);
b.clearTag();
b.addAllTag(tags);
return imageRepository.getImages().update(b);
}
public ImageData updateImage(Project project, long id, Map<String, String> metadata) throws CloudException {
ImageImpl i = findImage(project, id);
if (i == null) {
throw new WebApplicationException(Status.NOT_FOUND);
}
ImageData changes;
{
ImageData.Builder b = ImageData.newBuilder();
mapHeadersToBuilder(metadata, b);
changes = b.buildPartial();
}
Set<String> allowed = Sets.newHashSet("is_public", "name", "attributes", "is_protected");
for (Entry<FieldDescriptor, Object> entry : changes.getAllFields().entrySet()) {
FieldDescriptor field = entry.getKey();
String key = field.getName();
if (!allowed.contains(key)) {
Object existing = i.getData().getField(field);
if (!Objects.equal(existing, entry.getValue())) {
log.warn("Attempt to update blocked field: " + key);
throw new IllegalArgumentException();
}
}
}
ImageData.Builder b = ImageData.newBuilder(i.getData());
b.mergeFrom(changes);
removeRepeatedAttributes(b.getAttributesBuilder());
return imageRepository.getImages().update(b);
}
private void removeRepeatedAttributes(Attributes.Builder attributes) {
Map<String, KeyValueData> map = Maps.newLinkedHashMap();
for (KeyValueData kv : attributes.getUserAttributesList()) {
if (!kv.hasKey()) {
continue;
}
map.put(kv.getKey(), kv);
}
if (map.size() != attributes.getUserAttributesCount()) {
attributes.clearUserAttributes();
for (KeyValueData kv : map.values()) {
attributes.addUserAttributes(kv);
}
}
}
public BlobData findImageData(Project project, ImageData image) throws IOException {
ImageLocation imageLocation = image.getLocation();
if (imageLocation.hasStored()) {
String cookie = imageLocation.getStored();
BlobData blob = imageDataService.getImageFile(cookie);
return blob;
}
return null;
}
// public void changeMembers(long id, List<String> addMembers,
// List<String> removeMembers) {
// ImageData i = findImage(id);
// if (i == null) {
// throw new WebApplicationException(Status.NOT_FOUND);
// }
//
// ImageData.Builder b = ImageData.newBuilder(i);
//
// List<String> members = Lists.newArrayList(b.getMembers());
//
// for (String removeMember : removeMembers) {
// if (members.contains(removeMember)) {
// continue;
// }
// members.add(removeMember);
// }
//
// members.removeAll(removeMembers);
//
// b.clearMember();
// b.addallMembers(members);
//
// return imageRepository.getImages().update(b);
// }
@Override
public BlobData getImageBlob(Image i) throws IOException {
ImageImpl image = (ImageImpl) i;
ImageData data = image.getData();
Project project = new Project(data.getOwnerProject());
BlobData imageData = findImageData(project, data);
return imageData;
}
@Override
public ImageImpl createImage(long projectId, Map<String, String> metadata) throws CloudException {
ImageData.Builder b = ImageData.newBuilder();
b.setIsPublic(false);
if (metadata != null) {
mapHeadersToBuilder(metadata, b);
}
long t = Clock.getTimestamp();
b.setCreatedAt(t);
b.setUpdatedAt(t);
b.setImageState(ImageState.QUEUED);
if (projectId != 0) {
b.setOwnerProject(projectId);
}
ImageData created = imageRepository.getImages().create(b);
return new ImageImpl(created);
}
@Override
public ImageImpl uploadData(Image i, BlobData src) throws IOException, CloudException {
ImageImpl image = (ImageImpl) i;
if (image == null || src == null) {
throw new IllegalArgumentException();
}
long size = src.size();
ImageData existing = imageRepository.getImages().find(image.data.getId());
if (existing == null) {
throw new WebApplicationException(Status.NOT_FOUND);
}
ImageData.Builder b = ImageData.newBuilder(existing);
if (b.hasImageSize()) {
if (b.getImageSize() != size) {
throw new IllegalArgumentException();
}
}
b.setUpdatedAt(Clock.getTimestamp());
b.setImageSize(size);
b.setImageState(ImageState.ACTIVE);
{
String stored = imageDataService.storeImageFile(src);
ImageLocation.Builder loc = b.getLocationBuilder();
loc.setStored(stored);
b.setImageChecksum(src.getHash());
}
ImageData updated = imageRepository.getImages().update(b);
return new ImageImpl(updated);
}
public static void mapHeadersToBuilder(Map<String, String> properties, ImageData.Builder b) {
for (Entry<String, String> property : properties.entrySet()) {
String key = property.getKey();
String value = property.getValue();
if (key.startsWith("property-")) {
key = key.substring(9);
KeyValueData.Builder kv = b.getAttributesBuilder().addUserAttributesBuilder();
kv.setKey(key);
kv.setValue(value);
} else if (key.equals(ImageService.METADATA_KEY_NAME)) {
b.setName(value);
} else if (key.equals(ImageService.METADATA_KEY_CONTAINER_FORMAT)) {
b.setContainerFormat(value);
} else if (key.equals(ImageService.METADATA_KEY_DISK_FORMAT)) {
b.setDiskFormat(value);
} else if (key.equals(ImageService.METADATA_KEY_SIZE)) {
b.setImageSize(Long.valueOf(value));
} else if (key.equals("is_public")) {
boolean isPublic = Boolean.parseBoolean(value);
b.setIsPublic(isPublic);
} else if (key.equals("protected")) {
boolean isProtected = Boolean.parseBoolean(value);
b.setIsProtected(isProtected);
} else {
log.warn("Ignoring unknown property: " + key);
}
}
}
public static class ImageImpl implements ImageService.Image {
final ImageData data;
public ImageImpl(ImageData data) {
this.data = data;
}
@Override
public long getId() {
return data.getId();
}
@Override
public ImageKey getUniqueKey() {
return new ImageKey(BaseEncoding.base16().encode(data.getImageChecksum().toByteArray()));
}
public ImageData getData() {
return data;
}
@Override
public String getName() {
return data.getName();
}
@Override
public String getStatus() {
return data.getImageState().name();
}
@Override
public String getChecksum() {
return BaseEncoding.base16().encode(data.getImageChecksum().toByteArray());
}
}
@Override
public String getUrl(HttpServletRequest httpRequest, long imageId) {
String url = Urls.getRequestUrl(httpRequest);
if (!url.endsWith("/")) {
url += "/";
}
url += "openstack/images/v1/images/" + imageId;
return url;
}
}