{"action":"created","comment":{"url":"https://api.github.com/repos/geoserver/geoserver/pulls/comments/64169809","id":64169809,"diff_hunk":"@@ -0,0 +1,516 @@\n+/* (c) 2016 Open Source Geospatial Foundation - all rights reserved\n+ * This code is licensed under the GPL 2.0 license, available at the root\n+ * application directory.\n+ */\n+package org.geoserver.web.resources;\n+\n+import org.apache.wicket.Component;\n+import org.apache.wicket.ajax.AjaxRequestTarget;\n+import org.apache.wicket.ajax.markup.html.AjaxLink;\n+import org.apache.wicket.behavior.AttributeAppender;\n+import org.apache.wicket.markup.html.basic.Label;\n+import org.apache.wicket.markup.html.link.AbstractLink;\n+import org.apache.wicket.markup.html.link.Link;\n+import org.apache.wicket.model.Model;\n+import org.apache.wicket.request.handler.resource.ResourceStreamRequestHandler;\n+import org.apache.wicket.request.resource.ContentDisposition;\n+import org.apache.commons.io.FilenameUtils;\n+import org.apache.commons.io.IOUtils;\n+import org.geoserver.platform.resource.Paths;\n+import org.geoserver.platform.resource.Resource;\n+import org.geoserver.platform.resource.ResourceStore;\n+import org.geoserver.platform.resource.Resources;\n+import org.geoserver.web.GeoServerSecuredPage;\n+import org.geoserver.web.treeview.TreeNode;\n+import org.geoserver.web.treeview.TreeView;\n+import org.geoserver.web.wicket.GeoServerDialog;\n+import org.geoserver.web.wicket.ParamResourceModel;\n+import org.geoserver.web.wicket.GeoServerDialog.DialogDelegate;\n+\n+import java.io.IOException;\n+import java.io.InputStream;\n+import java.io.OutputStream;\n+\n+/**\n+ * The ResourceBrowser page.\n+ * \n+ * @author Niels Charlier\n+ *\n+ */\n+public class PageResourceBrowser extends GeoServerSecuredPage {\n+    \n+    private static final long serialVersionUID = 3979040405548783679L;\n+        \n+    /**\n+     * Behaviour for disabled button\n+     */\n+    private static final AttributeAppender DISABLED_BEHAVIOR = new AttributeAppender(\"class\", new Model<String>(\"disabled\"), \" \");\n+\n+    /**\n+     * The extension that are recognised as simple text resources (and can be edited with simple text editor).\n+     */\n+    private static final String[] TEXTUAL_EXTENSIONS = new String[] {\"txt\", \"properties\", \"info\", \"xml\", \"sld\", \"rst\", \"log\"};\n+        \n+    /**\n+     * The expanded states model.\n+     */\n+    protected final ResourceExpandedStates expandedStates = new ResourceExpandedStates();\n+    \n+    /**\n+     * The clip board.\n+     */\n+    protected final ClipBoard clipBoard;\n+          \n+    public PageResourceBrowser() {\n+        //create the root node\n+        final ResourceNode rootNode = new ResourceNode(store().get(Paths.BASE), expandedStates);\n+        rootNode.getExpanded().setObject(true);\n+        \n+        //create tree view and clip board\n+        final TreeView<Resource> treeView = new TreeView<Resource>(\"treeview\", rootNode);\n+        clipBoard = new ClipBoard(treeView);\n+        \n+        //used for all pop-up dialogs.\n+        final GeoServerDialog dialog = new GeoServerDialog(\"dialog\");\n+        dialog.setResizable(false);        \n+        \n+        //upload button\n+        final AjaxLink<Void> btnUpload = new AjaxLink<Void>(\"upload\") {\n+            private static final long serialVersionUID = -6538820444407766106L;\n+\n+            @Override\n+            public void onClick(AjaxRequestTarget target) {\n+                dialog.setInitialHeight(225);\n+                                \n+                dialog.showOkCancel(target, new DialogDelegate() {\n+                    private static final long serialVersionUID = 1557172478015946688L;\n+                    private PanelUpload uploadPanel;\n+\n+                    @Override\n+                    protected Component getContents(String id) {\n+                        uploadPanel = new PanelUpload(id,\"/\" + treeView.getSelectedNode().getObject().path());\n+                        return uploadPanel;\n+                    }\n+\n+                    @Override\n+                    protected boolean onSubmit(AjaxRequestTarget target, Component contents) {\n+                        uploadPanel.getFeedbackMessages().clear();\n+                        if (uploadPanel.getFileUpload() == null) {\n+                            uploadPanel.error(new ParamResourceModel(\"fileRequired\", getPage()).getString());      \n+                        } else {\n+                            String dir = uploadPanel.getDirectory();\n+                            Resource dest = store().get(Paths.path(dir, uploadPanel.getFileUpload().getClientFileName()));                            \n+                            if (Resources.exists(dest)) {\n+                                uploadPanel.error(new ParamResourceModel(\"resourceExists\", getPage()).getString());                 \n+                            } else {\n+                                try (OutputStream os = dest.out()) {\n+                                    IOUtils.copy(uploadPanel.getFileUpload().getInputStream(), os);\n+                                    treeView.setSelectedNode(new ResourceNode(dest, expandedStates), target);\n+                                    return true;\n+                                }\n+                                catch (IOException | IllegalStateException e) {\n+                                    uploadPanel.error(e.getMessage());\n+                                }\n+                            }\n+                        }\n+                        target.add(uploadPanel.getFeedbackPanel());\n+                        return false;\n+                    }\n+                });\n+            }\n+        };\n+        \n+        //new resource button\n+        final AjaxLink<Void> btnNew = new AjaxLink<Void>(\"new\") {\n+            private static final long serialVersionUID = 8112272759002275843L;\n+\n+            @Override\n+            public void onClick(AjaxRequestTarget target) {\n+                dialog.setInitialHeight(525);\n+\n+                dialog.showOkCancel(target, new DialogDelegate() {\n+                    private static final long serialVersionUID = -8898887236980594842L;\n+\n+                    private PanelEdit editPanel;\n+\n+                    @Override\n+                    protected Component getContents(String id) {\n+                        //pick a non-existing resource name (can be changed by user)\n+                        String dest = \"/\" + Paths.path(treeView.getSelectedNode().getObject().path(), \"new.txt\");\n+                        for (int i = 1; Resources.exists(store().get(dest)); i++) {\n+                            dest = \"/\" + Paths.path(treeView.getSelectedNode().getObject().path(), \"new.\" + i + \".txt\");\n+                        }\n+                        editPanel = new PanelEdit(id, dest, true, \"\");\n+                        return editPanel;\n+                    }\n+\n+                    @Override\n+                    protected boolean onSubmit(AjaxRequestTarget target, Component contents) {\n+                        editPanel.getFeedbackMessages().clear();\n+                        Resource dest = store().get(editPanel.getResource());\n+                        if (Resources.exists(dest)) {\n+                            editPanel.error(new ParamResourceModel(\"resourceExists\", getPage()).getString());\n+                        } else {\n+                            try (OutputStream os = dest.out()) {\n+                                String newContents = editPanel.getContents();\n+                                if (newContents != null) {\n+                                    os.write(newContents.getBytes());\n+                                }\n+                                //select newly created node\n+                                treeView.setSelectedNode(new ResourceNode(dest, expandedStates), target);\n+                                return true;\n+                            } catch (IOException | IllegalStateException e) {\n+                                error(e.getMessage());\n+                            }\n+                        }\n+                        target.add(editPanel.getFeedbackPanel());\n+                        return false;\n+                    }\n+                });\n+\n+            }\n+            \n+        };\n+        \n+        //download button\n+        final Link<Void> btnDownload = new Link<Void>(\"download\") {\n+            private static final long serialVersionUID = 2746429086122117005L;\n+\n+            @Override\n+            public void onClick() {\n+                Resource res = treeView.getSelectedNode().getObject();\n+                getRequestCycle().scheduleRequestHandlerAfterCurrent(\n+                        new ResourceStreamRequestHandler(new WicketResourceAdaptor(res))\n+                            .setFileName(res.name())\n+                            .setContentDisposition(ContentDisposition.ATTACHMENT));\n+            }\n+            \n+        };\n+        \n+        //edit button\n+        final AjaxLink<Void> btnEdit = new AjaxLink<Void>(\"edit\") {\n+            private static final long serialVersionUID = 6690936054046040647L;\n+\n+            @Override\n+            public void onClick(AjaxRequestTarget target) {\n+                dialog.setInitialHeight(500);\n+                final Resource resource = treeView.getSelectedNode().getObject();\n+                final String contents;\n+                try (InputStream is = resource.in()) {\n+                    contents = IOUtils.toString(is);\n+                    \n+                    dialog.showOkCancel(target, new DialogDelegate() {\n+                        private static final long serialVersionUID = -8898887236980594842L;\n+\n+                        private PanelEdit editPanel;\n+\n+                        @Override\n+                        protected Component getContents(String id) {\n+                            editPanel = new PanelEdit(id, \"/\" + resource.path(), false, contents);\n+                            return editPanel;\n+                        }\n+\n+                        @Override\n+                        protected boolean onSubmit(AjaxRequestTarget target, Component contents) {\n+                            editPanel.getFeedbackMessages().clear();\n+                            try (OutputStream os = resource.out()) {\n+                                String newContents = editPanel.getContents();\n+                                if (newContents != null) {\n+                                    os.write(newContents.getBytes());\n+                                }\n+                                return true;\n+                            } catch (IOException | IllegalStateException e) {\n+                                error(e.getMessage());\n+                            }                        \n+                            target.add(editPanel.getFeedbackPanel());\n+                            return false;\n+                        }\n+                    });\n+                } catch (IOException | IllegalStateException e) {\n+                    error(e.getMessage());\n+                    target.add(getFeedbackPanel());\n+                }\n+            }\n+\n+        };\n+                \n+        //paste button\n+        final AjaxLink<Void> btnPaste = new AjaxLink<Void>(\"paste\") {\n+            private static final long serialVersionUID = 2647829118342823975L;\n+\n+            @Override\n+            public void onClick(AjaxRequestTarget target) {\n+                dialog.setInitialHeight(240);\n+                \n+                dialog.showOkCancel(target, new DialogDelegate() {\n+                    private static final long serialVersionUID = -8898887236980594842L;\n+                    \n+                    private PanelPaste pastePanel;\n+\n+                    @Override\n+                    protected Component getContents(String id) {\n+                        pastePanel = new PanelPaste(id,\n+                                \"/\" + clipBoard.getClipBoard().getObject().path(),\n+                                \"/\" + treeView.getSelectedNode().getObject().path(), \n+                                clipBoard.isClipBoardIsCopy());\n+                        return pastePanel;\n+                    }\n+\n+                    @Override\n+                    protected boolean onSubmit(AjaxRequestTarget target, Component contents) {\n+                        pastePanel.getFeedbackMessages().clear();\n+\n+                        String dir = pastePanel.getDirectory();\n+                        Resource src = clipBoard.getClipBoard().getObject();\n+                        Resource dest = store().get(Paths.path(dir, src.name()));\n+                        if (clipBoard.isClipBoardIsCopy() && dest.equals(src)) {\n+                            //if we are copying a resource to its own directory, we will give it a new name.\n+                            for (int i = 1; Resources.exists(dest); i++) {\n+                                dest = store().get(Paths.path(dir, \n+                                        FilenameUtils.getExtension(src.name()).isEmpty() ? src.name() + \".\" + i :\n+                                        FilenameUtils.getBaseName(src.name()) + \".\" + i + \".\" + \n+                                                FilenameUtils.getExtension(src.name())));\n+                            }\n+                        }\n+                        if (Resources.exists(dest)) {\n+                            pastePanel.error(new ParamResourceModel(\"resourceExists\", getPage()).getString());\n+                        } else {\n+                            try {                                \n+                                if (clipBoard.isClipBoardIsCopy()) {\n+                                    try (InputStream is = src.in()) {\n+                                        try (OutputStream os = dest.out()) {\n+                                            IOUtils.copy(is, os);       \n+                                        }\n+                                    }\n+                                    //we keep resource on clipboard for in case the user wants multiple copies\n+                                } else {\n+                                    if (!store().move(src.path(), dest.path())) {\n+                                        throw new IOException(new ParamResourceModel(\"moveFailed\", getPage()).getString());\n+                                    }\n+                                    //clear clipboard, resource isn't there anymore\n+                                    clipBoard.setClipBoard(null, target);\n+                                }\n+                                //we select the newly created node.\n+                                treeView.setSelectedNode(new ResourceNode(dest, expandedStates), target);\n+                                return true;\n+                            } catch (IOException | IllegalStateException e) {\n+                                pastePanel.error(e.getMessage());\n+                            }\n+                        }\n+                        target.add(pastePanel.getFeedbackPanel());\n+                        return false;\n+                    }\n+                });              \n+            }\n+            \n+        };\n+        \n+        //copy button\n+        final AjaxLink<Void> btnCopy = new AjaxLink<Void>(\"copy\") {\n+            private static final long serialVersionUID = 3883958793500232081L;\n+\n+            @Override\n+            public void onClick(AjaxRequestTarget target) {\n+                clipBoard.setClipBoard(treeView.getSelectedNode(), true, target);\n+                target.add(treeView.getSelectedView());\n+            }\n+            \n+        };\n+        \n+        //cut button\n+        final AjaxLink<Void> btnCut = new AjaxLink<Void>(\"cut\") {\n+            private static final long serialVersionUID = 2647829118342823975L;\n+\n+            @Override\n+            public void onClick(AjaxRequestTarget target) {\n+                clipBoard.setClipBoard(treeView.getSelectedNode(), false, target);\n+                enable(btnPaste, !treeView.getSelectedNode().isLeaf());\n+                target.add(treeView.getSelectedView());\n+                target.add(btnPaste);\n+            }\n+            \n+        };\n+        \n+        //rename button\n+        final AjaxLink<Void> btnRename = new AjaxLink<Void>(\"rename\") {\n+            private static final long serialVersionUID = 2647829118342823975L;\n+\n+            @Override\n+            public void onClick(AjaxRequestTarget target) {\n+                dialog.setInitialHeight(150);\n+                \n+                dialog.showOkCancel(target, new DialogDelegate() {\n+                    private static final long serialVersionUID = -8898887236980594842L;\n+                    \n+                    private PanelRename renamePanel;\n+\n+                    @Override\n+                    protected Component getContents(String id) {\n+                        renamePanel = new PanelRename(id, treeView.getSelectedNode().getObject().name());\n+                        return renamePanel;\n+                    }\n+\n+                    @Override\n+                    protected boolean onSubmit(AjaxRequestTarget target, Component contents) {\n+                        renamePanel.getFeedbackMessages().clear();\n+\n+                        Resource src = treeView.getSelectedNode().getObject();\n+                        Resource dest = treeView.getSelectedNode().getObject().parent().get(renamePanel.getName());\n+                        if (Resources.exists(dest)) {\n+                            renamePanel.error(new ParamResourceModel(\"resourceExists\", getPage()).getString());\n+                        } else {\n+                           Boolean expandedModel = expandedStates.getResourceExpandedState(src).getObject();\n+                           if (!src.renameTo(dest)) {\n+                               renamePanel.error(new ParamResourceModel(\"renameFailed\", getPage()).getString());\n+                           } else {\n+                               if (clipBoard.getClipBoard() != null && src.equals(clipBoard.getClipBoard().getObject())) {\n+                                   //if the node being renamed is on the clipboard, the clipboard must be updated accordingly.\n+                                   clipBoard.setClipBoard(new ResourceNode(dest, expandedStates), target);\n+                               }\n+                               //we have a new expanded state. if the original node was expanded, we expand this one as well.\n+                               //(child nodes might still loose their expanded state though)\n+                               expandedStates.getResourceExpandedState(dest).setObject(expandedModel);\n+                               //select the new node\n+                               treeView.setSelectedNode(new ResourceNode(dest, expandedStates), target);\n+                               return true;\n+                           }\n+                        }\n+                        target.add(renamePanel.getFeedbackPanel());\n+                        return false;\n+                    }\n+                });\n+            }\n+            \n+        };\n+        \n+        //delete button\n+        final AjaxLink<Void> btnDelete = new AjaxLink<Void>(\"delete\") {\n+            private static final long serialVersionUID = -7370119488741589880L;\n+\n+            @Override\n+            public void onClick(AjaxRequestTarget target) {\n+                dialog.setInitialHeight(75);\n+\n+                final Resource res = treeView.getSelectedNode().getObject();\n+\n+                dialog.showOkCancel(target, new DialogDelegate() {\n+                    private static final long serialVersionUID = 1557172478015946688L;\n+\n+                    @Override\n+                    protected Component getContents(String id) {\n+                        return new Label(id, new ParamResourceModel(\n+                                res.getType() == Resource.Type.DIRECTORY ? \"confirmDeleteDir\" : \"confirmDelete\", \n+                                        getPage()).getString().replaceAll(\"%\", \"/\" + res.path()));\n+                    }\n+\n+                    @Override\n+                    protected boolean onSubmit(AjaxRequestTarget target, Component contents) {\n+                        if (!res.delete()) {\n+                            error(new ParamResourceModel(\"deleteFailed\", getPage()).getString());\n+                            target.add(getFeedbackPanel());\n+                        } else {\n+                            if (clipBoard.getClipBoard() != null && res.equals(clipBoard.getClipBoard().getObject())) {\n+                                //if deleted node was on clipboard, clear the clipboard\n+                                clipBoard.setClipBoard(null, target);\n+                            }\n+                            //remove selection\n+                            treeView.setSelectedNode(null, target);\n+                        }\n+                        return true;           \n+                    }\n+                });\n+            }\n+            \n+        };\n+                 \n+        //update menu buttons enabled states according to current selection\n+        treeView.addSelectionListener(target -> {\n+                         TreeNode<Resource> node = treeView.getSelectedNode();\n+                         enable(btnUpload, node!= null && !node.isLeaf());\n+                         enable(btnNew, node!= null && !node.isLeaf());\n+                         enable(btnDownload, node!= null && node.isLeaf());\n+                         enable(btnEdit, node!= null && node.isLeaf() && isTextual(node.getObject()));\n+                         enable(btnCopy, node!= null && node.isLeaf());\n+                         enable(btnCut, node!= null && !node.getObject().path().isEmpty());\n+                         enable(btnRename, node!= null && !node.getObject().path().isEmpty());\n+                         enable(btnPaste, node!= null && clipBoard.getClipBoard() != null && !node.isLeaf());\n+                         enable(btnDelete, node!= null && !node.getObject().path().isEmpty());\n+                         target.add(btnUpload, btnNew, btnDownload, btnEdit, btnCopy, btnCut, btnPaste, btnRename, btnDelete);\n+                });\n+        \n+        //initialize and add buttons\n+        initButtons(btnUpload, btnNew, btnDownload, btnEdit, btnCopy, btnCut, btnPaste, btnRename, btnDelete);\n+        add(dialog, btnUpload, btnNew, btnDownload, btnEdit, btnCopy, btnCut, btnPaste, btnRename, btnDelete, treeView);\n+    }\n+    \n+    /**\n+     * The TreeView\n+     * \n+     * @return the TreeView\n+     */\n+    @SuppressWarnings(\"unchecked\")\n+    protected TreeView<Resource> treeView() {\n+        return (TreeView<Resource>) get(\"treeview\");\n+    }\n+       \n+    /**\n+     * The resource store \n+     * \n+     * @return resource store\n+     */\n+    protected ResourceStore store() {\n+        return getGeoServerApplication().getResourceLoader();\n+    }\n+    \n+    /**\n+     * Initialize the buttons (start with all disabled)\n+     */\n+    protected static void initButtons(AbstractLink... buttons) {\n+        for (AbstractLink button : buttons) {\n+            button.setEnabled(false);\n+            button.add(DISABLED_BEHAVIOR);\n+            button.setOutputMarkupId(true);\n+        }\n+    }\n+    \n+    /**\n+     * Enable/disable a button\n+     * \n+     * @param button the button\n+     * @param enabled enabled state\n+     */\n+    protected static void enable(AbstractLink button, boolean enabled) {\n+        if (enabled != button.isEnabled()) {\n+            button.setEnabled(enabled);\n+            if (enabled) {\n+                button.remove(DISABLED_BEHAVIOR);\n+            } else {\n+                button.add(DISABLED_BEHAVIOR);\n+            }\n+        }\n+    }\n+    \n+    /**\n+     * Guess if a resource is textual or not (by extension)\n+     * \n+     * @param resource the resource\n+     * @return whether that resource is likely textual or not.\n+     */\n+    private static boolean isTextual(Resource resource) {\n+        if (resource.getType() != Resource.Type.RESOURCE) {\n+            return false;\n+        }\n+        int i = resource.name().lastIndexOf(\".\");\n+        if (i >= 0) {\n+            String ext = resource.name().substring(i + 1).toLowerCase();","path":"src/community/web-resources/src/main/java/org/geoserver/web/resources/PageResourceBrowser.java","position":505,"original_position":505,"commit_id":"1293d4a37732ec90c4bac8c1329272712945d921","original_commit_id":"1293d4a37732ec90c4bac8c1329272712945d921","user":{"login":"bencaradocdavies","id":1900866,"avatar_url":"https://avatars.githubusercontent.com/u/1900866?v=3","gravatar_id":"","url":"https://api.github.com/users/bencaradocdavies","html_url":"https://github.com/bencaradocdavies","followers_url":"https://api.github.com/users/bencaradocdavies/followers","following_url":"https://api.github.com/users/bencaradocdavies/following{/other_user}","gists_url":"https://api.github.com/users/bencaradocdavies/gists{/gist_id}","starred_url":"https://api.github.com/users/bencaradocdavies/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bencaradocdavies/subscriptions","organizations_url":"https://api.github.com/users/bencaradocdavies/orgs","repos_url":"https://api.github.com/users/bencaradocdavies/repos","events_url":"https://api.github.com/users/bencaradocdavies/events{/privacy}","received_events_url":"https://api.github.com/users/bencaradocdavies/received_events","type":"User","site_admin":false},"body":"Good. I wanted to check if you thought of non-lowercase extensions and you did!","created_at":"2016-05-23T05:05:24Z","updated_at":"2016-05-23T05:05:24Z","html_url":"https://github.com/geoserver/geoserver/pull/1572#discussion_r64169809","pull_request_url":"https://api.github.com/repos/geoserver/geoserver/pulls/1572","_links":{"self":{"href":"https://api.github.com/repos/geoserver/geoserver/pulls/comments/64169809"},"html":{"href":"https://github.com/geoserver/geoserver/pull/1572#discussion_r64169809"},"pull_request":{"href":"https://api.github.com/repos/geoserver/geoserver/pulls/1572"}}},"pull_request":{"url":"https://api.github.com/repos/geoserver/geoserver/pulls/1572","id":66878781,"html_url":"https://github.com/geoserver/geoserver/pull/1572","diff_url":"https://github.com/geoserver/geoserver/pull/1572.diff","patch_url":"https://github.com/geoserver/geoserver/pull/1572.patch","issue_url":"https://api.github.com/repos/geoserver/geoserver/issues/1572","number":1572,"state":"open","locked":false,"title":"[GEOS-7516] Resource Browser (ResourceStore GUI)","user":{"login":"NielsCharlier","id":1906115,"avatar_url":"https://avatars.githubusercontent.com/u/1906115?v=3","gravatar_id":"","url":"https://api.github.com/users/NielsCharlier","html_url":"https://github.com/NielsCharlier","followers_url":"https://api.github.com/users/NielsCharlier/followers","following_url":"https://api.github.com/users/NielsCharlier/following{/other_user}","gists_url":"https://api.github.com/users/NielsCharlier/gists{/gist_id}","starred_url":"https://api.github.com/users/NielsCharlier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/NielsCharlier/subscriptions","organizations_url":"https://api.github.com/users/NielsCharlier/orgs","repos_url":"https://api.github.com/users/NielsCharlier/repos","events_url":"https://api.github.com/users/NielsCharlier/events{/privacy}","received_events_url":"https://api.github.com/users/NielsCharlier/received_events","type":"User","site_admin":false},"body":"GSIP 139 - GeoServer Resource Browser GUI\r\nhttps://github.com/geoserver/geoserver/wiki/GSIP-139\r\n\r\n[GEOS-7516] Resource Browser (ResourceStore GUI)\r\nhttps://osgeo-org.atlassian.net/browse/GEOS-7516","created_at":"2016-04-18T16:02:22Z","updated_at":"2016-05-23T05:05:24Z","closed_at":null,"merged_at":null,"merge_commit_sha":"9cffb95d006630b6f58d5bd5e3f740883ef7e31c","assignee":{"login":"bencaradocdavies","id":1900866,"avatar_url":"https://avatars.githubusercontent.com/u/1900866?v=3","gravatar_id":"","url":"https://api.github.com/users/bencaradocdavies","html_url":"https://github.com/bencaradocdavies","followers_url":"https://api.github.com/users/bencaradocdavies/followers","following_url":"https://api.github.com/users/bencaradocdavies/following{/other_user}","gists_url":"https://api.github.com/users/bencaradocdavies/gists{/gist_id}","starred_url":"https://api.github.com/users/bencaradocdavies/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bencaradocdavies/subscriptions","organizations_url":"https://api.github.com/users/bencaradocdavies/orgs","repos_url":"https://api.github.com/users/bencaradocdavies/repos","events_url":"https://api.github.com/users/bencaradocdavies/events{/privacy}","received_events_url":"https://api.github.com/users/bencaradocdavies/received_events","type":"User","site_admin":false},"milestone":null,"commits_url":"https://api.github.com/repos/geoserver/geoserver/pulls/1572/commits","review_comments_url":"https://api.github.com/repos/geoserver/geoserver/pulls/1572/comments","review_comment_url":"https://api.github.com/repos/geoserver/geoserver/pulls/comments{/number}","comments_url":"https://api.github.com/repos/geoserver/geoserver/issues/1572/comments","statuses_url":"https://api.github.com/repos/geoserver/geoserver/statuses/1293d4a37732ec90c4bac8c1329272712945d921","head":{"label":"NielsCharlier:webresources","ref":"webresources","sha":"1293d4a37732ec90c4bac8c1329272712945d921","user":{"login":"NielsCharlier","id":1906115,"avatar_url":"https://avatars.githubusercontent.com/u/1906115?v=3","gravatar_id":"","url":"https://api.github.com/users/NielsCharlier","html_url":"https://github.com/NielsCharlier","followers_url":"https://api.github.com/users/NielsCharlier/followers","following_url":"https://api.github.com/users/NielsCharlier/following{/other_user}","gists_url":"https://api.github.com/users/NielsCharlier/gists{/gist_id}","starred_url":"https://api.github.com/users/NielsCharlier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/NielsCharlier/subscriptions","organizations_url":"https://api.github.com/users/NielsCharlier/orgs","repos_url":"https://api.github.com/users/NielsCharlier/repos","events_url":"https://api.github.com/users/NielsCharlier/events{/privacy}","received_events_url":"https://api.github.com/users/NielsCharlier/received_events","type":"User","site_admin":false},"repo":{"id":5855205,"name":"geoserver","full_name":"NielsCharlier/geoserver","owner":{"login":"NielsCharlier","id":1906115,"avatar_url":"https://avatars.githubusercontent.com/u/1906115?v=3","gravatar_id":"","url":"https://api.github.com/users/NielsCharlier","html_url":"https://github.com/NielsCharlier","followers_url":"https://api.github.com/users/NielsCharlier/followers","following_url":"https://api.github.com/users/NielsCharlier/following{/other_user}","gists_url":"https://api.github.com/users/NielsCharlier/gists{/gist_id}","starred_url":"https://api.github.com/users/NielsCharlier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/NielsCharlier/subscriptions","organizations_url":"https://api.github.com/users/NielsCharlier/orgs","repos_url":"https://api.github.com/users/NielsCharlier/repos","events_url":"https://api.github.com/users/NielsCharlier/events{/privacy}","received_events_url":"https://api.github.com/users/NielsCharlier/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/NielsCharlier/geoserver","description":"Official GeoServer repository","fork":true,"url":"https://api.github.com/repos/NielsCharlier/geoserver","forks_url":"https://api.github.com/repos/NielsCharlier/geoserver/forks","keys_url":"https://api.github.com/repos/NielsCharlier/geoserver/keys{/key_id}","collaborators_url":"https://api.github.com/repos/NielsCharlier/geoserver/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/NielsCharlier/geoserver/teams","hooks_url":"https://api.github.com/repos/NielsCharlier/geoserver/hooks","issue_events_url":"https://api.github.com/repos/NielsCharlier/geoserver/issues/events{/number}","events_url":"https://api.github.com/repos/NielsCharlier/geoserver/events","assignees_url":"https://api.github.com/repos/NielsCharlier/geoserver/assignees{/user}","branches_url":"https://api.github.com/repos/NielsCharlier/geoserver/branches{/branch}","tags_url":"https://api.github.com/repos/NielsCharlier/geoserver/tags","blobs_url":"https://api.github.com/repos/NielsCharlier/geoserver/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/NielsCharlier/geoserver/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/NielsCharlier/geoserver/git/refs{/sha}","trees_url":"https://api.github.com/repos/NielsCharlier/geoserver/git/trees{/sha}","statuses_url":"https://api.github.com/repos/NielsCharlier/geoserver/statuses/{sha}","languages_url":"https://api.github.com/repos/NielsCharlier/geoserver/languages","stargazers_url":"https://api.github.com/repos/NielsCharlier/geoserver/stargazers","contributors_url":"https://api.github.com/repos/NielsCharlier/geoserver/contributors","subscribers_url":"https://api.github.com/repos/NielsCharlier/geoserver/subscribers","subscription_url":"https://api.github.com/repos/NielsCharlier/geoserver/subscription","commits_url":"https://api.github.com/repos/NielsCharlier/geoserver/commits{/sha}","git_commits_url":"https://api.github.com/repos/NielsCharlier/geoserver/git/commits{/sha}","comments_url":"https://api.github.com/repos/NielsCharlier/geoserver/comments{/number}","issue_comment_url":"https://api.github.com/repos/NielsCharlier/geoserver/issues/comments{/number}","contents_url":"https://api.github.com/repos/NielsCharlier/geoserver/contents/{+path}","compare_url":"https://api.github.com/repos/NielsCharlier/geoserver/compare/{base}...{head}","merges_url":"https://api.github.com/repos/NielsCharlier/geoserver/merges","archive_url":"https://api.github.com/repos/NielsCharlier/geoserver/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/NielsCharlier/geoserver/downloads","issues_url":"https://api.github.com/repos/NielsCharlier/geoserver/issues{/number}","pulls_url":"https://api.github.com/repos/NielsCharlier/geoserver/pulls{/number}","milestones_url":"https://api.github.com/repos/NielsCharlier/geoserver/milestones{/number}","notifications_url":"https://api.github.com/repos/NielsCharlier/geoserver/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/NielsCharlier/geoserver/labels{/name}","releases_url":"https://api.github.com/repos/NielsCharlier/geoserver/releases{/id}","deployments_url":"https://api.github.com/repos/NielsCharlier/geoserver/deployments","created_at":"2012-09-18T11:22:31Z","updated_at":"2015-05-06T03:50:14Z","pushed_at":"2016-05-19T11:38:26Z","git_url":"git://github.com/NielsCharlier/geoserver.git","ssh_url":"git@github.com:NielsCharlier/geoserver.git","clone_url":"https://github.com/NielsCharlier/geoserver.git","svn_url":"https://github.com/NielsCharlier/geoserver","homepage":"http://geoserver.org","size":130723,"stargazers_count":0,"watchers_count":0,"language":"Java","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"geoserver:master","ref":"master","sha":"fc09ecca650544113a2001ba60abefb92ce77510","user":{"login":"geoserver","id":186522,"avatar_url":"https://avatars.githubusercontent.com/u/186522?v=3","gravatar_id":"","url":"https://api.github.com/users/geoserver","html_url":"https://github.com/geoserver","followers_url":"https://api.github.com/users/geoserver/followers","following_url":"https://api.github.com/users/geoserver/following{/other_user}","gists_url":"https://api.github.com/users/geoserver/gists{/gist_id}","starred_url":"https://api.github.com/users/geoserver/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/geoserver/subscriptions","organizations_url":"https://api.github.com/users/geoserver/orgs","repos_url":"https://api.github.com/users/geoserver/repos","events_url":"https://api.github.com/users/geoserver/events{/privacy}","received_events_url":"https://api.github.com/users/geoserver/received_events","type":"Organization","site_admin":false},"repo":{"id":2751199,"name":"geoserver","full_name":"geoserver/geoserver","owner":{"login":"geoserver","id":186522,"avatar_url":"https://avatars.githubusercontent.com/u/186522?v=3","gravatar_id":"","url":"https://api.github.com/users/geoserver","html_url":"https://github.com/geoserver","followers_url":"https://api.github.com/users/geoserver/followers","following_url":"https://api.github.com/users/geoserver/following{/other_user}","gists_url":"https://api.github.com/users/geoserver/gists{/gist_id}","starred_url":"https://api.github.com/users/geoserver/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/geoserver/subscriptions","organizations_url":"https://api.github.com/users/geoserver/orgs","repos_url":"https://api.github.com/users/geoserver/repos","events_url":"https://api.github.com/users/geoserver/events{/privacy}","received_events_url":"https://api.github.com/users/geoserver/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/geoserver/geoserver","description":"Official GeoServer repository","fork":false,"url":"https://api.github.com/repos/geoserver/geoserver","forks_url":"https://api.github.com/repos/geoserver/geoserver/forks","keys_url":"https://api.github.com/repos/geoserver/geoserver/keys{/key_id}","collaborators_url":"https://api.github.com/repos/geoserver/geoserver/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/geoserver/geoserver/teams","hooks_url":"https://api.github.com/repos/geoserver/geoserver/hooks","issue_events_url":"https://api.github.com/repos/geoserver/geoserver/issues/events{/number}","events_url":"https://api.github.com/repos/geoserver/geoserver/events","assignees_url":"https://api.github.com/repos/geoserver/geoserver/assignees{/user}","branches_url":"https://api.github.com/repos/geoserver/geoserver/branches{/branch}","tags_url":"https://api.github.com/repos/geoserver/geoserver/tags","blobs_url":"https://api.github.com/repos/geoserver/geoserver/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/geoserver/geoserver/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/geoserver/geoserver/git/refs{/sha}","trees_url":"https://api.github.com/repos/geoserver/geoserver/git/trees{/sha}","statuses_url":"https://api.github.com/repos/geoserver/geoserver/statuses/{sha}","languages_url":"https://api.github.com/repos/geoserver/geoserver/languages","stargazers_url":"https://api.github.com/repos/geoserver/geoserver/stargazers","contributors_url":"https://api.github.com/repos/geoserver/geoserver/contributors","subscribers_url":"https://api.github.com/repos/geoserver/geoserver/subscribers","subscription_url":"https://api.github.com/repos/geoserver/geoserver/subscription","commits_url":"https://api.github.com/repos/geoserver/geoserver/commits{/sha}","git_commits_url":"https://api.github.com/repos/geoserver/geoserver/git/commits{/sha}","comments_url":"https://api.github.com/repos/geoserver/geoserver/comments{/number}","issue_comment_url":"https://api.github.com/repos/geoserver/geoserver/issues/comments{/number}","contents_url":"https://api.github.com/repos/geoserver/geoserver/contents/{+path}","compare_url":"https://api.github.com/repos/geoserver/geoserver/compare/{base}...{head}","merges_url":"https://api.github.com/repos/geoserver/geoserver/merges","archive_url":"https://api.github.com/repos/geoserver/geoserver/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/geoserver/geoserver/downloads","issues_url":"https://api.github.com/repos/geoserver/geoserver/issues{/number}","pulls_url":"https://api.github.com/repos/geoserver/geoserver/pulls{/number}","milestones_url":"https://api.github.com/repos/geoserver/geoserver/milestones{/number}","notifications_url":"https://api.github.com/repos/geoserver/geoserver/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/geoserver/geoserver/labels{/name}","releases_url":"https://api.github.com/repos/geoserver/geoserver/releases{/id}","deployments_url":"https://api.github.com/repos/geoserver/geoserver/deployments","created_at":"2011-11-10T19:56:06Z","updated_at":"2016-05-23T01:25:59Z","pushed_at":"2016-05-21T19:41:50Z","git_url":"git://github.com/geoserver/geoserver.git","ssh_url":"git@github.com:geoserver/geoserver.git","clone_url":"https://github.com/geoserver/geoserver.git","svn_url":"https://github.com/geoserver/geoserver","homepage":"http://geoserver.org/","size":147357,"stargazers_count":600,"watchers_count":600,"language":"Java","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":678,"mirror_url":null,"open_issues_count":19,"forks":678,"open_issues":19,"watchers":600,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/geoserver/geoserver/pulls/1572"},"html":{"href":"https://github.com/geoserver/geoserver/pull/1572"},"issue":{"href":"https://api.github.com/repos/geoserver/geoserver/issues/1572"},"comments":{"href":"https://api.github.com/repos/geoserver/geoserver/issues/1572/comments"},"review_comments":{"href":"https://api.github.com/repos/geoserver/geoserver/pulls/1572/comments"},"review_comment":{"href":"https://api.github.com/repos/geoserver/geoserver/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/geoserver/geoserver/pulls/1572/commits"},"statuses":{"href":"https://api.github.com/repos/geoserver/geoserver/statuses/1293d4a37732ec90c4bac8c1329272712945d921"}}}}
{"actor":{"display_login":"bencaradocdavies"}}