From b0c026abea92aa46b33a21366c2f4f10bf2109f4 Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Wed, 8 Jul 2015 15:22:39 +0200 Subject: [PATCH] uniti front-end e back-end in un unico war --- build.gradle | 17 +- jpacrepo.iml | 2 + .../jpacrepo/context/ApplicationContext.java | 7 + .../org/jpacrepo/context/ContextProducer.java | 10 +- .../frontend/component/JPacRepoApp.java | 218 ++++++++++++++++++ .../frontend/component/PackageTable.java | 76 ++++++ .../frontend/servlet/JPacRepoServlet.java | 38 +++ .../java/org/jpacrepo/model/PkgData_.java | 37 +++ .../java/org/jpacrepo/model/PkgName_.java | 14 ++ .../org/jpacrepo/service/PacmanService.java | 14 +- .../jpacrepo/service/PacmanServiceEJB.java | 133 +++++++++++ src/main/java/org/jpacrepo/util/Utility.java | 35 +++ src/main/resources/WEB-INF/web.xml | 17 ++ src/main/resources/template/jumbotron.html | 4 + .../template/package-search-form.html | 46 ++++ src/main/webapp/css/jpacrepo-web.css | 28 +++ src/main/webapp/img/download.png | Bin 0 -> 5834 bytes src/main/webapp/img/download.svg | 15 ++ 18 files changed, 707 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/jpacrepo/frontend/component/JPacRepoApp.java create mode 100644 src/main/java/org/jpacrepo/frontend/component/PackageTable.java create mode 100644 src/main/java/org/jpacrepo/frontend/servlet/JPacRepoServlet.java create mode 100644 src/main/java/org/jpacrepo/model/PkgData_.java create mode 100644 src/main/java/org/jpacrepo/model/PkgName_.java create mode 100644 src/main/java/org/jpacrepo/util/Utility.java create mode 100644 src/main/resources/WEB-INF/web.xml create mode 100644 src/main/resources/template/jumbotron.html create mode 100644 src/main/resources/template/package-search-form.html create mode 100644 src/main/webapp/css/jpacrepo-web.css create mode 100644 src/main/webapp/img/download.png create mode 100644 src/main/webapp/img/download.svg diff --git a/build.gradle b/build.gradle index 4050554..6b8c9e4 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,11 @@ dependencies { compile 'javax:javaee-api:7.0' compile 'commons-io:commons-io:2.4' compile 'commons-codec:commons-codec:1.10' + compile 'eu.webtoolkit:jwt:3.3.4' + compile 'org.projectlombok:lombok:1.16.4' + compile 'commons-fileupload:commons-fileupload:1.3.1' + + testCompile 'com.thoughtworks.xstream:xstream:1.4.8' testCompile 'org.jboss.resteasy:resteasy-jaxrs:3.0.11.Final' testCompile 'org.jboss.resteasy:resteasy-client:3.0.11.Final' @@ -37,7 +42,17 @@ dependencies { } task deployWildfly(dependsOn: 'war') << { - '/opt/wildfly/bin/jboss-cli.sh --connect --user=admin --password=qwerty --command="deploy build/libs/jpacrepo-1.0.war --force"'.execute().waitFor() + def username = 'admin', password = '123456' + def sout = new StringBuffer(), serr = new StringBuffer() + def cmd = sprintf('/opt/wildfly/bin/jboss-cli.sh --connect --user=%s --password=%s --command="deploy %s --force"', username, password, tasks['war'].archivePath) + def proc = cmd.execute() + proc.consumeProcessOutput(sout, serr) + proc.waitFor() + if(proc.exitValue() != 0) + { + println "$serr" + throw new RuntimeException("Error occurred during deployment") + } } // client.jar diff --git a/jpacrepo.iml b/jpacrepo.iml index ceb1dc1..fdc2763 100644 --- a/jpacrepo.iml +++ b/jpacrepo.iml @@ -55,5 +55,7 @@ + + \ No newline at end of file diff --git a/src/main/java/org/jpacrepo/context/ApplicationContext.java b/src/main/java/org/jpacrepo/context/ApplicationContext.java index 3451bd0..1733ba9 100644 --- a/src/main/java/org/jpacrepo/context/ApplicationContext.java +++ b/src/main/java/org/jpacrepo/context/ApplicationContext.java @@ -1,8 +1,12 @@ package org.jpacrepo.context; +import lombok.Getter; +import lombok.Setter; import org.jpacrepo.model.PkgData; +import org.jpacrepo.service.PacmanService; import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; import javax.inject.Qualifier; import java.io.File; import java.io.FileInputStream; @@ -28,6 +32,9 @@ public class ApplicationContext private String repoFolder; + @Getter @Setter + private PacmanService pacmanService; + public ApplicationContext(String propertyFile) { systemProperties = new Properties(); diff --git a/src/main/java/org/jpacrepo/context/ContextProducer.java b/src/main/java/org/jpacrepo/context/ContextProducer.java index ed03eb8..f59c5f1 100644 --- a/src/main/java/org/jpacrepo/context/ContextProducer.java +++ b/src/main/java/org/jpacrepo/context/ContextProducer.java @@ -1,5 +1,8 @@ package org.jpacrepo.context; +import org.jpacrepo.service.PacmanService; + +import javax.ejb.EJB; import javax.enterprise.inject.Produces; /** @@ -9,10 +12,15 @@ import javax.enterprise.inject.Produces; public class ContextProducer { + @EJB + PacmanService service; + @Produces @DefaultConfiguration public ApplicationContext produce() { - return new ApplicationContext("/etc/jpacrepo/server.properties"); + ApplicationContext ctx = new ApplicationContext("/etc/jpacrepo/server.properties"); + ctx.setPacmanService(service); + return ctx; } } diff --git a/src/main/java/org/jpacrepo/frontend/component/JPacRepoApp.java b/src/main/java/org/jpacrepo/frontend/component/JPacRepoApp.java new file mode 100644 index 0000000..70e8967 --- /dev/null +++ b/src/main/java/org/jpacrepo/frontend/component/JPacRepoApp.java @@ -0,0 +1,218 @@ +package org.jpacrepo.frontend.component; + +/** + * Created by walter on 06/06/15. + */ + + +import eu.webtoolkit.jwt.*; +import org.jpacrepo.context.ApplicationContext; +import org.jpacrepo.model.PkgName; +import org.jpacrepo.util.Utility; + +import java.util.*; + +/** + * Created by walter on 03/01/15. + */ +public class JPacRepoApp extends WApplication +{ + private ApplicationContext ctx; + private PackageTable table; + private WPushButton searchButton; + private WMenu pageMenu; + private WMenu pagerMenu; + private WComboBox pageCombo; + + private String lastName; + + private WSuggestionPopup nameSuggestion, versionSuggestion, archSuggestion; + + private int pageSize = 10; + + private WLineEdit packageNameEdit, packageVersionEdit, packageArchEdit; + + public JPacRepoApp(WEnvironment env, ApplicationContext ctx) + { + super(env); + this.ctx = ctx; + init(); + } + + private void init() + { + WBootstrapTheme theme = new WBootstrapTheme(); + theme.setVersion(WBootstrapTheme.Version.Version3); + setTheme(theme); + useStyleSheet(new WLink(WApplication.getRelativeResourcesUrl() + "/themes/bootstrap/3/bootstrap-theme.min.css")); + useStyleSheet(new WLink("css/jpacrepo-web.css")); +// useStyleSheet(new WLink("css/everywidget.css")); + createView(); + } + + public void createView() + { + WTemplate jumbotron = new WTemplate(Utility.getTemplate("jumbotron.html")); + packageArchEdit = new WLineEdit(); + packageArchEdit.setWidth(new WLength("100%")); + packageNameEdit = new WLineEdit(); + packageNameEdit.setWidth(new WLength("100%")); + packageVersionEdit = new WLineEdit(); + packageVersionEdit.setWidth(new WLength("100%")); + searchButton = new WPushButton("Search"); + pageCombo = new WComboBox(); + for (int i = 1; i <= 10; i++) + { + pageCombo.addItem(Integer.toString(i * 10)); + } + + + searchButton.clicked().addListener(this, () -> + { + pageSize = Integer.parseInt(pageCombo.getCurrentText().toString()); + while (pageMenu.getCount() > 0) + { + pageMenu.removeItem(pageMenu.itemAt(0)); + } + while (pagerMenu.getCount() > 0) + { + pagerMenu.removeItem(pagerMenu.itemAt(0)); + } + + long resultNumber = ctx.getPacmanService().countResults(packageNameEdit.getDisplayText(), packageVersionEdit.getDisplayText(), packageArchEdit.getDisplayText()); + if (resultNumber < pageSize) + { + table.showPackages(ctx.getPacmanService().searchPackage(packageNameEdit.getDisplayText(), packageVersionEdit.getDisplayText(), packageArchEdit.getDisplayText(), -1, -1)); + } + else + { + int remaining = (int) resultNumber; + int i = 1; + while (remaining > 0) + { + final int logicalPageNumber = i - 1; + WMenuItem menuItem = new WMenuItem(Integer.toString(i++)); + pageMenu.addItem(menuItem); + menuItem.clicked().addListener(this, () -> + { + table.showPackages(ctx.getPacmanService().searchPackage(packageNameEdit.getText(), packageVersionEdit.getText(), packageArchEdit.getText(), logicalPageNumber, pageSize)); + }); + remaining -= pageSize; + } + pageMenu.select(0); + + WMenuItem previousItem = new WMenuItem("Previous"), nextItem = new WMenuItem("Next"); + previousItem.setStyleClass("previous"); + nextItem.setStyleClass("next"); + + previousItem.clicked().addListener(this, () -> + { + int newIndex = pageMenu.getCurrentIndex() - 1; + if(newIndex>=0) + { + pageMenu.select(newIndex); + pageMenu.itemAt(newIndex).clicked().trigger(new WMouseEvent()); + } + }); + + nextItem.clicked().addListener(this, () -> + { + int newIndex = pageMenu.getCurrentIndex() + 1; + if(newIndex nameList = ctx.getPacmanService().listProperty("name", new HashMap<>(), PkgName.class); + List nameTxtList = new ArrayList<>(); + nameList.forEach((p) -> nameTxtList.add(p.id)); + nameSuggestion = getSuggestionPopup(nameTxtList, res, packageNameEdit); + nameSuggestion.setFilterLength(3); + nameSuggestion.setMaximumSize(WLength.Auto, new WLength("70%")); + + versionSuggestion = getSuggestionPopup(new ArrayList<>(), res, packageVersionEdit); + versionSuggestion.setMaximumSize(WLength.Auto, new WLength("70%")); + + packageNameEdit.blurred().addListener(this, () -> + { + if(!packageNameEdit.getDisplayText().equals(lastName)) + { + lastName = packageNameEdit.getDisplayText(); + Map predicateMap = new HashMap<>(); + predicateMap.put("name", packageNameEdit.getDisplayText()); + List versions = ctx.getPacmanService().listProperty("version", predicateMap, String.class); + + versionSuggestion.getModel().removeRows(0, versionSuggestion.getModel().getRowCount()); + versions.forEach((e) -> versionSuggestion.addSuggestion(e)); + packageVersionEdit.setText(""); + packageArchEdit.setText(""); + } + }); + + List archs = ctx.getPacmanService().listProperty("arch", new HashMap<>(), String.class); + archSuggestion = getSuggestionPopup(archs, res, packageArchEdit); + + res.addWidget(jumbotron); + res.addWidget(inputTemplate); + WContainerWidget containerWidget = new WContainerWidget(); + containerWidget.addWidget(table); + containerWidget.setStyleClass("row"); + res.addWidget(containerWidget); + res.setStyleClass("container main-container"); + + pageMenu = new WMenu(); + pageMenu.setStyleClass("pagination"); + res.addWidget(pageMenu); + + pagerMenu = new WMenu(); + pageCombo.setWidth(new WLength("100%")); + pagerMenu.setStyleClass("pager"); + res.addWidget(pagerMenu); + + getRoot().addWidget(res); + } + + WSuggestionPopup getSuggestionPopup(List suggestions, WWidget parent, WFormWidget linkedFormWidget) + { + WSuggestionPopup.Options contactOptions = new WSuggestionPopup.Options(); + contactOptions.highlightBeginTag = ""; + contactOptions.highlightEndTag = ""; + contactOptions.listSeparator = ','; + contactOptions.whitespace = " \\n"; + contactOptions.wordSeparators = "-., \"@\\n;"; + //contactOptions.appendReplacedText = ", "; + WSuggestionPopup popup = new WSuggestionPopup(WSuggestionPopup + .generateMatcherJS(contactOptions), WSuggestionPopup + .generateReplacerJS(contactOptions), parent); + popup.forEdit(linkedFormWidget, EnumSet.of(WSuggestionPopup.PopupTrigger.Editing, WSuggestionPopup.PopupTrigger.DropDownIcon)); + List wstrings = new ArrayList<>(); + for(String suggestion :suggestions) + { + wstrings.add(new WString(suggestion)); +// popup.addSuggestion(suggestion); + } + popup.setModel(new WStringListModel(wstrings)); + return popup; + } + + +} diff --git a/src/main/java/org/jpacrepo/frontend/component/PackageTable.java b/src/main/java/org/jpacrepo/frontend/component/PackageTable.java new file mode 100644 index 0000000..ea9fc6f --- /dev/null +++ b/src/main/java/org/jpacrepo/frontend/component/PackageTable.java @@ -0,0 +1,76 @@ +package org.jpacrepo.frontend.component; + +import eu.webtoolkit.jwt.*; +import org.jpacrepo.model.PkgData; + +import java.text.DecimalFormat; +import java.util.List; + +/** + * Created by walter on 06/06/15. + */ +public class PackageTable extends WTable +{ + public PackageTable() + { + toggleStyleClass("table-bordered", true); + toggleStyleClass("table-condensed", true); + toggleStyleClass("table-striped", true); + + } + + public void showPackages(List pkgs) + { + clear(); + setHeaderCount(1); + setWidth(new WLength("100%")); + + getElementAt(0, 0).addWidget(new WText("Name")); + getElementAt(0, 1).addWidget(new WText("Version")); + getElementAt(0, 2).addWidget(new WText("Arch")); + getElementAt(0, 3).addWidget(new WText("Description")); + getElementAt(0, 4).addWidget(new WText("Size")); + + + for(int row =1 ; row fileName; + public static volatile ListAttribute backup; + public static volatile ListAttribute makeopkgopt; + public static volatile ListAttribute depend; + public static volatile ListAttribute replaces; + public static volatile SingularAttribute updTimestamp; + public static volatile SingularAttribute description; + public static volatile SingularAttribute buildDate; + public static volatile SingularAttribute version; + public static volatile SingularAttribute packager; + public static volatile SingularAttribute url; + public static volatile SingularAttribute license; + public static volatile SingularAttribute size; + public static volatile ListAttribute makedepend; + public static volatile ListAttribute optdepend; + public static volatile SingularAttribute md5sum; + public static volatile ListAttribute provides; + public static volatile SingularAttribute name; + public static volatile SingularAttribute id; + public static volatile SingularAttribute arch; + public static volatile SingularAttribute base; + public static volatile ListAttribute conflict; + +} + diff --git a/src/main/java/org/jpacrepo/model/PkgName_.java b/src/main/java/org/jpacrepo/model/PkgName_.java new file mode 100644 index 0000000..393d74d --- /dev/null +++ b/src/main/java/org/jpacrepo/model/PkgName_.java @@ -0,0 +1,14 @@ +package org.jpacrepo.model; + +import javax.annotation.Generated; +import javax.persistence.metamodel.SingularAttribute; +import javax.persistence.metamodel.StaticMetamodel; + +@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor") +@StaticMetamodel(PkgName.class) +public abstract class PkgName_ { + + public static volatile SingularAttribute id; + +} + diff --git a/src/main/java/org/jpacrepo/service/PacmanService.java b/src/main/java/org/jpacrepo/service/PacmanService.java index ac589ed..e82098d 100644 --- a/src/main/java/org/jpacrepo/service/PacmanService.java +++ b/src/main/java/org/jpacrepo/service/PacmanService.java @@ -1,14 +1,24 @@ package org.jpacrepo.service; -import javax.ejb.Remote; +import org.jpacrepo.model.PkgData; + +import javax.ejb.Local; +import java.util.List; +import java.util.Map; /** * Created by walter on 28/03/15. */ -@Remote +@Local public interface PacmanService { public void deletePackage(String filename) throws Exception; + public long countResults(String name, String version, String arch); + + public List searchPackage(String name, String version, String arch, int page, int pageSize); + + public List listProperty(String property, Map equalityConditions, Class cls); + } diff --git a/src/main/java/org/jpacrepo/service/PacmanServiceEJB.java b/src/main/java/org/jpacrepo/service/PacmanServiceEJB.java index fc77e3b..ba1d3ae 100644 --- a/src/main/java/org/jpacrepo/service/PacmanServiceEJB.java +++ b/src/main/java/org/jpacrepo/service/PacmanServiceEJB.java @@ -17,6 +17,10 @@ import javax.inject.Inject; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.TypedQuery; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; import javax.transaction.*; import java.io.File; import java.io.IOException; @@ -238,6 +242,135 @@ public class PacmanServiceEJB implements PacmanService throw new RuntimeException(e); } }); + } + + @Override + public long countResults(String name, String version, String arch) + { + CriteriaBuilder builder; + CriteriaQuery criteriaQuery; + Root entity; + + builder = em.getCriteriaBuilder(); + criteriaQuery = builder.createQuery(Long.class); + entity = criteriaQuery.from(PkgData.class); + Predicate finalPredicate=null, p; + + if(name != null && !name.isEmpty()) + { + p = builder.equal(entity.get("name").get("id"), name); + finalPredicate = p; + } + if(version != null && !version.isEmpty()) + { + p=builder.equal(entity.get("version"), version); + finalPredicate = finalPredicate != null ? builder.and(finalPredicate, p) : p; + } + if(arch != null && !arch.isEmpty()) + { + p=builder.equal(entity.get("arch"), arch); + finalPredicate = finalPredicate != null ? builder.and(finalPredicate, p) : p; + } + + if(finalPredicate != null) + { + criteriaQuery.select(builder.count(entity)).where(finalPredicate); + } + else + { + criteriaQuery.select(builder.count(entity)); + } + TypedQuery query = em.createQuery(criteriaQuery); + return em.createQuery(criteriaQuery).getSingleResult(); + } + + @Override + public List listProperty(String property, Map equalityConditions, Class cls) + { + CriteriaBuilder builder = em.getCriteriaBuilder(); + CriteriaQuery criteriaQuery = builder.createQuery(cls); + Root entity = criteriaQuery.from(PkgData.class); + + Predicate finalPredicate=null, p; + String key = equalityConditions.get("name"); + if(key != null && !key.isEmpty()) + { + p = builder.equal(entity.get("name").get("id"), key); + finalPredicate = p; + } + + key = equalityConditions.get("version"); + if(key != null && !key.isEmpty()) + { + p = builder.equal(entity.get("version"), key); + finalPredicate = p; + } + + key = equalityConditions.get("arch"); + if(key != null && !key.isEmpty()) + { + p = builder.equal(entity.get("arch"), key); + finalPredicate = p; + } + + if(finalPredicate != null) + { + criteriaQuery.select(entity.get(property)).distinct(true).where(finalPredicate); + } + else + { + criteriaQuery.select(entity.get(property)).distinct(true); + } + return em.createQuery(criteriaQuery).getResultList(); + } + + + @Override + public List searchPackage(String name, String version, String arch, int pageNumber, int pageSize) + { + CriteriaBuilder builder; + CriteriaQuery criteriaQuery; + Root entity; + + builder = em.getCriteriaBuilder(); + criteriaQuery = builder.createQuery(PkgData.class); + entity = criteriaQuery.from(PkgData.class); + Predicate finalPredicate=null, p; + + if(name != null && !name.isEmpty()) + { + p = builder.equal(entity.get("name").get("id"), name); + finalPredicate = p; + } + if(version != null && !version.isEmpty()) + { + p=builder.equal(entity.get("version"), version); + finalPredicate = finalPredicate != null ? builder.and(finalPredicate, p) : p; + } + if(arch != null && !arch.isEmpty()) + { + p=builder.equal(entity.get("arch"), arch); + finalPredicate = finalPredicate != null ? builder.and(finalPredicate, p) : p; + } + + if(finalPredicate != null) + { + criteriaQuery.select(entity).where(finalPredicate).orderBy(builder.asc(entity.get("fileName"))); + } + else + { + criteriaQuery.select(entity).orderBy(builder.asc(entity.get("fileName"))); + } + TypedQuery query = em.createQuery(criteriaQuery); + if(pageNumber>=0) + { + query.setFirstResult(pageNumber*pageSize); + } + if(pageSize>0) + { + query.setMaxResults(pageSize); + } + return query.getResultList(); } } \ No newline at end of file diff --git a/src/main/java/org/jpacrepo/util/Utility.java b/src/main/java/org/jpacrepo/util/Utility.java new file mode 100644 index 0000000..7985edf --- /dev/null +++ b/src/main/java/org/jpacrepo/util/Utility.java @@ -0,0 +1,35 @@ +package org.jpacrepo.util; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Created by walter on 06/06/15. + */ +public class Utility +{ + + public static String getTemplate(String filename) + { + return new String(readResource("/template/" + filename)); + } + + public static byte[] readResource(String resourcePath) + { + try + { + InputStream is = Utility.class.getClassLoader().getResourceAsStream(resourcePath); + byte[] res = new byte[is.available()]; + if(is.read(res,0, res.length)!= res.length) + { + throw new IOException("Uncompleted read"); + } + is.close(); + return res; + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/resources/WEB-INF/web.xml b/src/main/resources/WEB-INF/web.xml new file mode 100644 index 0000000..e39c8f0 --- /dev/null +++ b/src/main/resources/WEB-INF/web.xml @@ -0,0 +1,17 @@ + + + + eu.webtoolkit.jwt.ServletInit + + + jpacrepo + jpacrepo + + + + + + diff --git a/src/main/resources/template/jumbotron.html b/src/main/resources/template/jumbotron.html new file mode 100644 index 0000000..1e40846 --- /dev/null +++ b/src/main/resources/template/jumbotron.html @@ -0,0 +1,4 @@ +
+

JPacRepo

+

The personal archlinux package repository

+
diff --git a/src/main/resources/template/package-search-form.html b/src/main/resources/template/package-search-form.html new file mode 100644 index 0000000..4f527a1 --- /dev/null +++ b/src/main/resources/template/package-search-form.html @@ -0,0 +1,46 @@ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+
+ ${nameInput} +
+
+ ${versionInput} +
+
+ ${archInput} +
+
+ ${pageInput} +
+
+ ${searchButton} +
+
+
+
diff --git a/src/main/webapp/css/jpacrepo-web.css b/src/main/webapp/css/jpacrepo-web.css new file mode 100644 index 0000000..36cb492 --- /dev/null +++ b/src/main/webapp/css/jpacrepo-web.css @@ -0,0 +1,28 @@ +div.input-control { + margin-bottom: 2%; + background-color: #ccc; + padding: 1% 2%; + border-radius: 4px; +} + +html body.Wt-ltr div.Wt-domRoot div div.container div.row table.table-bordered.table-condensed.table-striped tbody tr td { + text-align: center; +} +html body.Wt-ltr div.Wt-domRoot div div.container div.row table.table-bordered.table-condensed.table-striped thead tr th { + text-align: center; +} + +div.search-input form.form-inline div.form-group +{ + text-align: center; +} + +div.container.main-container +{ + text-align: center; +} + +td a img.download-icon +{ + margin-right: 5px; +} \ No newline at end of file diff --git a/src/main/webapp/img/download.png b/src/main/webapp/img/download.png new file mode 100644 index 0000000000000000000000000000000000000000..9a2d9fec84bdd9a63ce9c96791021da3c3a3ca39 GIT binary patch literal 5834 zcmcIoc{r5a`@f&ZFf$TUN@$tsO;gG;B1&ooZ9=4a(;_2l5^X|a9@6r@uccCmWZp!p zMj=u$?IBCAGAUbODEpFa#?0@i_xro9?{)qD{XKJCbDwjbdpn=!KKHrL`NMJJy2;Ag z$^bCgX8md>030=OpeP|S82o+=5v?O@oR4e{@H!IYcE}SfJp%T4GHmv{d3!o}x_N{g zEb+7e(D1WaZMh}*Q)j)=(YoIgl$sWNdg@^<5IWXeDw$5%9r0~?!G-eG3HM8qpWqMD zsk$fClhquUWZs7%QsI{lVRzS41T>s6cNtBwns>U&nS%_&@vba+sC>+tKB3TLMx zFaE!upinO=U%4ww{zN3|CggH~q_E%zZ|9DqG$6h#&0iF{N!FdfWKW+lU&o`Tw6Xhl z42SHanGF|;8ynvB?z=xnsUfkdDucaI12DMdH`tgpZbn}~S*{{wY7zj4{fEqAR>}A4 zm&EJa(h8_bRu~0XTKjL#*gH}lbo{%Q@z+OFRTC0OB+z``y(1?e-bglW*9FD_b>+MI zRK(h%B~#Ou`Qc9mOcDcxZN4?y;VIFBZJmWtL<*;B`&1fm>&v~Kq+M>^#-1IVvdZ6p zgh8a~cwMiWQ|fU2K(@u?W%>~uO%8xP-3>@^!*a8BQQxY#gNH9QnbaBsYeh(N!&N~} z(W-?%UYIX_eJ+L%>WP)TagxG+9=|67x5h43*CD7^y3+P-@r^P!n+_lEW(wwcOML&@ z9Fgu}tSj{SRJ75AaWOM*8~?`#(#PmEu8GMp$p>5#nx`=<_Y-z5MHOR|jX0S$jr+Y{@=vAIXK~)S8LC~Qe_)wn zadgt{5W^&-7=G= z_f8C8z1l91$RKQ%>st!voty%iyFQEl$O%sxG--P65df^F?ZxD=g{_!X642Hc^^7cg zDAyE=f6FqU?s8Tp!79=1LrmCSSy+a2R_FI~R*Xy}uL3&O2{3DSxu6Tga2k0qA{gd%w zJ{E*RiIls(d&a3Z{Uk=?{N7hi6nJrNrhIW3T>G#;QlNhVIQ6@F$lZHf3E;d;&Mj={ zxfw2W=?^C!8}f6e;qc>)y_@fF|D9vn+Hydp&4?jAlU|pJm4~RMW zRAumy5i|k_u^I&}k?7J%VT4g&$zM?kd>(a&GcUqzdAzeL)B~t{~rpO zSsnY>AsY!0a?2a7!-X^ z*^Dd2*FFpm@bphoIjbM`eeP3REv^8JF-tI{*sJV@F*H$QkK zPaK5L6FjF1m1-4OvC?C%qH%1_b(`IEKim-u7)ePq2(hurU0dyZs-$1c@#+wV%hh~M z1KYDFw^kXkzxAJbKl#%=(##*CmdQMD$lDgaZVGs1*x%kK2aEdK9twHDnit{`xJL_= z4xZnoOarg4AD=3-+HEeHT~~(qptBZjbYZ}4U}WGdO7&KZdp~y8Wth3L#QeSrE_|OG zrjI7HR{#7XoJ9PQ;grrJ4i-#ffZf_6hrD|#knVH7?G6Rhn{2j1^T0JE-{H3&>y<19 zQqFqQbnvrH@hj4R2g0s}P@RN-q%DN#?*dhG!sc>x?dkatooREFkK3JGagRj2D?qOF z+y|Onil0=W_p=_XzXT&;8u0zW`n$%M-PZUzjfLPxq-H{Oo2ov>J#h3A{?=W&$pE#h za^z|H3PL@ppE3hfLWX*{cv7WJiY@_7opj+ez=XAILKko%hXrrX@%IY6)yf zU%VOYpgXThiruQ#2~alq#mIo{yImb`5krRrB|u9k=(-aH@cqaqQ5@`ebp%aCz zW#BTnCY#zjuTo0x4Gq9)(PTNi`0|v)1o2994L6b)vr^>nRr!flZ$}fosGL{#ptB>@lhVAVzgN({AmX9g<9pNk zS{BH|9DTD?!}P6|A9;IPkR8X1dran^lVZ>IZLXRjT%!u)*ig7Sbm9%xwBWS`Y?4^M zhwMa)5i%iMIoB5u2AuXH+EOTx{cIRPi;+XKGns22AL*iWMiZjt7^RW^n-Vnh;MwqL zA54g5DKXKl@+5pPe&BHhzQc=2a955fX%g7?_tIgGhS=%gHK;Pbi&YtZRV3)Ms-b-b z6M&1;nx5AM+9~u>9!;_0TtfxN*xGDp8e{*YXV)$Fmn$?K?#w7PoV|_$S}{<#?IhViu^MD5^}5M zq+4DW<})h=+Ko}2`0w9RR4X#L2#9%#oHQde=kDG>Px%!Wx~^-xy=#EJwI=&VUbf2W zPUAd9FvP#wKIz%FSL>ONKDJ2`Z3{ph>AM9!-af<(# z0S2?;q1~(x@%+o`=n2ka%dn~dJ0yV2+&~dH9bv)bLNAK}gO><2NYR9b(e+rBH)0|H z;}4v}`ISgJzq0Pjc2BxpwJ4+c@D{;lmweZwksb@}Z|>;)5!m8hf1@o=y4K=$@AJ{7 zky%@d=8GjQS}LrQVqT5tZbp?^dZFH2PRzD#t*^-+cMlZz@67GH<@;<`vkRm)M!xGx zFjkujmq$f+ZpnTF28j^If#b>H*iCc6gg2NNj0wF=el-bhqhXDE#js5HoiYF-@fuxOvJJQ?_0(8 zjqgX)KHrp{sJ0V7Z=IOh6pYL2nf1sDZT#)?`f^M17Kh^8x3oHNB;Woj3gYLCD0K-nxb;_Uf;|iVoNoFVFFGfC}k5Hi=k}6Jbq3CBS%d0!3un zKcl;M@%N z?GqZpn$y=0n;kRcxO7rE1K#n)hK=uKadp(eh-5rz;^Tx*VZlf8&+inzX>}o~YB2Cy z!QJy^n2${5TZn5aI3^zU{%c}Z77Y2R&UGWFyGasmqxpl|t%o3)Jr+e3o^Y)>{5szT zvDCSIgqxqFt<=ff3{JmHuBzR_sT?tD(O2q+8?C-g(}>sNJIWf zE&001aH$L?jNIvP;`i|$s1Kevs>M+K$vwjkR*f!s^o!ffDn>q!OkA*-wthnUMV6X; zd%pUJ!muMhc$X??U>%!<_4-3R`kC;;XMC-dA78|K%D!)sSp?2IG)lG)+I_FyIJ{j74vh7dbk z?g{=_&SL`4LbJhH$qqN+Sp};mn6g_O$RB_>X6p0-6QRnAIL2!iiN_AZs3UZH<}%B4fg`I+K?PJzGgm&I;4 z+MI3BTDd%aT5KwO6k^L5KKK`39M>4APL%RmR%%k>k@T@5!56yJ9q%1E@v=F>h*~AR z`P?wMn2M?bG)Z=a%m`yC3UxRGuWO5`kO@g7AK5aA8HS1nBu@91Gtpo;mxTlm4h0~a z|BBgz9rhnFq{}KDK@gY2QzwpvS5?Lbk+@S)FnhYVN^r2=sS{A`)bLvYis!8G(wTBQ zcz|kcBkBQh<&jF+lRgLdnDjuZ89_rL`lFJ(TvUTGQ!g^WiV#Rc!no<^95C(I zK+8Lxx0dGfP4{cp)@l~q6bC#{8@p`YpJ#+=#-CPjo7(g?ZSV0oi^0?H^mPI}QP=fh zS0#zTocYV-QN8#W9xhDa6;$XeOju|iinsOgt94s)YO?{^B5!o2J@yi{_Kx*v*-zjyi2@+PD5#*eNm^J4R~)d}5xKd{3ukLJ|& zM1{)8_uZ*V>}9Syl$2C1cu%)SAyM?+w!D$V{l0O`JX>V~YZY;>fR2tltkcG3i8u|h zoLV7^ktq_jKJkx!^|>N;PIFAeqW*g9sAO@#m@!7V{lI=#5(yvs9uhT9&|*Z1sFWesh!{;)XfIf<{ad*)l_k-g@XtAoUqjtM3E!WD@>3znX> zzw|87sXXoPf>sB(;gnLRy*_NN@H0#6{7%`88n;9H9rkBd}OHPz3X)P&Wm>e&(-@?4I+83KL7fZ7YD@LL)Cjj7u`n-J^Yi|CRU z_g*Q*W`&iZ1FOl=Jqm~S%esHe02+=K0Y+Ru1p=Cq36De&kF7M5mI#KX|N9E6FC1gISM)FN^}wGV$^kXL%^}VQS&fL7(6f>W zy@t!-vW^7jn~wUiH zEF=gW2NT)7ibu{)vsXGDhncBL!Cqh&7b-z_7cVWTcqW!T zo+C?~4-joW`9S&Bn_U-Zu~gXkA%TANm6LE`;h~!1+-bsTnb_%yHy1ty&o=KMXD(zL zHoc9@bvhy1-fyAGRniD^8GZNr_rE%B9Zv(V=L%65`Z;uFDUe>h9(;6pXx?Z(! zysNh^0?#s~P9N01ov$C~yhMV`W|s{A?@Jz5#8JVrr6YMX@Hu;dXxSf&*)Mw%bC0!3Ysr|dl=^UFjb9l$28>2+2r3-bsP#wn)dO(S8m96;Oy@hlWjV*SDCx7e^ta9 z*S6&I+J9G+^?A9|g;6cT8T1t88MR>$F;b7}p`CX~Y4pnsNM|m_~0a-50iWUK}$BtK0RaTF>^mJWHB)P>lph zE(?8XBA=G?a?Z~T&97-+Q9g$RXQrwY>C3}5=Y6aB{$#BhawT6J%jvx;txx?(nrqgV zQXbUP?K6@Zr%nQHFuE{ZIq3DGV@1XL#Dv+XT>k%kZZyazvEXZj*|~t7dqyT~*{s>P K`mPl>@_zsxQPs!* literal 0 HcmV?d00001 diff --git a/src/main/webapp/img/download.svg b/src/main/webapp/img/download.svg new file mode 100644 index 0000000..377ae33 --- /dev/null +++ b/src/main/webapp/img/download.svg @@ -0,0 +1,15 @@ + + + + + + + + +