/*
* Copyright 2010 NCHOVY
*
* 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 org.krakenapps.dom.api.impl;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.felix.ipojo.annotations.Component;
import org.apache.felix.ipojo.annotations.Invalidate;
import org.apache.felix.ipojo.annotations.Provides;
import org.apache.felix.ipojo.annotations.Requires;
import org.apache.felix.ipojo.annotations.Validate;
import org.krakenapps.confdb.Config;
import org.krakenapps.confdb.ConfigTransaction;
import org.krakenapps.confdb.Predicate;
import org.krakenapps.confdb.Predicates;
import org.krakenapps.dom.api.AdminApi;
import org.krakenapps.dom.api.ConfigManager;
import org.krakenapps.dom.api.DOMException;
import org.krakenapps.dom.api.DefaultEntityEventListener;
import org.krakenapps.dom.api.DefaultEntityEventProvider;
import org.krakenapps.dom.api.EntityEventListener;
import org.krakenapps.dom.api.EntityEventProvider;
import org.krakenapps.dom.api.EntityState;
import org.krakenapps.dom.api.FileUploadApi;
import org.krakenapps.dom.api.OrganizationApi;
import org.krakenapps.dom.api.Transaction;
import org.krakenapps.dom.api.UploadCallback;
import org.krakenapps.dom.api.UploadToken;
import org.krakenapps.dom.api.UserApi;
import org.krakenapps.dom.model.Admin;
import org.krakenapps.dom.model.FileSpace;
import org.krakenapps.dom.model.UploadedFile;
import org.krakenapps.dom.model.User;
import org.krakenapps.msgbus.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component(name = "dom-file-upload-api")
@Provides
public class FileUploadApiImpl extends DefaultEntityEventProvider<FileSpace> implements FileUploadApi {
private static final String FSP_BASE_DIR_KEY = "upload-base-dir";
private static final Class<FileSpace> fsp = FileSpace.class;
private static final String FSP_NOT_FOUND = "file-space-not-found";
private static final String FSP_ALREADY_EXIST = "file-space-already-exist";
private static final Class<UploadedFile> file = UploadedFile.class;
private static final String FILE_NOT_FOUND = "uploaded-file-not-found";
private static final String FILE_ALREADY_EXIST = "uploaded-file-already-exist";
private DefaultEntityEventProvider<UploadedFile> fileEventProvider = new DefaultEntityEventProvider<UploadedFile>();
private Logger logger = LoggerFactory.getLogger(FileUploadApiImpl.class);
private ConcurrentMap<String, UploadItem> uploadTokens = new ConcurrentHashMap<String, FileUploadApiImpl.UploadItem>();
private ConcurrentMap<String, String> sessionDownloadTokens = new ConcurrentHashMap<String, String>(); // session-token
private ConcurrentMap<String, DownloadToken> downloadTokens = new ConcurrentHashMap<String, DownloadToken>(); // session-token
private EntityEventListener<User> userEventListener = new DefaultEntityEventListener<User>() {
@Override
public void entitiesRemoved(String domain, Collection<EntityState> objs) {
LoginNameFilter filter = new LoginNameFilter();
for (EntityState o : objs) {
User user = (User) o.entity;
filter.add(user.getLoginName());
}
cfg.removes(domain, UploadedFile.class, Arrays.asList((Predicate) filter), null, null);
fileSpaceRemoveOrUpdate(domain, filter);
}
@Override
public void entityRemoved(String domain, User obj, Object state) {
Predicate pred = Predicates.field("owner/loginName", obj.getLoginName());
cfg.removes(domain, UploadedFile.class, Arrays.asList(pred), null, null);
fileSpaceRemoveOrUpdate(domain, pred);
}
private void fileSpaceRemoveOrUpdate(String domain, Predicate pred) {
Collection<FileSpace> fileSpaces = cfg.all(domain, FileSpace.class, pred);
if (!fileSpaces.isEmpty()) {
User master = null;
for (Admin admin : adminApi.getAdmins(domain)) {
if (admin.getRole().getName().equals("master")) {
master = admin.getUser();
break;
}
}
for (FileSpace fileSpace : fileSpaces) {
if (fileSpace.getFiles().isEmpty()) {
cfg.remove(domain, FileSpace.class, Predicates.field("guid", fileSpace.getGuid()), null, null);
} else {
fileSpace.setOwner(master);
cfg.update(domain, FileSpace.class, Predicates.field("guid", fileSpace.getGuid()), fileSpace, null, null);
}
}
}
}
};
private static class LoginNameFilter implements Predicate {
private HashSet<String> loginNames = new HashSet<String>();
public void add(String loginName) {
loginNames.add(loginName);
}
@SuppressWarnings("unchecked")
@Override
public boolean eval(Config c) {
Map<String, Object> m = (Map<String, Object>) c.getDocument();
if (m == null)
return false;
Map<String, Object> owner = (Map<String, Object>) m.get("owner");
if (owner == null)
return false;
String loginName = (String) owner.get("login_name");
if (loginName == null)
return false;
return loginNames.contains(loginName);
}
}
private EntityEventListener<FileSpace> spaceEventListener = new DefaultEntityEventListener<FileSpace>() {
@Override
public void entityRemoving(String domain, FileSpace obj, ConfigTransaction xact, Object state) {
Transaction x = Transaction.getInstance(xact);
cfg.removes(x, domain, file, getPreds(obj.getFiles()), null, fileEventProvider, state, null);
}
};
private EntityEventListener<UploadedFile> fileEventListener = new DefaultEntityEventListener<UploadedFile>() {
@Override
public void entityRemoved(String domain, UploadedFile obj, Object state) {
File file = new File(obj.getPath());
if (!file.delete())
logger.error("kraken dom: file delete failed [{}]", file.getAbsolutePath());
}
};
@Requires
private ConfigManager cfg;
@Requires
private OrganizationApi orgApi;
@Requires
private UserApi userApi;
@Requires
private AdminApi adminApi;
@Validate
public void validate() {
userApi.addEntityEventListener(userEventListener);
this.addEntityEventListener(spaceEventListener);
fileEventProvider.addEntityEventListener(fileEventListener);
}
@Invalidate
public void invalidate() {
if (userApi != null)
userApi.removeEntityEventListener(userEventListener);
this.removeEntityEventListener(spaceEventListener);
fileEventProvider.removeEntityEventListener(fileEventListener);
}
private Predicate getPred(String guid) {
return Predicates.field("guid", guid);
}
private List<Predicate> getPreds(List<? extends Object> objs) {
if (objs == null)
return new ArrayList<Predicate>();
List<Predicate> preds = new ArrayList<Predicate>(objs.size());
for (Object obj : objs) {
if (obj instanceof FileSpace)
preds.add(getPred(((FileSpace) obj).getGuid()));
else if (obj instanceof UploadedFile)
preds.add(getPred(((UploadedFile) obj).getGuid()));
}
return preds;
}
@Override
public File getBaseDirectory(String domain) {
String dir = orgApi.getOrganizationParameter(domain, FSP_BASE_DIR_KEY, String.class);
if (dir == null)
dir = new File(System.getProperty("kraken.data.dir"), "kraken-dom/upload/" + domain).getAbsolutePath();
File f = new File(dir);
f.mkdirs();
return f;
}
@Override
public void setBaseDirectory(String domain, File dir) {
orgApi.setOrganizationParameter(domain, FSP_BASE_DIR_KEY, dir.getAbsoluteFile());
}
@Override
public Collection<FileSpace> getFileSpaces(String domain) {
return cfg.all(domain, fsp);
}
@Override
public FileSpace findFileSpace(String domain, String guid) {
FileSpace fileSpace = cfg.find(domain, fsp, getPred(guid));
if (fileSpace != null)
fileSpace.setFiles((List<UploadedFile>) cfg.all(domain, UploadedFile.class, Predicates.field("space/guid", guid)));
return fileSpace;
}
@Override
public FileSpace getFileSpace(String domain, String guid) {
FileSpace fileSpace = cfg.get(domain, fsp, getPred(guid), FSP_NOT_FOUND);
fileSpace.setFiles((List<UploadedFile>) cfg.all(domain, UploadedFile.class, Predicates.field("space/guid", guid)));
return fileSpace;
}
@Override
public void createFileSpaces(String domain, Collection<FileSpace> spaces) {
List<FileSpace> spaceList = new ArrayList<FileSpace>(spaces);
cfg.adds(domain, fsp, getPreds(spaceList), spaceList, FSP_ALREADY_EXIST, this);
}
@Override
public void createFileSpace(String domain, FileSpace space) {
cfg.add(domain, fsp, getPred(space.getGuid()), space, FSP_ALREADY_EXIST, this);
}
@Override
public void updateFileSpaces(String domain, String loginName, Collection<FileSpace> spaces) {
List<FileSpace> spaceList = new ArrayList<FileSpace>(spaces);
for (FileSpace space : spaceList)
space.setUpdated(new Date());
cfg.updates(domain, fsp, getPreds(spaceList), spaceList, FSP_NOT_FOUND, this);
}
@Override
public void updateFileSpace(String domain, String loginName, FileSpace space) {
checkPermissionLevel(domain, loginName, space.getGuid(), "update-file-space-permission-denied");
space.setUpdated(new Date());
cfg.update(domain, fsp, getPred(space.getGuid()), space, FSP_NOT_FOUND, this);
}
@Override
public void removeFileSpaces(String domain, String loginName, Collection<String> guids) {
List<Predicate> preds = new ArrayList<Predicate>();
for (String guid : guids)
preds.add(getPred(guid));
cfg.removes(domain, fsp, preds, FSP_NOT_FOUND, this);
}
@Override
public void removeFileSpace(String domain, String loginName, String guid) {
checkPermissionLevel(domain, loginName, guid, "remove-file-space-permission-denied");
cfg.remove(domain, fsp, getPred(guid), FSP_NOT_FOUND, this);
}
private void checkPermissionLevel(String domain, String loginName, String guid, String exceptionMessage) {
FileSpace space = getFileSpace(domain, guid);
if (!space.getOwner().getLoginName().equals(loginName))
throw new DOMException(exceptionMessage);
}
@Override
public String setUploadToken(UploadToken token, UploadCallback callback) {
userApi.getUser(token.getOrgDomain(), token.getLoginName());
UploadItem item = new UploadItem(token, callback);
uploadTokens.putIfAbsent(item.guid, item);
return item.guid;
}
@Override
public void writeFile(String token, InputStream is) throws IOException {
UploadItem item = uploadTokens.remove(token);
if (item == null)
throw new DOMException("upload-token-not-found");
String orgDomain = item.token.getOrgDomain();
File temp = File.createTempFile("tmp-", null, getBaseDirectory(orgDomain));
if (temp.exists())
temp.delete();
OutputStream os = null;
long totalSize = 0;
try {
os = new FileOutputStream(temp);
byte[] buf = new byte[8096];
while (true) {
int l = is.read(buf);
if (l < 0)
break;
totalSize += l;
os.write(buf, 0, l);
}
} catch (IOException e) {
throw new DOMException("upload-failed");
} finally {
try {
if (os != null)
os.close();
} catch (IOException e) {
}
}
String guid = null;
if (totalSize == item.token.getFileSize()) {
UploadedFile uploaded = new UploadedFile();
uploaded.setGuid(item.token.getFileGuid());
File newFile = null;
guid = uploaded.getGuid();
if (item.token.getSpaceGuid() != null) {
FileSpace space = getFileSpace(orgDomain, item.token.getSpaceGuid());
uploaded.setSpace(space);
File spaceDir = new File(getBaseDirectory(orgDomain), space.getGuid());
spaceDir.mkdirs();
newFile = new File(spaceDir, guid);
} else
newFile = new File(getBaseDirectory(orgDomain), guid);
if (newFile.exists())
newFile.delete();
logger.trace("kraken dom: rename from [{}] to [{}]", temp.getAbsolutePath(), newFile.getAbsolutePath());
if (!temp.renameTo(newFile))
throw new DOMException("rename-uploaded-file-failed");
uploaded.setOwner(userApi.findUser(orgDomain, item.token.getLoginName()));
uploaded.setFileName(item.token.getFileName());
uploaded.setFileSize(item.token.getFileSize());
uploaded.setPath(newFile.getAbsolutePath());
cfg.add(orgDomain, UploadedFile.class, getPred(guid), uploaded, FILE_ALREADY_EXIST, fileEventProvider);
} else {
temp.delete();
}
if (item.callback != null)
item.callback.onUploadFile(item.token, guid);
}
private static class UploadItem {
public UploadToken token;
public String guid = UUID.randomUUID().toString();
public UploadCallback callback;
public UploadItem(UploadToken token, UploadCallback callback) {
this.token = token;
this.callback = callback;
}
}
@Override
public String setDownloadToken(Session session) {
userApi.getUser(session.getOrgDomain(), session.getAdminLoginName());
DownloadToken token = new DownloadToken(session);
String old = sessionDownloadTokens.putIfAbsent(session.getGuid(), token.guid);
if (old != null)
return old;
downloadTokens.put(token.guid, token);
return token.guid;
}
@Override
public UploadedFile getFileMetadataWithToken(String tokenGuid, String fileGuid) {
DownloadToken token = downloadTokens.get(tokenGuid);
if (token == null)
throw new DOMException("download-token-not-found");
return getFileMetadata(token.session.getOrgDomain(), fileGuid);
}
@Override
public UploadedFile getFileMetadata(String domain, String fileGuid) {
return cfg.get(domain, file, getPred(fileGuid), FILE_NOT_FOUND);
}
@Override
public void removeDownloadToken(Session session) {
String token = sessionDownloadTokens.remove(session.getGuid());
if (token != null)
downloadTokens.remove(token);
}
private class DownloadToken {
private String guid = UUID.randomUUID().toString();
private Session session;
public DownloadToken(Session session) {
this.session = session;
}
}
@Override
public void deleteFiles(String domain, Collection<String> guids) {
List<UploadedFile> uploadeds = new ArrayList<UploadedFile>();
List<Predicate> preds = new ArrayList<Predicate>();
for (String guid : guids) {
uploadeds.add(cfg.get(domain, UploadedFile.class, getPred(guid), FILE_NOT_FOUND));
preds.add(getPred(guid));
}
cfg.removes(domain, file, preds, FILE_NOT_FOUND, fileEventProvider);
}
@Override
public void deleteFile(String domain, String guid) {
deleteFiles(domain, Arrays.asList(guid));
}
@Override
public void deleteFiles(String domain, String loginName, Collection<String> guids) {
List<UploadedFile> uploadeds = new ArrayList<UploadedFile>();
List<Predicate> preds = new ArrayList<Predicate>();
for (String guid : guids) {
UploadedFile uploaded = cfg.get(domain, UploadedFile.class, getPred(guid), FILE_NOT_FOUND);
if (!loginName.equals(uploaded.getOwner().getLoginName())) {
Map<String, Object> params = new HashMap<String, Object>();
params.put("guid", uploaded.getGuid());
throw new DOMException("file-delete-permission-denied", params);
}
uploadeds.add(uploaded);
preds.add(getPred(guid));
}
cfg.removes(domain, file, preds, FILE_NOT_FOUND, fileEventProvider);
}
@Override
public void deleteFile(String domain, String loginName, String guid) {
deleteFiles(domain, loginName, Arrays.asList(guid));
}
@Override
public EntityEventProvider<UploadedFile> getUploadedFileEventProvider() {
return fileEventProvider;
}
}