From 386a7763b3ccb10c89d31e3fc2681e7069a283a8 Mon Sep 17 00:00:00 2001 From: jensp Date: Fri, 9 Mar 2018 19:01:21 +0000 Subject: [PATCH] CCM NG: Next part of DatabaseThemeProvider git-svn-id: https://svn.libreccm.org/ccm/ccm_ng@5342 8810af33-2d31-482b-a856-94f89814c4df --- .../org/libreccm/theming/ThemeProvider.java | 18 +- .../theming/db/DatabaseThemeProvider.java | 245 ++++++++++++++++ .../org/libreccm/theming/db/Directory.java | 1 - .../java/org/libreccm/theming/db/Theme.java | 8 +- .../org/libreccm/theming/db/ThemeFile.java | 24 +- .../libreccm/theming/db/ThemeFileManager.java | 273 +++++++++++++++++- .../theming/db/ThemeFileRepository.java | 39 ++- .../org/libreccm/theming/db/ThemeManager.java | 251 +++++++++++++++- .../libreccm/theming/db/ThemeRepository.java | 28 +- .../org/libreccm/theming/db/ThemeVersion.java | 30 -- .../theming/manifest/ThemeManifestUtil.java | 39 ++- 11 files changed, 888 insertions(+), 68 deletions(-) create mode 100644 ccm-core/src/main/java/org/libreccm/theming/db/DatabaseThemeProvider.java delete mode 100644 ccm-core/src/main/java/org/libreccm/theming/db/ThemeVersion.java diff --git a/ccm-core/src/main/java/org/libreccm/theming/ThemeProvider.java b/ccm-core/src/main/java/org/libreccm/theming/ThemeProvider.java index 60a0e492b..146e227d9 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/ThemeProvider.java +++ b/ccm-core/src/main/java/org/libreccm/theming/ThemeProvider.java @@ -92,14 +92,16 @@ public interface ThemeProvider extends Serializable { * should throw an NullPointerException if {@code null} is * provided as path. * - * @return A list of all files in the provided directory. If there is such - * path in the theme the list is empty. If the path is the path of a - * file and not a directory the list should have one element, the - * data about the file itself. - * - * @throws IllegalArgumentException If {@code theme} is an empty string, - * if there is no theme with the name provided by {@code theme} or - * if there is no file/directory with the provided path in the theme. + * @return A list of all files in the provided directory. If there is no + * such path in the theme the list is empty. If the path is the path + * of a file and not a directory the list should have one element, + * the data about the file itself. + * + * @throws IllegalArgumentException If {@code theme} is an empty string, if + * there is no theme with the name provided + * by {@code theme} or if there is no + * file/directory with the provided path in + * the theme. */ List listThemeFiles(String theme, ThemeVersion version, diff --git a/ccm-core/src/main/java/org/libreccm/theming/db/DatabaseThemeProvider.java b/ccm-core/src/main/java/org/libreccm/theming/db/DatabaseThemeProvider.java new file mode 100644 index 000000000..525f44aa5 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/theming/db/DatabaseThemeProvider.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2018 LibreCCM Foundation. + * + * This library 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; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.libreccm.theming.db; + +import org.libreccm.core.UnexpectedErrorException; +import org.libreccm.theming.ThemeConstants; +import org.libreccm.theming.ThemeFileInfo; +import org.libreccm.theming.ThemeInfo; +import org.libreccm.theming.ThemeProvider; +import org.libreccm.theming.ThemeVersion; +import org.libreccm.theming.manifest.ThemeManifest; +import org.libreccm.theming.manifest.ThemeManifestUtil; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.transaction.Transactional; + +/** + * An implementation of {@link ThemeProvider} which serves themes from the + * database. + * + * Supports all operations. + * + * @author Jens Pelzetter + */ +@RequestScoped +public class DatabaseThemeProvider implements ThemeProvider { + + private static final long serialVersionUID = -8661840420214119753L; + + @Inject + private ThemeFileManager fileManager; + + @Inject + private ThemeFileRepository fileRepository; + + @Inject + private ThemeManifestUtil manifestUtil; + + @Inject + private ThemeManager themeManager; + + @Inject + private ThemeRepository themeRepository; + + @Override + @Transactional(Transactional.TxType.REQUIRED) + public List getThemes() { + + return themeRepository + .findAll(ThemeVersion.DRAFT) + .stream() + .map(this::createThemeInfo) + .sorted() + .collect(Collectors.toList()); + } + + @Override + public List getLiveThemes() { + + return themeRepository + .findAll(ThemeVersion.LIVE) + .stream() + .map(this::createThemeInfo) + .sorted() + .collect(Collectors.toList()); + } + + @Override + public Optional getThemeInfo(final String themeName, + final ThemeVersion version) { + + return themeRepository + .findThemeByName(themeName, version) + .map(this::createThemeInfo); + } + + @Override + public boolean providesTheme(final String theme, + final ThemeVersion version) { + + return themeRepository + .findThemeByName(theme, version) + .isPresent(); + } + + @Override + public List listThemeFiles(final String themeName, + final ThemeVersion version, + final String path) { + + final Theme theme = themeRepository + .findThemeByName(path, version) + .orElseThrow(() -> new IllegalArgumentException(String + .format("No Theme \"%s\" in the database.", themeName))); + + final Optional themeFile = fileRepository + .findByPath(theme, path, version); + + final List result = new ArrayList<>(); + if (themeFile.isPresent()) { + if (themeFile.get() instanceof DataFile) { + + result.add(themeFile.map(this::createThemeFileInfo).get()); + + } else if (themeFile.get() instanceof Directory) { + + final Directory directory = (Directory) themeFile.get(); + result.addAll(directory + .getFiles() + .stream() + .map(this::createThemeFileInfo) + .collect(Collectors.toList())); + } else { + throw new IllegalArgumentException(String + .format("Unknown type \"%s\".", + themeFile.get().getClass().getName())); + } + } + + return result; + } + + @Override + public Optional getThemeFileAsStream(String theme, + ThemeVersion version, + String path) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public OutputStream getOutputStreamForThemeFile(String theme, String path) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public boolean supportsChanges() { + return true; + } + + @Override + public boolean supportsDraftThemes() { + return true; + } + + @Override + public void publishTheme(final String themeName) { + + themeRepository + .findThemeByName(themeName, ThemeVersion.DRAFT) + .ifPresent(themeManager::publishTheme); + } + + private ThemeInfo createThemeInfo(final Theme theme) { + + Objects.requireNonNull(theme); + + final Optional manifestFileJson = fileRepository + .findByNameAndParent(ThemeConstants.THEME_MANIFEST_JSON, + theme.getRootDirectory()); + final Optional manifestFileXml = fileRepository + .findByNameAndParent(ThemeConstants.THEME_MANIFEST_XML, + theme.getRootDirectory()); + + final DataFile manifestFile; + final String filename; + if (manifestFileJson.isPresent()) { + manifestFile = (DataFile) manifestFileJson.get(); + filename = ThemeConstants.THEME_MANIFEST_JSON; + } else if (manifestFileXml.isPresent()) { + manifestFile = (DataFile) manifestFileXml.get(); + filename = ThemeConstants.THEME_MANIFEST_XML; + } else { + throw new IllegalArgumentException(String + .format("No manifest file found for theme \"%s\".", + theme.getName())); + } + + try (final InputStream inputStream = new ByteArrayInputStream( + manifestFile.getData())) { + final ThemeManifest manifest = manifestUtil + .loadManifest(inputStream, filename); + final ThemeInfo themeInfo = new ThemeInfo(); + themeInfo.setManifest(manifest); + themeInfo.setProvider(getClass()); + themeInfo.setVersion(theme.getVersion()); + + return themeInfo; + } catch (IOException ex) { + throw new UnexpectedErrorException(ex); + } + } + + private ThemeFileInfo createThemeFileInfo(final ThemeFile file) { + + final ThemeFileInfo fileInfo = new ThemeFileInfo(); + + fileInfo.setName(file.getName()); + fileInfo.setWritable(true); + + if (file instanceof DataFile) { + + final DataFile dataFile = (DataFile) file; + + fileInfo.setDirectory(false); + fileInfo.setMimeType(dataFile.getType()); + fileInfo.setSize(dataFile.getSize()); + + } + + if (file instanceof Directory) { + fileInfo.setDirectory(true); + } + + return fileInfo; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/theming/db/Directory.java b/ccm-core/src/main/java/org/libreccm/theming/db/Directory.java index 9083a20b1..9855352cd 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/db/Directory.java +++ b/ccm-core/src/main/java/org/libreccm/theming/db/Directory.java @@ -23,7 +23,6 @@ import org.libreccm.core.CoreConstants; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Objects; import javax.persistence.Entity; import javax.persistence.OneToMany; diff --git a/ccm-core/src/main/java/org/libreccm/theming/db/Theme.java b/ccm-core/src/main/java/org/libreccm/theming/db/Theme.java index cfd787ca4..48929d5d3 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/db/Theme.java +++ b/ccm-core/src/main/java/org/libreccm/theming/db/Theme.java @@ -19,6 +19,7 @@ package org.libreccm.theming.db; import org.libreccm.core.CoreConstants; +import org.libreccm.theming.ThemeVersion; import java.io.Serializable; import java.util.Objects; @@ -43,6 +44,9 @@ import javax.persistence.Table; @Entity @Table(name = "THEMES", schema = CoreConstants.DB_SCHEMA) @NamedQueries({ + @NamedQuery(name = "Theme.findAllForVersion", + query = "SELECT t FROM Theme t WHERE t.version = :version") + , @NamedQuery(name = "Theme.findByUuid", query = "SELECT t FROM Theme t " + "WHERE t.uuid = :uuid " @@ -50,8 +54,8 @@ import javax.persistence.Table; , @NamedQuery(name = "Theme.findByName", query = "SELECT t FROM Theme t " - + "WHERE t.name = :name " - + "AND t.version = :version") + + "WHERE t.name = :name " + + "AND t.version = :version") }) public class Theme implements Serializable { diff --git a/ccm-core/src/main/java/org/libreccm/theming/db/ThemeFile.java b/ccm-core/src/main/java/org/libreccm/theming/db/ThemeFile.java index e15591fcb..f6f7f53bb 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/db/ThemeFile.java +++ b/ccm-core/src/main/java/org/libreccm/theming/db/ThemeFile.java @@ -19,6 +19,7 @@ package org.libreccm.theming.db; import org.libreccm.core.CoreConstants; +import org.libreccm.theming.ThemeVersion; import java.io.Serializable; import java.util.Objects; @@ -88,6 +89,10 @@ public class ThemeFile implements Serializable { @NotNull private String path; + @ManyToOne + @JoinColumn(name = "THEME_ID") + private Theme theme; + @ManyToOne @JoinColumn(name = "PARENT_DIRECTORY_ID") private Directory parent; @@ -132,6 +137,14 @@ public class ThemeFile implements Serializable { this.version = version; } + public Theme getTheme() { + return theme; + } + + protected void setTheme(final Theme theme) { + this.theme = theme; + } + public Directory getParent() { return parent; } @@ -147,6 +160,7 @@ public class ThemeFile implements Serializable { hash = 37 * hash + Objects.hashCode(name); hash = 37 * hash + Objects.hashCode(path); hash = 37 * hash + Objects.hashCode(uuid); + hash = 37 * hash + Objects.hashCode(theme); hash = 37 * hash + Objects.hashCode(parent); return hash; } @@ -178,6 +192,10 @@ public class ThemeFile implements Serializable { if (!Objects.equals(uuid, other.getUuid())) { return false; } + if (!Objects.equals(theme, other.getTheme())) { + return false; + } + return Objects.equals(parent, other.getParent()); } @@ -195,13 +213,17 @@ public class ThemeFile implements Serializable { + "fileId = %d, " + "name = \"%s\", " + "path = \"%s\", " - + "uuid = \"%s\"%s" + + "uuid = \"%s\", " + + "theme = \"%s\", " + + "parent = \"%s\"%s" + " }", super.toString(), fileId, name, path, uuid, + Objects.toString(theme), + Objects.toString(parent), data); } diff --git a/ccm-core/src/main/java/org/libreccm/theming/db/ThemeFileManager.java b/ccm-core/src/main/java/org/libreccm/theming/db/ThemeFileManager.java index c72e7dbc2..6cd2cd61c 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/db/ThemeFileManager.java +++ b/ccm-core/src/main/java/org/libreccm/theming/db/ThemeFileManager.java @@ -19,10 +19,15 @@ package org.libreccm.theming.db; import org.libreccm.security.RequiresPrivilege; +import org.libreccm.theming.ThemeVersion; import org.libreccm.theming.ThemingPrivileges; +import java.util.Date; +import java.util.Objects; + import javax.enterprise.context.RequestScoped; import javax.inject.Inject; +import javax.transaction.Transactional; /** * Provides methods for managing the files of the theme stored in the database. @@ -35,32 +40,93 @@ public class ThemeFileManager { @Inject private ThemeFileRepository fileRepository; + @Inject + private ThemeRepository themeRepository; + /** - * Creates a new {@link DataFile}. + * Creates a new, empty {@link DataFile}. * - * @param parent The directory in which the {@link DataFile} is created. + * @param theme The {@link Theme} to which the file belongs. + * @param parent The {@link Directory} in which the {@link DataFile} is + * created. * @param name The name of the new {@link DataFile}. * * @return The new {@link DataFile}. */ @RequiresPrivilege(ThemingPrivileges.EDIT_THEME) - public DataFile createDataFile(final Directory parent, + @Transactional(Transactional.TxType.REQUIRED) + public DataFile createDataFile(final Theme theme, + final Directory parent, final String name) { - throw new UnsupportedOperationException(); + + Objects.requireNonNull(parent); + Objects.requireNonNull(name); + + if (name.matches("\\s*")) { + throw new IllegalArgumentException( + "The name of file can't be empty."); + } + + final Date now = new Date(); + final String path = String.join("/", parent.getPath(), name); + + final DataFile dataFile = new DataFile(); + dataFile.setCreationDate(now); + dataFile.setLastModified(now); + dataFile.setName(name); + dataFile.setParent(parent); + dataFile.setPath(path); + dataFile.setTheme(theme); + dataFile.setVersion(ThemeVersion.DRAFT); + + parent.addFile(dataFile); + + fileRepository.save(dataFile); + fileRepository.save(parent); + themeRepository.save(theme); + + return dataFile; } /** * Creates a new {@link Directory}. * + * @param theme The {@link Theme} to which the file belongs. * @param parent The parent directory of the new {@link Directory}. * @param name The name of the new {@link Directory} * * @return The new {@link Directory}. */ @RequiresPrivilege(ThemingPrivileges.EDIT_THEME) - public Directory createDirectory(final Directory parent, + @Transactional(Transactional.TxType.REQUIRED) + public Directory createDirectory(final Theme theme, + final Directory parent, final String name) { - throw new UnsupportedOperationException(); + + Objects.requireNonNull(parent); + Objects.requireNonNull(name); + + if (name.matches("\\s*")) { + throw new IllegalArgumentException( + "The name of file can't be empty."); + } + + final String path = String.join("/", parent.getPath(), name); + + final Directory directory = new Directory(); + directory.setName(name); + directory.setParent(parent); + directory.setPath(path); + directory.setTheme(theme); + directory.setVersion(ThemeVersion.DRAFT); + + parent.addFile(directory); + + fileRepository.save(directory); + fileRepository.save(parent); + themeRepository.save(theme); + + return directory; } /** @@ -70,8 +136,33 @@ public class ThemeFileManager { * @param file The {@link ThemeFile} to delete. */ @RequiresPrivilege(ThemingPrivileges.EDIT_THEME) + @Transactional(Transactional.TxType.REQUIRED) public void delete(final ThemeFile file) { - throw new UnsupportedOperationException(); + + Objects.requireNonNull(file); + + if (file instanceof DataFile) { + final Directory parent = file.getParent(); + parent.removeFile(file); + fileRepository.delete(file); + fileRepository.save(parent); + } else if (file instanceof Directory) { + final Directory directory = (Directory) file; + if (directory.getFiles().isEmpty()) { + final Directory parent = file.getParent(); + parent.removeFile(file); + fileRepository.delete(file); + fileRepository.save(parent); + } else { + throw new IllegalArgumentException(String + .format("File \"%s\" is a directory and not empty.", + directory.getPath())); + } + } else { + throw new IllegalArgumentException(String + .format("Don't know how handle file type \"%s\".", + file.getClass().getName())); + } } /** @@ -84,8 +175,28 @@ public class ThemeFileManager { * @param file The {@link ThemeFile} to delete. */ @RequiresPrivilege(ThemingPrivileges.EDIT_THEME) + @Transactional(Transactional.TxType.REQUIRED) public void deleteRecursive(final ThemeFile file) { - throw new UnsupportedOperationException(); + + Objects.requireNonNull(file); + + if (file instanceof DataFile) { + delete(file); + } else if (file instanceof Directory) { + + final Directory directory = (Directory) file; + directory + .getFiles() + .forEach(subFile -> deleteRecursive(subFile)); + final Directory parent = file.getParent(); + parent.removeFile(file); + fileRepository.delete(file); + fileRepository.save(parent); + } else { + throw new IllegalArgumentException(String + .format("Don't know how handle file type \"%s\".", + file.getClass().getName())); + } } /** @@ -98,8 +209,13 @@ public class ThemeFileManager { * @return The newly created copy. */ @RequiresPrivilege(ThemingPrivileges.EDIT_THEME) + @Transactional(Transactional.TxType.REQUIRED) public ThemeFile copy(final ThemeFile file, final Directory target) { - throw new UnsupportedOperationException(); + + Objects.requireNonNull(file); + Objects.requireNonNull(target); + + return copy(file, target, file.getName()); } /** @@ -112,10 +228,73 @@ public class ThemeFileManager { * @return The copy. */ @RequiresPrivilege(ThemingPrivileges.EDIT_THEME) + @Transactional(Transactional.TxType.REQUIRED) public ThemeFile copy(final ThemeFile file, final Directory target, final String nameOfCopy) { - throw new UnsupportedOperationException(); + + Objects.requireNonNull(file); + Objects.requireNonNull(target); + Objects.requireNonNull(nameOfCopy); + + if (nameOfCopy.matches("\\s*")) { + throw new IllegalArgumentException( + "The name of the copy can't be empty."); + } + + target + .getFiles() + .stream() + .filter(subFile -> subFile.getName().equals(nameOfCopy)) + .findAny() + .ifPresent(subFile -> { + throw new IllegalArgumentException(String + .format("The target directory \"%s\"already contains a " + + "file with name \"%s\".", + target.getPath(), + nameOfCopy)); + }); + + if (file instanceof DataFile) { + + final DataFile source = (DataFile) file; + final DataFile copy = new DataFile(); + final Date now = new Date(); + copy.setCreationDate(now); + copy.setData(source.getData()); + copy.setLastModified(now); + copy.setName(nameOfCopy); + copy.setParent(target); + copy.setPath(String.join("/", target.getPath(), copy.getName())); + copy.setSize(source.getSize()); + copy.setTheme(source.getTheme()); + copy.setType(source.getType()); + copy.setVersion(source.getVersion()); + + fileRepository.save(copy); + fileRepository.save(target); + themeRepository.save(copy.getTheme()); + + return copy; + } else if (file instanceof Directory) { + + final Directory source = (Directory) file; + final Directory copy = new Directory(); + copy.setName(nameOfCopy); + copy.setParent(target); + copy.setPath(String.join("/", target.getPath(), copy.getName())); + copy.setTheme(source.getTheme()); + copy.setVersion(source.getVersion()); + + fileRepository.save(copy); + fileRepository.save(target); + + return copy; + } else { + throw new IllegalArgumentException(String + .format("Don't know how handle file type \"%s\".", + file.getClass().getName())); + } } /** @@ -128,9 +307,14 @@ public class ThemeFileManager { * @return The copy. */ @RequiresPrivilege(ThemingPrivileges.EDIT_THEME) + @Transactional(Transactional.TxType.REQUIRED) public ThemeFile copyRecursive(final ThemeFile file, final Directory target) { - throw new UnsupportedOperationException(); + + Objects.requireNonNull(file); + Objects.requireNonNull(target); + + return copyRecursive(file, target, file.getName()); } /** @@ -144,10 +328,32 @@ public class ThemeFileManager { * @return The copy. */ @RequiresPrivilege(ThemingPrivileges.EDIT_THEME) + @Transactional(Transactional.TxType.REQUIRED) public ThemeFile copyRecursive(final ThemeFile file, final Directory target, final String nameOfCopy) { - throw new UnsupportedOperationException(); + + Objects.requireNonNull(file); + Objects.requireNonNull(target); + Objects.requireNonNull(nameOfCopy); + + if (nameOfCopy.matches("\\s*")) { + throw new IllegalArgumentException( + "The name of a file can't be empty."); + } + + final ThemeFile copy = copy(file, target, nameOfCopy); + + if (file instanceof Directory) { + final Directory source = (Directory) file; + final Directory copiedDirectory = (Directory) copy; + + source + .getFiles() + .forEach(subFile -> copyRecursive(subFile, copiedDirectory)); + } + + return copy; } /** @@ -161,7 +367,11 @@ public class ThemeFileManager { @RequiresPrivilege(ThemingPrivileges.EDIT_THEME) public ThemeFile move(final ThemeFile file, final Directory target) { - throw new UnsupportedOperationException(); + + Objects.requireNonNull(file); + Objects.requireNonNull(target); + + return move(file, target, file.getName()); } /** @@ -177,7 +387,42 @@ public class ThemeFileManager { public ThemeFile move(final ThemeFile file, final Directory target, final String newName) { - throw new UnsupportedOperationException(); + + Objects.requireNonNull(file); + Objects.requireNonNull(target); + Objects.requireNonNull(newName); + + if (newName.matches("\\s*")) { + throw new IllegalArgumentException( + "The name of a file can't be empty."); + } + + target + .getFiles() + .stream() + .filter(subFile -> subFile.getName().equals(newName)) + .findAny() + .ifPresent(subFile -> { + throw new IllegalArgumentException(String + .format("The target directory \"%s\"already contains a " + + "file with name \"%s\".", + target.getPath(), + newName)); + }); + + final Directory oldParent = file.getParent(); + + file.setName(newName); + oldParent.removeFile(file); + target.addFile(file); + file.setParent(target); + + fileRepository.save(file); + fileRepository.save(oldParent); + fileRepository.save(target); + + return file; + } } diff --git a/ccm-core/src/main/java/org/libreccm/theming/db/ThemeFileRepository.java b/ccm-core/src/main/java/org/libreccm/theming/db/ThemeFileRepository.java index c099cf5a2..a71b17485 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/db/ThemeFileRepository.java +++ b/ccm-core/src/main/java/org/libreccm/theming/db/ThemeFileRepository.java @@ -19,8 +19,17 @@ package org.libreccm.theming.db; import org.libreccm.core.AbstractEntityRepository; +import org.libreccm.core.UnexpectedErrorException; +import org.libreccm.theming.ThemeVersion; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLConnection; +import java.util.Date; import java.util.Optional; +import java.util.UUID; import javax.enterprise.context.RequestScoped; import javax.persistence.NoResultException; @@ -55,6 +64,33 @@ public class ThemeFileRepository extends AbstractEntityRepository findByUuid(final String uuid, final ThemeVersion version) { @@ -70,7 +106,8 @@ public class ThemeFileRepository extends AbstractEntityRepository findByPath(final String path, + public Optional findByPath(final Theme theme, + final String path, final ThemeVersion version) { final TypedQuery query = getEntityManager() diff --git a/ccm-core/src/main/java/org/libreccm/theming/db/ThemeManager.java b/ccm-core/src/main/java/org/libreccm/theming/db/ThemeManager.java index 003d27008..4936b0c97 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/db/ThemeManager.java +++ b/ccm-core/src/main/java/org/libreccm/theming/db/ThemeManager.java @@ -19,11 +19,18 @@ package org.libreccm.theming.db; import org.libreccm.security.RequiresPrivilege; +import org.libreccm.theming.ThemeConstants; +import org.libreccm.theming.ThemeVersion; import org.libreccm.theming.ThemingPrivileges; import org.libreccm.theming.manifest.ThemeManifest; +import org.libreccm.theming.manifest.ThemeManifestUtil; + +import java.util.Objects; +import java.util.Optional; import javax.enterprise.context.RequestScoped; import javax.inject.Inject; +import javax.transaction.Transactional; /** * Provides methods for managing themes stored in the database. @@ -33,9 +40,15 @@ import javax.inject.Inject; @RequestScoped public class ThemeManager { + @Inject + private ThemeManifestUtil manifestUtil; + @Inject private ThemeRepository themeRepository; + @Inject + private ThemeFileRepository themeFileRepository; + /** * Creates a new theme, including the root directory and a theme manifest * file. @@ -47,8 +60,44 @@ public class ThemeManager { * @return The new theme. */ @RequiresPrivilege(ThemingPrivileges.ADMINISTER_THEMES) + @Transactional(Transactional.TxType.REQUIRED) public Theme createTheme(final String name) { - throw new UnsupportedOperationException(); + + Objects.requireNonNull(name); + + if (name.matches("\\s*")) { + throw new IllegalArgumentException( + "The name of a theme can't be empty."); + } + + final Theme theme = new Theme(); + theme.setName(name); + theme.setVersion(ThemeVersion.DRAFT); + + final Directory root = new Directory(); + root.setName(name); + root.setPath("/"); + root.setTheme(theme); + + final ThemeManifest manifest = new ThemeManifest(); + manifest.setName(name); + + final DataFile manifestFile = new DataFile(); + manifestFile.setName(ThemeConstants.THEME_MANIFEST_JSON); + manifestFile.setPath(String.format("/%s", + ThemeConstants.THEME_MANIFEST_JSON)); + manifestFile.setTheme(theme); + + final String manifestData = manifestUtil + .serializeManifest(manifest, ThemeConstants.THEME_MANIFEST_JSON); + manifestFile.setData(manifestData.getBytes()); + root.addFile(manifestFile); + + themeRepository.save(theme); + themeFileRepository.save(root); + themeFileRepository.save(manifestFile); + + return theme; } /** @@ -57,8 +106,71 @@ public class ThemeManager { * @param theme The theme to delete. */ @RequiresPrivilege(ThemingPrivileges.ADMINISTER_THEMES) + @Transactional(Transactional.TxType.REQUIRED) public void deleteTheme(final Theme theme) { - throw new UnsupportedOperationException(); + + Objects.requireNonNull(theme); + + if (isLive(theme)) { + throw new IllegalArgumentException(String + .format("The theme \"%s\" is live and can't be deleted.", + theme.getName())); + } + + themeRepository.delete(theme); + } + + /** + * Checks if a theme has a live version. + * + * @param theme The theme. + * + * @return {@code true} if there is a live version of the provided theme, + * {@code false} otherwise. + */ + @Transactional(Transactional.TxType.REQUIRED) + public boolean isLive(final Theme theme) { + + Objects.requireNonNull(theme); + + return themeRepository + .findThemeByUuid(theme.getUuid(), ThemeVersion.LIVE) + .isPresent(); + } + + @Transactional(Transactional.TxType.REQUIRED) + public Theme getDraftTheme(final Theme theme) { + + Objects.requireNonNull(theme); + + if (theme.getVersion() == ThemeVersion.DRAFT) { + return theme; + } else { + return themeRepository + .findThemeByUuid(theme.getUuid(), ThemeVersion.DRAFT) + .orElseThrow(() -> new IllegalArgumentException(String + .format("No draft theme with UUID \"%s\" in the database.", + theme.getUuid()))); + } + + } + + /** + * Retrieves the live version of a theme. + * + * @param theme The theme. + * + * @return An {@link Optional} containing the live version of the provided + * theme or an empty {@link Optional} if the theme has no live + * version. + */ + @Transactional(Transactional.TxType.REQUIRED) + public Optional getLiveTheme(final Theme theme) { + + Objects.requireNonNull(theme); + + return themeRepository + .findThemeByUuid(theme.getUuid(), ThemeVersion.LIVE); } /** @@ -71,18 +183,149 @@ public class ThemeManager { * */ @RequiresPrivilege(ThemingPrivileges.ADMINISTER_THEMES) + @Transactional(Transactional.TxType.REQUIRED) public void publishTheme(final Theme theme) { + + Objects.requireNonNull(theme); + + final Theme draftTheme; + if (theme.getVersion() == ThemeVersion.DRAFT) { + draftTheme = theme; + } else { + draftTheme = getDraftTheme(theme); + } + + if (isLive(draftTheme)) { + unpublishTheme(draftTheme); + } + + final Theme liveTheme = new Theme(); + liveTheme.setName(draftTheme.getName()); + liveTheme.setUuid(draftTheme.getUuid()); + liveTheme.setVersion(ThemeVersion.LIVE); + + final Directory liveRoot = new Directory(); + liveRoot.setName(draftTheme.getRootDirectory().getName()); + liveRoot.setPath(draftTheme.getRootDirectory().getPath()); + liveRoot.setUuid(draftTheme.getRootDirectory().getUuid()); + liveRoot.setTheme(theme); + liveRoot.setVersion(ThemeVersion.LIVE); + + themeRepository.save(liveTheme); + themeFileRepository.save(liveRoot); + + draftTheme + .getRootDirectory() + .getFiles() + .forEach(file -> publishFile(liveTheme, liveRoot, file)); + throw new UnsupportedOperationException(); } + private void publishFile(final Theme liveTheme, + final Directory liveParent, + final ThemeFile draftFile) { + + Objects.requireNonNull(liveParent); + Objects.requireNonNull(draftFile); + + if (liveParent.getVersion() != ThemeVersion.LIVE) { + throw new IllegalArgumentException("Parent directory is not live."); + } + + if (draftFile.getVersion() != ThemeVersion.DRAFT) { + throw new IllegalArgumentException("File to publish is not draft."); + } + + if (draftFile instanceof Directory) { + + final Directory draftDirectory = (Directory) draftFile; + + final Directory liveDirectory = new Directory(); + liveDirectory.setName(draftDirectory.getName()); + liveDirectory.setPath(draftDirectory.getPath()); + liveDirectory.setParent(liveParent); + liveDirectory.setUuid(draftDirectory.getUuid()); + liveDirectory.setVersion(ThemeVersion.LIVE); + liveDirectory.setTheme(liveTheme); + + themeFileRepository.save(liveDirectory); + + draftDirectory + .getFiles() + .forEach(file -> publishFile(liveTheme, liveDirectory, file)); + + } else if (draftFile instanceof DataFile) { + + final DataFile draftDataFile = (DataFile) draftFile; + + final DataFile liveDataFile = new DataFile(); + liveDataFile.setCreationDate(draftDataFile.getCreationDate()); + liveDataFile.setData(draftDataFile.getData()); + liveDataFile.setLastModified(draftDataFile.getLastModified()); + liveDataFile.setName(draftDataFile.getName()); + liveDataFile.setParent(liveParent); + liveDataFile.setPath(draftDataFile.getPath()); + liveDataFile.setSize(draftDataFile.getSize()); + liveDataFile.setType(draftDataFile.getType()); + liveDataFile.setUuid(draftDataFile.getUuid()); + liveDataFile.setTheme(liveTheme); + liveDataFile.setVersion(ThemeVersion.LIVE); + + themeFileRepository.save(liveDataFile); + } else { + throw new IllegalArgumentException(String + .format("Don't know how handle file type \"%s\".", + draftFile.getClass().getName())); + } + } + /** - * Unpublishes a theme by deleting the live version of the theme. + * Unpublishes a theme by deleting the live version of the theme. If the + * theme is not published the method will return without doing anything. * * @param theme The theme to unpublish. */ @RequiresPrivilege(ThemingPrivileges.ADMINISTER_THEMES) + @Transactional(Transactional.TxType.REQUIRED) public void unpublishTheme(final Theme theme) { - throw new UnsupportedOperationException(); + + Objects.requireNonNull(theme); + + if (!isLive(theme)) { + return; + } + + final Theme liveTheme = getLiveTheme(theme).get(); + final Directory liveRoot = liveTheme.getRootDirectory(); + + liveRoot + .getFiles() + .forEach(file -> unpublishFile(file)); + } + + private void unpublishFile(final ThemeFile themeFile) { + + Objects.requireNonNull(themeFile); + if (themeFile.getVersion() != ThemeVersion.LIVE) { + throw new IllegalArgumentException( + "Only live files can be unpublished."); + } + + if (themeFile instanceof DataFile) { + themeFileRepository.delete(themeFile); + } else if (themeFile instanceof Directory) { + final Directory directory = (Directory) themeFile; + directory + .getFiles() + .forEach(file -> unpublishFile(file)); + themeFileRepository.delete(themeFile); + } else { + throw new IllegalArgumentException(String + .format("Don't know how handle file type \"%s\".", + themeFile.getClass().getName())); + } + } } diff --git a/ccm-core/src/main/java/org/libreccm/theming/db/ThemeRepository.java b/ccm-core/src/main/java/org/libreccm/theming/db/ThemeRepository.java index 4c4bc9ddf..eee8febde 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/db/ThemeRepository.java +++ b/ccm-core/src/main/java/org/libreccm/theming/db/ThemeRepository.java @@ -19,8 +19,13 @@ package org.libreccm.theming.db; import org.libreccm.core.AbstractEntityRepository; +import org.libreccm.security.RequiresPrivilege; +import org.libreccm.theming.ThemeVersion; +import org.libreccm.theming.ThemingPrivileges; +import java.util.List; import java.util.Optional; +import java.util.UUID; import javax.enterprise.context.RequestScoped; import javax.persistence.NoResultException; @@ -57,10 +62,27 @@ public class ThemeRepository extends AbstractEntityRepository { } @Override - public void save(final Theme theme) { - super.save(theme);; + public void initNewEntity(final Theme theme) { + if (theme.getUuid() == null || theme.getUuid().isEmpty()) { + theme.setUuid(UUID.randomUUID().toString()); + } } - + + @Override + @RequiresPrivilege(ThemingPrivileges.ADMINISTER_THEMES) + public void save(final Theme theme) { + super.save(theme); + } + + public List findAll(final ThemeVersion version) { + + final TypedQuery query = getEntityManager() + .createNamedQuery("Theme.findAllForVersion", Theme.class); + query.setParameter("version", version); + + return query.getResultList(); + } + @Transactional(Transactional.TxType.REQUIRED) public Optional findThemeByUuid(final String uuid, final ThemeVersion version) { diff --git a/ccm-core/src/main/java/org/libreccm/theming/db/ThemeVersion.java b/ccm-core/src/main/java/org/libreccm/theming/db/ThemeVersion.java deleted file mode 100644 index 38a3c57c6..000000000 --- a/ccm-core/src/main/java/org/libreccm/theming/db/ThemeVersion.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2018 LibreCCM Foundation. - * - * This library 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; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library 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 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., 51 Franklin Street, Fifth Floor, Boston, - * MA 02110-1301 USA - */ -package org.libreccm.theming.db; - -/** - * - * @author Jens Pelzetter - */ -public enum ThemeVersion { - - DRAFT, - LIVE - -} diff --git a/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifestUtil.java b/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifestUtil.java index b2ef9883c..0814d4ed2 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifestUtil.java +++ b/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifestUtil.java @@ -32,6 +32,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.Serializable; +import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.nio.file.Files; @@ -60,7 +61,6 @@ public class ThemeManifestUtil implements Serializable { public ThemeManifest loadManifest(final Path path) { // final String pathStr = path.toString().toLowerCase(Locale.ROOT); - final BufferedReader reader; try { reader = Files.newBufferedReader(path, Charset.forName("UTF-8")); @@ -69,7 +69,7 @@ public class ThemeManifestUtil implements Serializable { } return parseManifest(reader, path.toString()); - + // final ObjectMapper mapper; // if (pathStr.endsWith(THEME_MANIFEST_JSON)) { // mapper = new ObjectMapper(); @@ -105,7 +105,7 @@ public class ThemeManifestUtil implements Serializable { } return parseManifest(reader, fileName); - + // final ObjectMapper mapper; // if (fileName.endsWith(THEME_MANIFEST_JSON)) { // mapper = new ObjectMapper(); @@ -130,7 +130,38 @@ public class ThemeManifestUtil implements Serializable { // return manifest; } - private ThemeManifest parseManifest(final Reader reader, final String path) { + public String serializeManifest(final ThemeManifest manifest, + final String format) { + + final ObjectMapper mapper; + + switch (format) { + case THEME_MANIFEST_JSON: + mapper = new ObjectMapper(); + break; + case THEME_MANIFEST_XML: + final JacksonXmlModule xmlModule = new JacksonXmlModule(); + mapper = new XmlMapper(xmlModule); + break; + default: + throw new IllegalArgumentException( + "Unsupported format for ThemeManifest"); + } + + mapper.registerModule(new JaxbAnnotationModule()); + + final StringWriter writer = new StringWriter(); + try { + mapper.writeValue(writer, manifest); + } catch (IOException ex) { + throw new UnexpectedErrorException(ex); + } + + return writer.toString(); + } + + private ThemeManifest parseManifest(final Reader reader, + final String path) { final String pathStr = path.toLowerCase(Locale.ROOT);