From 5f8b151629c59312feaf5bed68cfbed3bbd71fac Mon Sep 17 00:00:00 2001 From: emmanue1 <emmanue1@users.noreply.github.com> Date: Wed, 13 May 2015 19:27:20 +0200 Subject: [PATCH] Adds support for WAR files --- .../model/container/GenericContainer.groovy | 4 +- .../gui/model/container/WarContainer.groovy | 22 +++ .../GenericContainerFactoryProvider.groovy | 3 +- .../JarContainerFactoryProvider.groovy | 29 +-- .../WarContainerFactoryProvider.groovy | 37 ++++ .../AbstractFileLoaderProvider.groovy | 2 +- .../fileloader/WarFileLoaderProvider.groovy | 13 ++ .../indexer/TextFileIndexerProvider.groovy | 4 +- .../indexer/WebXmlFileIndexerProvider.groovy | 40 ++++ .../indexer/ZipFileIndexerProvider.groovy | 2 +- .../PackageSourceSaverProvider.groovy | 2 +- .../ZipFileSourceSaverProvider.groovy | 4 +- .../CssFileTreeNodeFactoryProvider.groovy | 42 ++++ .../HtmlFileTreeNodeFactoryProvider.groovy | 2 +- .../JspFileTreeNodeFactoryProvider.groovy | 42 ++++ ...ManifestFileTreeNodeFactoryProvider.groovy | 5 +- ...infDirectoryTreeNodeFactoryProvider.groovy | 10 +- .../TextFileTreeNodeFactoryProvider.groovy | 2 +- .../WarFileTreeNodeFactoryProvider.groovy | 29 +++ .../WarPackageTreeNodeFactoryProvider.groovy | 16 ++ .../WebXmlFileTreeNodeFactoryProvider.groovy | 38 ++++ ...sesDirectoryTreeNodeFactoryProvider.groovy | 18 ++ ...LibDirectoryTreeNodeFactoryProvider.groovy | 17 ++ .../XmlFileTreeNodeFactoryProvider.groovy | 4 +- .../gui/view/component/WebXmlFilePage.groovy | 187 ++++++++++++++++++ .../gui/util/xml/AbstractXmlPathFinder.java | 86 ++++++++ .../services/jd.gui.spi.ContainerFactory | 2 + .../META-INF/services/jd.gui.spi.FileLoader | 1 + .../META-INF/services/jd.gui.spi.Indexer | 1 + .../services/jd.gui.spi.TreeNodeFactory | 8 +- .../resources/images/archivefolder_obj.png | Bin 0 -> 3017 bytes .../src/main/resources/images/css_obj.png | Bin 0 -> 2972 bytes .../src/main/resources/images/inf_obj.png | Bin 0 -> 2954 bytes .../resources/images/packagefolder_obj.png | Bin 0 -> 421 bytes 34 files changed, 632 insertions(+), 40 deletions(-) create mode 100644 services/src/main/groovy/jd/gui/model/container/WarContainer.groovy create mode 100644 services/src/main/groovy/jd/gui/service/container/WarContainerFactoryProvider.groovy create mode 100644 services/src/main/groovy/jd/gui/service/fileloader/WarFileLoaderProvider.groovy create mode 100644 services/src/main/groovy/jd/gui/service/indexer/WebXmlFileIndexerProvider.groovy create mode 100644 services/src/main/groovy/jd/gui/service/treenode/CssFileTreeNodeFactoryProvider.groovy create mode 100644 services/src/main/groovy/jd/gui/service/treenode/JspFileTreeNodeFactoryProvider.groovy create mode 100644 services/src/main/groovy/jd/gui/service/treenode/WarFileTreeNodeFactoryProvider.groovy create mode 100644 services/src/main/groovy/jd/gui/service/treenode/WarPackageTreeNodeFactoryProvider.groovy create mode 100644 services/src/main/groovy/jd/gui/service/treenode/WebXmlFileTreeNodeFactoryProvider.groovy create mode 100644 services/src/main/groovy/jd/gui/service/treenode/WebinfClassesDirectoryTreeNodeFactoryProvider.groovy create mode 100644 services/src/main/groovy/jd/gui/service/treenode/WebinfLibDirectoryTreeNodeFactoryProvider.groovy create mode 100644 services/src/main/groovy/jd/gui/view/component/WebXmlFilePage.groovy create mode 100644 services/src/main/java/jd/gui/util/xml/AbstractXmlPathFinder.java create mode 100644 services/src/main/resources/images/archivefolder_obj.png create mode 100644 services/src/main/resources/images/css_obj.png create mode 100644 services/src/main/resources/images/inf_obj.png create mode 100644 services/src/main/resources/images/packagefolder_obj.png diff --git a/services/src/main/groovy/jd/gui/model/container/GenericContainer.groovy b/services/src/main/groovy/jd/gui/model/container/GenericContainer.groovy index 365cb254..90b4f394 100644 --- a/services/src/main/groovy/jd/gui/model/container/GenericContainer.groovy +++ b/services/src/main/groovy/jd/gui/model/container/GenericContainer.groovy @@ -114,7 +114,7 @@ class GenericContainer implements Container { } protected Collection<Container.Entry> loadChildrenFromFileEntry() { - def tmpFile = File.createTempFile('jd-gui.', '.tmp.zip') + def tmpFile = File.createTempFile('jd-gui.', '.tmp.' + fsPath.fileName.toString()) def tmpPath = Paths.get(tmpFile.toURI()) tmpFile.withOutputStream { OutputStream os -> @@ -130,7 +130,7 @@ class GenericContainer implements Container { tmpFile.deleteOnExit() def rootPath = rootDirectories.next() - def container = api.getContainerFactory(subFileSystem)?.make(api, this, rootPath) + def container = api.getContainerFactory(rootPath)?.make(api, this, rootPath) if (container) { return container.root.children } diff --git a/services/src/main/groovy/jd/gui/model/container/WarContainer.groovy b/services/src/main/groovy/jd/gui/model/container/WarContainer.groovy new file mode 100644 index 00000000..9e6c944d --- /dev/null +++ b/services/src/main/groovy/jd/gui/model/container/WarContainer.groovy @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2008-2015 Emmanuel Dupuy + * This program is made available under the terms of the GPLv3 License. + */ + +package jd.gui.model.container + +import groovy.transform.CompileStatic +import jd.gui.api.API +import jd.gui.api.model.Container + +import java.nio.file.Path + +@CompileStatic +class WarContainer extends GenericContainer { + + WarContainer(API api, Container.Entry parentEntry, Path rootPath) { + super(api, parentEntry, rootPath) + } + + String getType() { 'war' } +} diff --git a/services/src/main/groovy/jd/gui/service/container/GenericContainerFactoryProvider.groovy b/services/src/main/groovy/jd/gui/service/container/GenericContainerFactoryProvider.groovy index eb15f726..816e2453 100644 --- a/services/src/main/groovy/jd/gui/service/container/GenericContainerFactoryProvider.groovy +++ b/services/src/main/groovy/jd/gui/service/container/GenericContainerFactoryProvider.groovy @@ -10,14 +10,13 @@ import jd.gui.api.model.Container import jd.gui.model.container.GenericContainer import jd.gui.spi.ContainerFactory -import java.nio.file.FileSystem import java.nio.file.Path class GenericContainerFactoryProvider implements ContainerFactory { String getType() { 'generic' } - boolean accept(API api, FileSystem fileSystem) { true } + boolean accept(API api, Path rootPath) { true } Container make(API api, Container.Entry parentEntry, Path rootPath) { return new GenericContainer(api, parentEntry, rootPath) diff --git a/services/src/main/groovy/jd/gui/service/container/JarContainerFactoryProvider.groovy b/services/src/main/groovy/jd/gui/service/container/JarContainerFactoryProvider.groovy index 16e75e02..ca1b4515 100644 --- a/services/src/main/groovy/jd/gui/service/container/JarContainerFactoryProvider.groovy +++ b/services/src/main/groovy/jd/gui/service/container/JarContainerFactoryProvider.groovy @@ -10,7 +10,6 @@ import jd.gui.api.model.Container import jd.gui.model.container.JarContainer import jd.gui.spi.ContainerFactory -import java.nio.file.FileSystem import java.nio.file.Files import java.nio.file.InvalidPathException import java.nio.file.Path @@ -19,25 +18,17 @@ class JarContainerFactoryProvider implements ContainerFactory { String getType() { 'jar' } - boolean accept(API api, FileSystem fileSystem) { - def rootDirectories = fileSystem.rootDirectories.iterator() - - if (rootDirectories.hasNext()) { - def rootPath = rootDirectories.next() - - if (rootPath.toUri().toString().toLowerCase().endsWith('.jar!/')) { - // Specification: http://docs.oracle.com/javase/6/docs/technotes/guides/jar/jar.html - return true - } else { - // Extension: accept uncompressed JAR file containing a folder 'META-INF' - try { - return Files.exists(fileSystem.getPath('/META-INF')) - } catch (InvalidPathException e) { - return false - } - } + boolean accept(API api, Path rootPath) { + if (rootPath.toUri().toString().toLowerCase().endsWith('.jar!/')) { + // Specification: http://docs.oracle.com/javase/6/docs/technotes/guides/jar/jar.html + return true } else { - return false + // Extension: accept uncompressed JAR file containing a folder 'META-INF' + try { + return rootPath.fileSystem.provider().scheme.equals('file') && Files.exists(rootPath.resolve('META-INF')) + } catch (InvalidPathException e) { + return false + } } } diff --git a/services/src/main/groovy/jd/gui/service/container/WarContainerFactoryProvider.groovy b/services/src/main/groovy/jd/gui/service/container/WarContainerFactoryProvider.groovy new file mode 100644 index 00000000..d1466f36 --- /dev/null +++ b/services/src/main/groovy/jd/gui/service/container/WarContainerFactoryProvider.groovy @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2008-2015 Emmanuel Dupuy + * This program is made available under the terms of the GPLv3 License. + */ + +package jd.gui.service.container + +import jd.gui.api.API +import jd.gui.api.model.Container +import jd.gui.model.container.WarContainer +import jd.gui.spi.ContainerFactory + +import java.nio.file.Files +import java.nio.file.InvalidPathException +import java.nio.file.Path + +class WarContainerFactoryProvider implements ContainerFactory { + + String getType() { 'war' } + + boolean accept(API api, Path rootPath) { + if (rootPath.toUri().toString().toLowerCase().endsWith('.war!/')) { + return true + } else { + // Extension: accept uncompressed JAR file containing a folder 'WEB-INF' + try { + return rootPath.fileSystem.provider().scheme.equals('file') && Files.exists(rootPath.resolve('WEB-INF')) + } catch (InvalidPathException e) { + return false + } + } + } + + Container make(API api, Container.Entry parentEntry, Path rootPath) { + return new WarContainer(api, parentEntry, rootPath) + } +} diff --git a/services/src/main/groovy/jd/gui/service/fileloader/AbstractFileLoaderProvider.groovy b/services/src/main/groovy/jd/gui/service/fileloader/AbstractFileLoaderProvider.groovy index 9b44b1b6..4a53f668 100644 --- a/services/src/main/groovy/jd/gui/service/fileloader/AbstractFileLoaderProvider.groovy +++ b/services/src/main/groovy/jd/gui/service/fileloader/AbstractFileLoaderProvider.groovy @@ -34,7 +34,7 @@ abstract class AbstractFileLoaderProvider implements FileLoader { InputStream getInputStream() { null } Collection<Container.Entry> getChildren() { children } } - def container = api.getContainerFactory(rootPath.fileSystem)?.make(api, parentEntry, rootPath) + def container = api.getContainerFactory(rootPath)?.make(api, parentEntry, rootPath) if (container) { parentEntry.children = container.root.children diff --git a/services/src/main/groovy/jd/gui/service/fileloader/WarFileLoaderProvider.groovy b/services/src/main/groovy/jd/gui/service/fileloader/WarFileLoaderProvider.groovy new file mode 100644 index 00000000..d7241e1b --- /dev/null +++ b/services/src/main/groovy/jd/gui/service/fileloader/WarFileLoaderProvider.groovy @@ -0,0 +1,13 @@ +package jd.gui.service.fileloader + +import jd.gui.api.API + +class WarFileLoaderProvider extends ZipFileLoaderProvider { + + String[] getExtensions() { ['war'] } + String getDescription() { 'War files (*.war)' } + + boolean accept(API api, File file) { + return file.exists() && file.canRead() && file.name.toLowerCase().endsWith('.war') + } +} diff --git a/services/src/main/groovy/jd/gui/service/indexer/TextFileIndexerProvider.groovy b/services/src/main/groovy/jd/gui/service/indexer/TextFileIndexerProvider.groovy index db19f3c3..bdd43d67 100644 --- a/services/src/main/groovy/jd/gui/service/indexer/TextFileIndexerProvider.groovy +++ b/services/src/main/groovy/jd/gui/service/indexer/TextFileIndexerProvider.groovy @@ -15,8 +15,8 @@ import java.util.regex.Pattern class TextFileIndexerProvider implements Indexer { String[] getTypes() { [ - '*:file:*.txt', '*:file:*.html', '*:file:*.js', '*:file:*.jsp', '*:file:*.xml', - '*:file:*.xsl', '*:file:*.xslt', '*:file:*.xsd', '*:file:*.properties', '*:file:*.sql'] } + '*:file:*.txt', '*:file:*.html', '*:file:*.xhtml', '*:file:*.js', '*:file:*.jsp', '*:file:*.jspf', + '*:file:*.xml', '*:file:*.xsl', '*:file:*.xslt', '*:file:*.xsd', '*:file:*.properties', '*:file:*.sql'] } Pattern getPathPattern() { null } diff --git a/services/src/main/groovy/jd/gui/service/indexer/WebXmlFileIndexerProvider.groovy b/services/src/main/groovy/jd/gui/service/indexer/WebXmlFileIndexerProvider.groovy new file mode 100644 index 00000000..d4c51fe6 --- /dev/null +++ b/services/src/main/groovy/jd/gui/service/indexer/WebXmlFileIndexerProvider.groovy @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2008-2015 Emmanuel Dupuy + * This program is made available under the terms of the GPLv3 License. + */ + +package jd.gui.service.indexer + +import jd.gui.api.API +import jd.gui.api.model.Container +import jd.gui.api.model.Indexes +import jd.gui.util.xml.AbstractXmlPathFinder + +class WebXmlFileIndexerProvider extends XmlFileIndexerProvider { + String[] getTypes() { ['*:file:WEB-INF/web.xml'] } + + void index(API api, Container.Entry entry, Indexes indexes) { + super.index(api, entry, indexes) + + new WebXmlPathFinder(entry, indexes).find(entry.inputStream.text) + } + + static class WebXmlPathFinder extends AbstractXmlPathFinder { + Container.Entry entry + Map<String, Collection> index + + WebXmlPathFinder(Container.Entry entry, Indexes indexes) { + super([ + 'web-app/filter/filter-class', + 'web-app/listener/listener-class', + 'web-app/servlet/servlet-class' + ]) + this.entry = entry + this.index = indexes.getIndex('typeReferences'); + } + + void handle(String path, String text, int position) { + index.get(text.replace('.', '/')).add(entry); + } + } +} diff --git a/services/src/main/groovy/jd/gui/service/indexer/ZipFileIndexerProvider.groovy b/services/src/main/groovy/jd/gui/service/indexer/ZipFileIndexerProvider.groovy index 70ee268f..c6320edc 100644 --- a/services/src/main/groovy/jd/gui/service/indexer/ZipFileIndexerProvider.groovy +++ b/services/src/main/groovy/jd/gui/service/indexer/ZipFileIndexerProvider.groovy @@ -14,7 +14,7 @@ import jd.gui.spi.Indexer import java.util.regex.Pattern class ZipFileIndexerProvider implements Indexer { - String[] getTypes() { ['*:file:*.zip', '*:file:*.jar'] } + String[] getTypes() { ['*:file:*.zip', '*:file:*.jar', '*:file:*.war'] } Pattern getPathPattern() { null } diff --git a/services/src/main/groovy/jd/gui/service/sourcesaver/PackageSourceSaverProvider.groovy b/services/src/main/groovy/jd/gui/service/sourcesaver/PackageSourceSaverProvider.groovy index 656bd995..c9be02ec 100644 --- a/services/src/main/groovy/jd/gui/service/sourcesaver/PackageSourceSaverProvider.groovy +++ b/services/src/main/groovy/jd/gui/service/sourcesaver/PackageSourceSaverProvider.groovy @@ -13,7 +13,7 @@ import jd.gui.util.JarContainerEntryUtil import java.nio.file.Path class PackageSourceSaverProvider extends DirectorySourceSaverProvider { - String[] getTypes() { ['jar:dir:*'] } + String[] getTypes() { ['jar:dir:*', 'war:dir:*'] } void save(API api, SourceSaver.Controller controller, SourceSaver.Listener listener, Path path, Container.Entry entry) { save(api, controller, listener, path, JarContainerEntryUtil.removeInnerTypeEntries(entry.children)) diff --git a/services/src/main/groovy/jd/gui/service/sourcesaver/ZipFileSourceSaverProvider.groovy b/services/src/main/groovy/jd/gui/service/sourcesaver/ZipFileSourceSaverProvider.groovy index 4c643d9f..f817ee3d 100644 --- a/services/src/main/groovy/jd/gui/service/sourcesaver/ZipFileSourceSaverProvider.groovy +++ b/services/src/main/groovy/jd/gui/service/sourcesaver/ZipFileSourceSaverProvider.groovy @@ -17,11 +17,11 @@ import java.nio.file.Paths class ZipFileSourceSaverProvider extends DirectorySourceSaverProvider { - String[] getTypes() { ['*:file:*.zip', '*:file:*.jar'] } + String[] getTypes() { ['*:file:*.zip', '*:file:*.jar', '*:file:*.war'] } String getSourcePath(Container.Entry entry) { def path = entry.path - return path.substring(0, path.length()-3) + 'src.zip' + return path + '.src.zip' } @CompileStatic diff --git a/services/src/main/groovy/jd/gui/service/treenode/CssFileTreeNodeFactoryProvider.groovy b/services/src/main/groovy/jd/gui/service/treenode/CssFileTreeNodeFactoryProvider.groovy new file mode 100644 index 00000000..87e460d6 --- /dev/null +++ b/services/src/main/groovy/jd/gui/service/treenode/CssFileTreeNodeFactoryProvider.groovy @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2008-2015 Emmanuel Dupuy + * This program is made available under the terms of the GPLv3 License. + */ + +package jd.gui.service.treenode + +import jd.gui.api.API +import jd.gui.api.feature.UriGettable +import jd.gui.api.model.Container +import jd.gui.view.data.TreeNodeBean +import org.fife.ui.rsyntaxtextarea.SyntaxConstants + +import javax.swing.ImageIcon +import javax.swing.JComponent +import javax.swing.tree.DefaultMutableTreeNode + +class CssFileTreeNodeFactoryProvider extends TextFileTreeNodeFactoryProvider { + static final ImageIcon icon = new ImageIcon(HtmlFileTreeNodeFactoryProvider.class.classLoader.getResource('images/css_obj.png')) + + String[] getTypes() { ['*:file:*.css'] } + + public <T extends DefaultMutableTreeNode & UriGettable> T make(API api, Container.Entry entry) { + int lastSlashIndex = entry.path.lastIndexOf('/') + def name = entry.path.substring(lastSlashIndex+1) + return new TreeNode(entry, new TreeNodeBean(label:name, icon:icon, tip:"Location: $entry.uri.path")) + } + + static class TreeNode extends TextFileTreeNodeFactoryProvider.TreeNode { + TreeNode(Container.Entry entry, Object userObject) { + super(entry, userObject) + } + + public <T extends JComponent & UriGettable> T createPage(API api) { + return new TextFileTreeNodeFactoryProvider.Page(entry) { + String getSyntaxStyle() { + SyntaxConstants.SYNTAX_STYLE_CSS + } + } + } + } +} \ No newline at end of file diff --git a/services/src/main/groovy/jd/gui/service/treenode/HtmlFileTreeNodeFactoryProvider.groovy b/services/src/main/groovy/jd/gui/service/treenode/HtmlFileTreeNodeFactoryProvider.groovy index 9f44da45..895e54b6 100644 --- a/services/src/main/groovy/jd/gui/service/treenode/HtmlFileTreeNodeFactoryProvider.groovy +++ b/services/src/main/groovy/jd/gui/service/treenode/HtmlFileTreeNodeFactoryProvider.groovy @@ -17,7 +17,7 @@ import javax.swing.tree.DefaultMutableTreeNode class HtmlFileTreeNodeFactoryProvider extends TextFileTreeNodeFactoryProvider { static final ImageIcon icon = new ImageIcon(HtmlFileTreeNodeFactoryProvider.class.classLoader.getResource('images/html_obj.gif')) - String[] getTypes() { ['*:file:*.html'] } + String[] getTypes() { ['*:file:*.html', '*:file:*.xhtml'] } public <T extends DefaultMutableTreeNode & UriGettable> T make(API api, Container.Entry entry) { int lastSlashIndex = entry.path.lastIndexOf('/') diff --git a/services/src/main/groovy/jd/gui/service/treenode/JspFileTreeNodeFactoryProvider.groovy b/services/src/main/groovy/jd/gui/service/treenode/JspFileTreeNodeFactoryProvider.groovy new file mode 100644 index 00000000..1b95abfb --- /dev/null +++ b/services/src/main/groovy/jd/gui/service/treenode/JspFileTreeNodeFactoryProvider.groovy @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2008-2015 Emmanuel Dupuy + * This program is made available under the terms of the GPLv3 License. + */ + +package jd.gui.service.treenode + +import jd.gui.api.API +import jd.gui.api.feature.UriGettable +import jd.gui.api.model.Container +import jd.gui.view.data.TreeNodeBean +import org.fife.ui.rsyntaxtextarea.SyntaxConstants + +import javax.swing.ImageIcon +import javax.swing.JComponent +import javax.swing.tree.DefaultMutableTreeNode + +class JspFileTreeNodeFactoryProvider extends TextFileTreeNodeFactoryProvider { + static final ImageIcon icon = new ImageIcon(HtmlFileTreeNodeFactoryProvider.class.classLoader.getResource('images/html_obj.gif')) + + String[] getTypes() { ['*:file:*.jsp', '*:file:*.jspf'] } + + public <T extends DefaultMutableTreeNode & UriGettable> T make(API api, Container.Entry entry) { + int lastSlashIndex = entry.path.lastIndexOf('/') + def name = entry.path.substring(lastSlashIndex+1) + return new TreeNode(entry, new TreeNodeBean(label:name, icon:icon, tip:"Location: $entry.uri.path")) + } + + static class TreeNode extends TextFileTreeNodeFactoryProvider.TreeNode { + TreeNode(Container.Entry entry, Object userObject) { + super(entry, userObject) + } + + public <T extends JComponent & UriGettable> T createPage(API api) { + return new TextFileTreeNodeFactoryProvider.Page(entry) { + String getSyntaxStyle() { + SyntaxConstants.SYNTAX_STYLE_JSP + } + } + } + } +} diff --git a/services/src/main/groovy/jd/gui/service/treenode/ManifestFileTreeNodeFactoryProvider.groovy b/services/src/main/groovy/jd/gui/service/treenode/ManifestFileTreeNodeFactoryProvider.groovy index 6b4d4a8d..bda5ed70 100644 --- a/services/src/main/groovy/jd/gui/service/treenode/ManifestFileTreeNodeFactoryProvider.groovy +++ b/services/src/main/groovy/jd/gui/service/treenode/ManifestFileTreeNodeFactoryProvider.groovy @@ -16,13 +16,12 @@ import javax.swing.* import javax.swing.tree.DefaultMutableTreeNode class ManifestFileTreeNodeFactoryProvider extends FileTreeNodeFactoryProvider { - static - final ImageIcon icon = new ImageIcon(ManifestFileTreeNodeFactoryProvider.class.classLoader.getResource('images/manifest_obj.png')) + static final ImageIcon icon = new ImageIcon(ManifestFileTreeNodeFactoryProvider.class.classLoader.getResource('images/manifest_obj.png')) String[] getTypes() { ['*:file:META-INF/MANIFEST.MF'] } public <T extends DefaultMutableTreeNode & UriGettable> T make(API api, Container.Entry entry) { - return new TreeNode(entry, new TreeNodeBean(label: 'MANIFEST.MF', icon: icon, tip:"Location: $entry.uri.path")) + return new TreeNode(entry, new TreeNodeBean(label:'MANIFEST.MF', icon:icon, tip:"Location: $entry.uri.path")) } static class TreeNode extends FileTreeNodeFactoryProvider.TreeNode implements PageCreator { diff --git a/services/src/main/groovy/jd/gui/service/treenode/MetainfDirectoryTreeNodeFactoryProvider.groovy b/services/src/main/groovy/jd/gui/service/treenode/MetainfDirectoryTreeNodeFactoryProvider.groovy index 767f1524..7d612d94 100644 --- a/services/src/main/groovy/jd/gui/service/treenode/MetainfDirectoryTreeNodeFactoryProvider.groovy +++ b/services/src/main/groovy/jd/gui/service/treenode/MetainfDirectoryTreeNodeFactoryProvider.groovy @@ -5,12 +5,18 @@ package jd.gui.service.treenode +import javax.swing.ImageIcon import java.util.regex.Pattern class MetainfDirectoryTreeNodeFactoryProvider extends DirectoryTreeNodeFactoryProvider { - Pattern pattern = ~/META-IN(F|F\/.*)/ + static final ImageIcon icon = new ImageIcon(MetainfDirectoryTreeNodeFactoryProvider.class.classLoader.getResource('images/inf_obj.png')) - String[] getTypes() { ['jar:dir:*'] } + Pattern pattern = ~/(WEB-INF|(WEB-INF\/classes\/)?META-IN(F|F\/.*))/ + + String[] getTypes() { ['jar:dir:*', 'war:dir:*'] } Pattern getPathPattern() { pattern } + + ImageIcon getIcon() { icon } + ImageIcon getOpenIcon() { null } } diff --git a/services/src/main/groovy/jd/gui/service/treenode/TextFileTreeNodeFactoryProvider.groovy b/services/src/main/groovy/jd/gui/service/treenode/TextFileTreeNodeFactoryProvider.groovy index 2111f0cb..b9ad3da8 100644 --- a/services/src/main/groovy/jd/gui/service/treenode/TextFileTreeNodeFactoryProvider.groovy +++ b/services/src/main/groovy/jd/gui/service/treenode/TextFileTreeNodeFactoryProvider.groovy @@ -28,7 +28,7 @@ class TextFileTreeNodeFactoryProvider extends FileTreeNodeFactoryProvider { Theme.load(TextFileTreeNodeFactoryProvider.class.classLoader.getResourceAsStream('rsyntaxtextarea/themes/eclipse.xml')) } - String[] getTypes() { ['*:file:*.txt'] } + String[] getTypes() { ['*:file:*.txt', '*:file:*.md', '*:file:*.SF', '*:file:*.policy'] } public <T extends DefaultMutableTreeNode & UriGettable> T make(API api, Container.Entry entry) { int lastSlashIndex = entry.path.lastIndexOf('/') diff --git a/services/src/main/groovy/jd/gui/service/treenode/WarFileTreeNodeFactoryProvider.groovy b/services/src/main/groovy/jd/gui/service/treenode/WarFileTreeNodeFactoryProvider.groovy new file mode 100644 index 00000000..4afa7f9c --- /dev/null +++ b/services/src/main/groovy/jd/gui/service/treenode/WarFileTreeNodeFactoryProvider.groovy @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2008-2015 Emmanuel Dupuy + * This program is made available under the terms of the GPLv3 License. + */ + +package jd.gui.service.treenode + +import jd.gui.api.API +import jd.gui.api.feature.UriGettable +import jd.gui.api.model.Container +import jd.gui.view.data.TreeNodeBean + +import javax.swing.ImageIcon +import javax.swing.tree.DefaultMutableTreeNode + +class WarFileTreeNodeFactoryProvider extends ZipFileTreeNodeFactoryProvider { + static final ImageIcon icon = new ImageIcon(JarFileTreeNodeFactoryProvider.class.classLoader.getResource('images/war_obj.gif')) + + String[] getTypes() { ['*:file:*.war'] } + + public <T extends DefaultMutableTreeNode & UriGettable> T make(API api, Container.Entry entry) { + int lastSlashIndex = entry.path.lastIndexOf('/') + def name = entry.path.substring(lastSlashIndex+1) + def node = new TreeNode(entry, 'war', new TreeNodeBean(label:name, icon:icon)) + // Add dummy node + node.add(new DefaultMutableTreeNode()) + return node + } +} diff --git a/services/src/main/groovy/jd/gui/service/treenode/WarPackageTreeNodeFactoryProvider.groovy b/services/src/main/groovy/jd/gui/service/treenode/WarPackageTreeNodeFactoryProvider.groovy new file mode 100644 index 00000000..67c2019c --- /dev/null +++ b/services/src/main/groovy/jd/gui/service/treenode/WarPackageTreeNodeFactoryProvider.groovy @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2008-2015 Emmanuel Dupuy + * This program is made available under the terms of the GPLv3 License. + */ + +package jd.gui.service.treenode + +import java.util.regex.Pattern + +class WarPackageTreeNodeFactoryProvider extends PackageTreeNodeFactoryProvider { + Pattern pattern = ~/WEB-INF\/classes\/.*/ + + String[] getTypes() { ['war:dir:*'] } + + Pattern getPathPattern() { pattern } +} diff --git a/services/src/main/groovy/jd/gui/service/treenode/WebXmlFileTreeNodeFactoryProvider.groovy b/services/src/main/groovy/jd/gui/service/treenode/WebXmlFileTreeNodeFactoryProvider.groovy new file mode 100644 index 00000000..b9324511 --- /dev/null +++ b/services/src/main/groovy/jd/gui/service/treenode/WebXmlFileTreeNodeFactoryProvider.groovy @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2008-2015 Emmanuel Dupuy + * This program is made available under the terms of the GPLv3 License. + */ + +package jd.gui.service.treenode + +import jd.gui.api.API +import jd.gui.api.feature.PageCreator +import jd.gui.api.feature.UriGettable +import jd.gui.api.model.Container +import jd.gui.view.component.WebXmlFilePage +import jd.gui.view.data.TreeNodeBean + +import javax.swing.ImageIcon +import javax.swing.JComponent +import javax.swing.tree.DefaultMutableTreeNode + + +class WebXmlFileTreeNodeFactoryProvider extends FileTreeNodeFactoryProvider { + static final ImageIcon icon = new ImageIcon(ManifestFileTreeNodeFactoryProvider.class.classLoader.getResource('images/xml_obj.gif')) + + String[] getTypes() { ['war:file:WEB-INF/web.xml'] } + + public <T extends DefaultMutableTreeNode & UriGettable> T make(API api, Container.Entry entry) { + return new TreeNode(entry, new TreeNodeBean(label:'web.xml', icon:icon, tip:"Location: $entry.uri.path")) + } + + static class TreeNode extends FileTreeNodeFactoryProvider.TreeNode implements PageCreator { + TreeNode(Container.Entry entry, Object userObject) { + super(entry, userObject) + } + // --- PageCreator --- // + public <T extends JComponent & UriGettable> T createPage(API api) { + return new WebXmlFilePage(api, entry) + } + } +} diff --git a/services/src/main/groovy/jd/gui/service/treenode/WebinfClassesDirectoryTreeNodeFactoryProvider.groovy b/services/src/main/groovy/jd/gui/service/treenode/WebinfClassesDirectoryTreeNodeFactoryProvider.groovy new file mode 100644 index 00000000..ff09139e --- /dev/null +++ b/services/src/main/groovy/jd/gui/service/treenode/WebinfClassesDirectoryTreeNodeFactoryProvider.groovy @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2008-2015 Emmanuel Dupuy + * This program is made available under the terms of the GPLv3 License. + */ + +package jd.gui.service.treenode + +import javax.swing.* +import java.util.regex.Pattern + +class WebinfClassesDirectoryTreeNodeFactoryProvider extends DirectoryTreeNodeFactoryProvider { + static final ImageIcon icon = new ImageIcon(WebinfClassesDirectoryTreeNodeFactoryProvider.class.classLoader.getResource('images/packagefolder_obj.png')) + + String[] getTypes() { ['war:dir:WEB-INF/classes'] } + + ImageIcon getIcon() { icon } + ImageIcon getOpenIcon() { null } +} diff --git a/services/src/main/groovy/jd/gui/service/treenode/WebinfLibDirectoryTreeNodeFactoryProvider.groovy b/services/src/main/groovy/jd/gui/service/treenode/WebinfLibDirectoryTreeNodeFactoryProvider.groovy new file mode 100644 index 00000000..7beb1e59 --- /dev/null +++ b/services/src/main/groovy/jd/gui/service/treenode/WebinfLibDirectoryTreeNodeFactoryProvider.groovy @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2008-2015 Emmanuel Dupuy + * This program is made available under the terms of the GPLv3 License. + */ + +package jd.gui.service.treenode + +import javax.swing.* + +class WebinfLibDirectoryTreeNodeFactoryProvider extends DirectoryTreeNodeFactoryProvider { + static final ImageIcon icon = new ImageIcon(WebinfLibDirectoryTreeNodeFactoryProvider.class.classLoader.getResource('images/archivefolder_obj.png')) + + String[] getTypes() { ['war:dir:WEB-INF/lib'] } + + ImageIcon getIcon() { icon } + ImageIcon getOpenIcon() { null } +} diff --git a/services/src/main/groovy/jd/gui/service/treenode/XmlFileTreeNodeFactoryProvider.groovy b/services/src/main/groovy/jd/gui/service/treenode/XmlFileTreeNodeFactoryProvider.groovy index c8290e56..90cc9fc2 100644 --- a/services/src/main/groovy/jd/gui/service/treenode/XmlFileTreeNodeFactoryProvider.groovy +++ b/services/src/main/groovy/jd/gui/service/treenode/XmlFileTreeNodeFactoryProvider.groovy @@ -17,7 +17,7 @@ import javax.swing.tree.DefaultMutableTreeNode class XmlFileTreeNodeFactoryProvider extends TextFileTreeNodeFactoryProvider { static final ImageIcon icon = new ImageIcon(XmlFileTreeNodeFactoryProvider.class.classLoader.getResource('images/xml_obj.gif')) - String[] getTypes() { ['*:file:*.xml', '*:file:*.xsl', '*:file:*.xslt', '*:file:*.xsd'] } + String[] getTypes() { ['*:file:*.xml', '*:file:*.xsl', '*:file:*.xslt', '*:file:*.xsd', '*:file:*.tld', '*:file:*.wsdl'] } public <T extends DefaultMutableTreeNode & UriGettable> T make(API api, Container.Entry entry) { int lastSlashIndex = entry.path.lastIndexOf('/') @@ -38,4 +38,4 @@ class XmlFileTreeNodeFactoryProvider extends TextFileTreeNodeFactoryProvider { } } } -} \ No newline at end of file +} diff --git a/services/src/main/groovy/jd/gui/view/component/WebXmlFilePage.groovy b/services/src/main/groovy/jd/gui/view/component/WebXmlFilePage.groovy new file mode 100644 index 00000000..0998ba4d --- /dev/null +++ b/services/src/main/groovy/jd/gui/view/component/WebXmlFilePage.groovy @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2008-2015 Emmanuel Dupuy + * This program is made available under the terms of the GPLv3 License. + */ + +package jd.gui.view.component + +import jd.gui.api.API +import jd.gui.api.feature.ContentSavable +import jd.gui.api.feature.IndexesChangeListener +import jd.gui.api.feature.UriGettable +import jd.gui.api.model.Container +import jd.gui.api.model.Indexes +import jd.gui.util.xml.AbstractXmlPathFinder +import org.fife.ui.rsyntaxtextarea.SyntaxConstants + +import java.awt.Point + + +class WebXmlFilePage extends HyperlinkPage implements UriGettable, ContentSavable, IndexesChangeListener { + protected API api + protected Container.Entry entry + protected Collection<Indexes> collectionOfIndexes + + WebXmlFilePage(API api, Container.Entry entry) { + this.api = api + this.entry = entry + // Load content file + def text = entry.inputStream.text + // Create hyperlinks + new PathFinder().find(text) + // Display + setText(text) + // Show hyperlinks + indexesChanged(api.collectionOfIndexes) + } + + String getSyntaxStyle() { SyntaxConstants.SYNTAX_STYLE_XML } + + protected boolean isHyperlinkEnabled(HyperlinkData hyperlinkData) { hyperlinkData.enabled } + + protected void openHyperlink(int x, int y, HyperlinkData hyperlinkData) { + if (hyperlinkData.enabled) { + // Save current position in history + def location = textArea.getLocationOnScreen() + int offset = textArea.viewToModel(new Point(x-location.x as int, y-location.y as int)) + def uri = entry.uri + api.addURI(new URI(uri.scheme, uri.authority, uri.path, 'position=' + offset, null)) + + // Open link + if (hyperlinkData instanceof TypeHyperlinkData) { + def internalTypeName = hyperlinkData.internalTypeName + def entries = collectionOfIndexes?.collect { it.getIndex('typeDeclarations')?.get(internalTypeName) }.flatten().grep { it!=null } + def rootUri = entry.container.root.uri.toString() + def sameContainerEntries = entries?.grep { it.uri.toString().startsWith(rootUri) } + + if (sameContainerEntries) { + api.openURI(x, y, sameContainerEntries, null, hyperlinkData.internalTypeName) + } else if (entries) { + api.openURI(x, y, entries, null, hyperlinkData.internalTypeName) + } + } else { + String path = hyperlinkData.path + def entry = searchEntry(this.entry.container.root, path) + if (entry) { + api.openURI(x, y, [entry], null, path) + } + } + } + } + + static Container.Entry searchEntry(Container.Entry parent, String path) { + if (path.charAt(0) == '/') + path = path.substring(1) + return recursiveSearchEntry(parent, path) + } + + static Container.Entry recursiveSearchEntry(Container.Entry parent, String path) { + def entry = parent.children.find { path.equals(it.path) } + + if (entry) { + return entry + } else { + entry = parent.children.find { path.startsWith(it.path + '/') } + return entry ? searchEntry(entry, path) : null + } + } + + // --- UriGettable --- // + URI getUri() { entry.uri } + + // --- SourceSavable --- // + String getFileName() { + def path = entry.path + int index = path.lastIndexOf('/') + return path.substring(index+1) + } + + void save(API api, OutputStream os) { + os << textArea.text + } + + // --- IndexesChangeListener --- // + void indexesChanged(Collection<Indexes> collectionOfIndexes) { + // Update the list of containers + this.collectionOfIndexes = collectionOfIndexes + // Refresh links + boolean refresh = false + + for (def entry : hyperlinks.entrySet()) { + def data = entry.value + boolean enabled + + if (data instanceof TypeHyperlinkData) { + def internalTypeName = data.internalTypeName + enabled = collectionOfIndexes.find { it.getIndex('typeDeclarations')?.get(internalTypeName) } != null + } else { + enabled = searchEntry(this.entry.container.root, data.path) != null + } + + if (data.enabled != enabled) { + data.enabled = enabled + refresh = true + } + } + + if (refresh) { + textArea.repaint() + } + } + + static class TypeHyperlinkData extends HyperlinkPage.HyperlinkData { + boolean enabled + String internalTypeName + + TypeHyperlinkData(int startPosition, int endPosition, String internalTypeName) { + super(startPosition, endPosition) + this.enabled = false + this.internalTypeName = internalTypeName + } + } + + static class PathHyperlinkData extends HyperlinkPage.HyperlinkData { + boolean enabled + String path + + PathHyperlinkData(int startPosition, int endPosition, String path) { + super(startPosition, endPosition) + this.enabled = false + this.path = path + } + } + + class PathFinder extends AbstractXmlPathFinder { + static HashSet<String> typeHyperlinkPaths = [ + 'web-app/filter/filter-class', + 'web-app/listener/listener-class', + 'web-app/servlet/servlet-class'] + + static HashSet<String> pathHyperlinkPaths = [ + 'web-app/jsp-config/taglib/taglib-location', + 'web-app/welcome-file-list/welcome-file', + 'web-app/login-config/form-login-config/form-login-page', + 'web-app/login-config/form-login-config/form-error-page', + 'web-app/jsp-config/jsp-property-group/include-prelude', + 'web-app/jsp-config/jsp-property-group/include-coda'] + + PathFinder() { + super(typeHyperlinkPaths + pathHyperlinkPaths) + } + + void handle(String path, String text, int position) { + def trim = text.trim() + if (trim) { + int startIndex = position + text.indexOf(trim) + int endIndex = startIndex + trim.length() + + if (pathHyperlinkPaths.contains(path)) { + addHyperlink(new PathHyperlinkData(startIndex, endIndex, trim)) + } else { + def internalTypeName = trim.replace('.', '/') + addHyperlink(new TypeHyperlinkData(startIndex, endIndex, internalTypeName)) + } + } + } + } +} diff --git a/services/src/main/java/jd/gui/util/xml/AbstractXmlPathFinder.java b/services/src/main/java/jd/gui/util/xml/AbstractXmlPathFinder.java new file mode 100644 index 00000000..50de9b67 --- /dev/null +++ b/services/src/main/java/jd/gui/util/xml/AbstractXmlPathFinder.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2008-2015 Emmanuel Dupuy + * This program is made available under the terms of the GPLv3 License. + */ + +package jd.gui.util.xml; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import java.io.StringReader; +import java.util.*; + +public abstract class AbstractXmlPathFinder { + + protected HashMap<String, HashSet<String>> tagNameToPaths = new HashMap<>(); + protected StringBuffer sb = new StringBuffer(200); + + public AbstractXmlPathFinder(Collection<String> paths) { + for (String path : paths) { + if ((path != null) && (path.length() > 0)) { + // Normalize path + path = '/' + path; + int lastIndex = path.lastIndexOf('/'); + String lastTagName = path.substring(lastIndex+1); + + // Add tag names to map + HashSet<String> setOfPaths = tagNameToPaths.get(lastTagName); + if (setOfPaths == null) { + tagNameToPaths.put(lastTagName, setOfPaths = new HashSet<>()); + } + setOfPaths.add(path); + } + } + } + + public void find(String text) { + sb.setLength(0); + + try { + XMLInputFactory factory = XMLInputFactory.newInstance(); + XMLStreamReader reader = factory.createXMLStreamReader(new StringReader(text)); + + String tagName = ""; + int offset = 0; + + while (reader.hasNext()) { + reader.next(); + + switch (reader.getEventType()) + { + case XMLStreamReader.START_ELEMENT: + sb.append('/').append(tagName = reader.getLocalName()); + offset = reader.getLocation().getCharacterOffset(); + break; + case XMLStreamReader.END_ELEMENT: + sb.setLength(sb.length() - reader.getLocalName().length() - 1); + break; + case XMLStreamReader.CHARACTERS: + HashSet<String> setOfPaths = tagNameToPaths.get(tagName); + + if (setOfPaths != null) { + String path = sb.toString(); + + if (setOfPaths.contains(path)) { + // Search start offset + while (offset > 0) { + if (text.charAt(offset) == '>') { + break; + } else { + offset--; + } + } + + handle(path.substring(1), reader.getText(), offset+1); + } + } + break; + } + } + } catch (XMLStreamException ignore) { + } + } + + public abstract void handle(String path, String text, int position); +} diff --git a/services/src/main/resources/META-INF/services/jd.gui.spi.ContainerFactory b/services/src/main/resources/META-INF/services/jd.gui.spi.ContainerFactory index 09008cf8..769ebe19 100644 --- a/services/src/main/resources/META-INF/services/jd.gui.spi.ContainerFactory +++ b/services/src/main/resources/META-INF/services/jd.gui.spi.ContainerFactory @@ -1,2 +1,4 @@ +# Order is important +jd.gui.service.container.WarContainerFactoryProvider jd.gui.service.container.JarContainerFactoryProvider jd.gui.service.container.GenericContainerFactoryProvider diff --git a/services/src/main/resources/META-INF/services/jd.gui.spi.FileLoader b/services/src/main/resources/META-INF/services/jd.gui.spi.FileLoader index 98254020..64c280a8 100644 --- a/services/src/main/resources/META-INF/services/jd.gui.spi.FileLoader +++ b/services/src/main/resources/META-INF/services/jd.gui.spi.FileLoader @@ -1,4 +1,5 @@ jd.gui.service.fileloader.ClassFileLoaderProvider jd.gui.service.fileloader.JarFileLoaderProvider jd.gui.service.fileloader.LogFileLoaderProvider +jd.gui.service.fileloader.WarFileLoaderProvider jd.gui.service.fileloader.ZipFileLoaderProvider diff --git a/services/src/main/resources/META-INF/services/jd.gui.spi.Indexer b/services/src/main/resources/META-INF/services/jd.gui.spi.Indexer index c8d5c7c9..cfe3ccd1 100644 --- a/services/src/main/resources/META-INF/services/jd.gui.spi.Indexer +++ b/services/src/main/resources/META-INF/services/jd.gui.spi.Indexer @@ -2,4 +2,5 @@ jd.gui.service.indexer.DirectoryIndexerProvider jd.gui.service.indexer.ClassFileIndexerProvider jd.gui.service.indexer.MetainfServiceFileIndexerProvider jd.gui.service.indexer.TextFileIndexerProvider +jd.gui.service.indexer.WebXmlFileIndexerProvider jd.gui.service.indexer.ZipFileIndexerProvider diff --git a/services/src/main/resources/META-INF/services/jd.gui.spi.TreeNodeFactory b/services/src/main/resources/META-INF/services/jd.gui.spi.TreeNodeFactory index ff7f4d20..76b760a5 100644 --- a/services/src/main/resources/META-INF/services/jd.gui.spi.TreeNodeFactory +++ b/services/src/main/resources/META-INF/services/jd.gui.spi.TreeNodeFactory @@ -1,4 +1,5 @@ jd.gui.service.treenode.ClassFileTreeNodeFactoryProvider +jd.gui.service.treenode.CssFileTreeNodeFactoryProvider jd.gui.service.treenode.DirectoryTreeNodeFactoryProvider jd.gui.service.treenode.DtdFileTreeNodeFactoryProvider jd.gui.service.treenode.FileTreeNodeFactoryProvider @@ -6,7 +7,7 @@ jd.gui.service.treenode.HtmlFileTreeNodeFactoryProvider jd.gui.service.treenode.JarFileTreeNodeFactoryProvider #jd.gui.service.treenode.JavaFileTreeNodeFactoryProvider jd.gui.service.treenode.JavascriptFileTreeNodeFactoryProvider -#jd.gui.service.treenode.JspFileTreeNodeFactoryProvider +jd.gui.service.treenode.JspFileTreeNodeFactoryProvider jd.gui.service.treenode.ManifestFileTreeNodeFactoryProvider jd.gui.service.treenode.MetainfDirectoryTreeNodeFactoryProvider jd.gui.service.treenode.MetainfServiceFileTreeNodeFactoryProvider @@ -14,5 +15,10 @@ jd.gui.service.treenode.PackageTreeNodeFactoryProvider jd.gui.service.treenode.PropertiesFileTreeNodeFactoryProvider jd.gui.service.treenode.SqlFileTreeNodeFactoryProvider jd.gui.service.treenode.TextFileTreeNodeFactoryProvider +jd.gui.service.treenode.WarFileTreeNodeFactoryProvider +jd.gui.service.treenode.WarPackageTreeNodeFactoryProvider +jd.gui.service.treenode.WebinfClassesDirectoryTreeNodeFactoryProvider +jd.gui.service.treenode.WebinfLibDirectoryTreeNodeFactoryProvider +jd.gui.service.treenode.WebXmlFileTreeNodeFactoryProvider jd.gui.service.treenode.XmlFileTreeNodeFactoryProvider jd.gui.service.treenode.ZipFileTreeNodeFactoryProvider diff --git a/services/src/main/resources/images/archivefolder_obj.png b/services/src/main/resources/images/archivefolder_obj.png new file mode 100644 index 0000000000000000000000000000000000000000..8d653f69d1f31a363cef2c946c4d31db01b18668 GIT binary patch literal 3017 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 LNkvXXu0mjf0A0qh literal 0 HcmV?d00001 diff --git a/services/src/main/resources/images/css_obj.png b/services/src/main/resources/images/css_obj.png new file mode 100644 index 0000000000000000000000000000000000000000..4caa409cdb4288ea619113dad80274997f3d41cd GIT binary patch literal 2972 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 S`_*j#0000<MNUMnLSTYkg2%)F literal 0 HcmV?d00001 diff --git a/services/src/main/resources/images/inf_obj.png b/services/src/main/resources/images/inf_obj.png new file mode 100644 index 0000000000000000000000000000000000000000..f9f9c42d6f82e0af8e7f42aab584c799a4c84fef GIT binary patch literal 2954 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 A`~Uy| literal 0 HcmV?d00001 diff --git a/services/src/main/resources/images/packagefolder_obj.png b/services/src/main/resources/images/packagefolder_obj.png new file mode 100644 index 0000000000000000000000000000000000000000..93053b772f5529aae3d02f2e27314b9ffa1f440f GIT binary patch literal 421 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbMffdHQn*Z&Ov*B0qFxl8`I(J?Pg z{l|rh9}i~yxX_ShXY}IOlpilPytvTu<HL#n|NkHEHv9E$*N+Dae%zSQ6YKWl#f(d< z8~%K||M>jQE%inZ4pgiy(*AH`!qOa-N`HrW@v>`kRQiJ?_tYD`zkh5`tI^?BGoYPR z2fwa!#Q_avED7=pW^j0RBMrn!@^*Kzw@WrC1#&nGJR*yMv<Dcwoy@iaG73Fi977~7 zPd$HIsL6nb<$|x)gf1rCits%j{{Qz#(mr1?Z<F=pGb}&a|FpR^3ZH%)C%g0O#;;*M zwe@Sy=YMPdaj@(BxjU9!9w~c2-<s9D&LhgQv`g%OYKuc^SaxA?Urd^0f77Zrvp+6T h@|L`zbFJ2bnd1m+(w)h=$AOkJc)I$ztaD0e0sz5$mJt8| literal 0 HcmV?d00001