/* documentr - Edit, maintain, and present software documentation on the web. Copyright (C) 2012-2013 Maik Schreiber 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 3 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 de.blizzy.documentr.subscription; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.util.Iterator; import java.util.List; import java.util.Set; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.PersonIdent; import org.gitective.core.BlobUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.google.common.base.Charsets; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.eventbus.Subscribe; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import de.blizzy.documentr.access.User; import de.blizzy.documentr.access.UserStore; import de.blizzy.documentr.repository.IGlobalRepositoryManager; import de.blizzy.documentr.repository.ILockedRepository; import de.blizzy.documentr.repository.ProjectBranchDeletedEvent; import de.blizzy.documentr.repository.ProjectBranchRenamedEvent; import de.blizzy.documentr.repository.ProjectDeletedEvent; import de.blizzy.documentr.repository.ProjectRenamedEvent; import de.blizzy.documentr.repository.RepositoryNotFoundException; import de.blizzy.documentr.repository.RepositoryUtil; import de.blizzy.documentr.util.Util; @Component @Slf4j public class SubscriptionStore { private static final String REPOSITORY_NAME = "_subscriptions"; //$NON-NLS-1$ private static final String SUBSCRIPTIONS_SUFFIX = ".subscriptions"; //$NON-NLS-1$ @Autowired private IGlobalRepositoryManager globalRepositoryManager; @Autowired private UserStore userStore; public void subscribe(String projectName, String branchName, String path, User user) throws IOException { ILockedRepository repo = null; try { repo = getOrCreateRepository(user); Set<Page> pages = getSubscriptions(user, repo); Page page = new Page(projectName, branchName, path); if (pages.add(page)) { saveSubscriptions(user, pages, repo, true); } } catch (GitAPIException e) { throw new IOException(e); } finally { Util.closeQuietly(repo); } } public void unsubscribe(String projectName, String branchName, String path, User user) throws IOException { ILockedRepository repo = null; try { repo = getOrCreateRepository(user); Set<Page> pages = getSubscriptions(user, repo); Page page = new Page(projectName, branchName, path); if (pages.remove(page)) { saveSubscriptions(user, pages, repo, true); } } catch (GitAPIException e) { throw new IOException(e); } finally { Util.closeQuietly(repo); } } private Set<Page> getSubscriptions(User user, ILockedRepository repo) { String json = BlobUtils.getHeadContent(repo.r(), user.getLoginName() + SUBSCRIPTIONS_SUFFIX); Set<Page> pages = Sets.newHashSet(); if (StringUtils.isNotBlank(json)) { Gson gson = new GsonBuilder().enableComplexMapKeySerialization().create(); List<Page> pagesList = gson.fromJson(json, new TypeToken<List<Page>>() {}.getType()); pages.addAll(pagesList); } return pages; } private void saveSubscriptions(User user, Set<Page> pages, ILockedRepository repo, boolean commit) throws IOException, GitAPIException { Git git = Git.wrap(repo.r()); if (!pages.isEmpty()) { Gson gson = new GsonBuilder().enableComplexMapKeySerialization().create(); String json = gson.toJson(pages); File workingDir = RepositoryUtil.getWorkingDir(repo.r()); File file = new File(workingDir, user.getLoginName() + SUBSCRIPTIONS_SUFFIX); FileUtils.writeStringToFile(file, json, Charsets.UTF_8); git.add() .addFilepattern(user.getLoginName() + SUBSCRIPTIONS_SUFFIX) .call(); } else { git.rm() .addFilepattern(user.getLoginName() + SUBSCRIPTIONS_SUFFIX) .call(); } if (commit) { PersonIdent ident = new PersonIdent(user.getLoginName(), user.getEmail()); git.commit() .setAuthor(ident) .setCommitter(ident) .setMessage(user.getLoginName() + SUBSCRIPTIONS_SUFFIX) .call(); } } public boolean isSubscribed(String projectName, String branchName, String path, User user) throws IOException { if (!user.isDisabled()) { ILockedRepository repo = null; try { repo = getOrCreateRepository(user); return isSubscribed(projectName, branchName, path, user, repo); } catch (GitAPIException e) { throw new IOException(e); } finally { Util.closeQuietly(repo); } } else { return false; } } private boolean isSubscribed(String projectName, String branchName, String path, User user, ILockedRepository repo) { if (!user.isDisabled()) { Set<Page> pages = getSubscriptions(user, repo); Page page = new Page(projectName, branchName, path); return pages.contains(page); } else { return false; } } public Set<String> getSubscriberEmails(String projectName, String branchName, String path) throws IOException { ILockedRepository repo = null; try { repo = globalRepositoryManager.getProjectCentralRepository(REPOSITORY_NAME, false); File workingDir = RepositoryUtil.getWorkingDir(repo.r()); FileFilter fileFilter = new FileFilter() { @Override public boolean accept(File file) { return file.isFile() && file.getName().endsWith(SUBSCRIPTIONS_SUFFIX); } }; List<File> files = Lists.newArrayList(workingDir.listFiles(fileFilter)); Function<File, String> loginNamesFunction = new Function<File, String>() { @Override public String apply(File file) { return StringUtils.removeEnd(file.getName(), SUBSCRIPTIONS_SUFFIX); } }; List<String> loginNames = Lists.transform(files, loginNamesFunction); Set<String> emails = Sets.newHashSet(); for (Iterator<String> iter = loginNames.iterator(); iter.hasNext();) { String loginName = iter.next(); User user = userStore.getUser(loginName); if (isSubscribed(projectName, branchName, path, user, repo)) { emails.add(user.getEmail()); } } log.debug("emails subscribed to {}/{}/{}: {}", projectName, branchName, Util.toUrlPagePath(path), emails); //$NON-NLS-1$ return emails; } finally { Util.closeQuietly(repo); } } private ILockedRepository getOrCreateRepository(User user) throws IOException, GitAPIException { try { return globalRepositoryManager.getProjectCentralRepository(REPOSITORY_NAME, false); } catch (RepositoryNotFoundException e) { return globalRepositoryManager.createProjectCentralRepository(REPOSITORY_NAME, false, user); } } @Subscribe public void renameProject(ProjectRenamedEvent event) { final String projectName = event.getProjectName(); final String newProjectName = event.getNewProjectName(); Function<Page, Page> function = new Function<Page, Page>() { @Override public Page apply(Page page) { return page.getProjectName().equals(projectName) ? new Page(newProjectName, page.getBranchName(), page.getPath()) : page; } }; try { transformAllSubscriptions(function, "rename project " + projectName + " to " + newProjectName, //$NON-NLS-1$ //$NON-NLS-2$ event.getCurrentUser()); } catch (IOException e) { log.error(StringUtils.EMPTY, e); } catch (GitAPIException e) { log.error(StringUtils.EMPTY, e); } } private void transformAllSubscriptions(Function<Page, Page> function, String commitMessage, User currentUser) throws IOException, GitAPIException { ILockedRepository repo = null; try { List<String> users = userStore.listUsers(); repo = getOrCreateRepository(currentUser); boolean anyChanged = false; for (String loginName : users) { User user = userStore.getUser(loginName); List<Page> pages = Lists.newArrayList(getSubscriptions(user, repo)); List<Page> newPages = Lists.newArrayList(Lists.transform(pages, function)); if (!newPages.equals(pages)) { saveSubscriptions(user, Sets.newHashSet(newPages), repo, false); anyChanged = true; } } if (anyChanged) { PersonIdent ident = new PersonIdent(currentUser.getLoginName(), currentUser.getEmail()); Git.wrap(repo.r()).commit() .setAuthor(ident) .setCommitter(ident) .setMessage(commitMessage) .call(); } } finally { Util.closeQuietly(repo); } } @Subscribe public void deleteProject(ProjectDeletedEvent event) { final String projectName = event.getProjectName(); Predicate<Page> predicate = new Predicate<Page>() { @Override public boolean apply(Page page) { return !page.getProjectName().equals(projectName); } }; try { deleteFromAllSubscriptions(predicate, "delete project " + projectName, event.getCurrentUser()); //$NON-NLS-1$ } catch (IOException e) { log.error(StringUtils.EMPTY, e); } catch (GitAPIException e) { log.error(StringUtils.EMPTY, e); } } private void deleteFromAllSubscriptions(Predicate<Page> predicate, String commitMessage, User currentUser) throws IOException, GitAPIException { ILockedRepository repo = null; try { List<String> users = userStore.listUsers(); repo = getOrCreateRepository(currentUser); boolean anyChanged = false; for (String loginName : users) { User user = userStore.getUser(loginName); Set<Page> pages = getSubscriptions(user, repo); Set<Page> newPages = Sets.newHashSet(Sets.filter(pages, predicate)); if (!newPages.equals(pages)) { saveSubscriptions(user, newPages, repo, false); anyChanged = true; } } if (anyChanged) { PersonIdent ident = new PersonIdent(currentUser.getLoginName(), currentUser.getEmail()); Git.wrap(repo.r()).commit() .setAuthor(ident) .setCommitter(ident) .setMessage(commitMessage) .call(); } } finally { Util.closeQuietly(repo); } } @Subscribe public void deleteProjectBranch(ProjectBranchDeletedEvent event) { final String projectName = event.getProjectName(); final String branchName = event.getBranchName(); Predicate<Page> predicate = new Predicate<Page>() { @Override public boolean apply(Page page) { return !page.getProjectName().equals(projectName) || !page.getBranchName().equals(branchName); } }; try { deleteFromAllSubscriptions(predicate, "delete branch " + projectName + "/" + branchName, //$NON-NLS-1$ //$NON-NLS-2$ event.getCurrentUser()); } catch (IOException e) { log.error(StringUtils.EMPTY, e); } catch (GitAPIException e) { log.error(StringUtils.EMPTY, e); } } @Subscribe public void renameProjectBranch(ProjectBranchRenamedEvent event) { final String projectName = event.getProjectName(); final String branchName = event.getBranchName(); final String newBranchName = event.getNewBranchName(); Function<Page, Page> function = new Function<Page, Page>() { @Override public Page apply(Page page) { return page.getProjectName().equals(projectName) && page.getBranchName().equals(branchName) ? new Page(projectName, newBranchName, page.getPath()) : page; } }; try { transformAllSubscriptions(function, "rename branch " + projectName + "/" + branchName + " to " + projectName + "/" + newBranchName, //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ event.getCurrentUser()); } catch (IOException e) { log.error(StringUtils.EMPTY, e); } catch (GitAPIException e) { log.error(StringUtils.EMPTY, e); } } }