diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..7bd36e0adc --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line=lf +insert_final_newline = true +indent_style = space +indent_size = 2 +charset = utf-8 + +# 4 space indentation +[*.py] +indent_style = space +indent_size = 4 diff --git a/.hgignore b/.hgignore index 14e7f7a85d..a49329d7a5 100644 --- a/.hgignore +++ b/.hgignore @@ -1,6 +1,7 @@ # netbeans temp & private files /?target/ nbactions.*\.xml +/?nbproject/ nb-configuration\.xml # MacOS X Files \.DS_Store$ @@ -25,3 +26,5 @@ Desktop DF$ # idea files \.iml \.idea$ +# jrebel +rebel.xml diff --git a/pom.xml b/pom.xml index f40b5f943b..5c22ea0433 100644 --- a/pom.xml +++ b/pom.xml @@ -262,7 +262,7 @@ http://download.oracle.com/javase/6/docs/api/ http://download.oracle.com/docs/cd/E17802_01/products/products/servlet/2.5/docs/servlet-2_5-mr2/ http://jersey.java.net/nonav/apidocs/${jersey.version}/jersey/ - http://google-guice.googlecode.com/svn/tags/3.0/javadoc/ + https://google.github.io/guice/api-docs/${guice.version}/javadoc http://www.slf4j.org/api/ http://shiro.apache.org/static/current/apidocs/ @@ -274,7 +274,7 @@ maven-source-plugin 2.2.1 - + org.apache.maven.plugins maven-clean-plugin @@ -419,16 +419,6 @@ - - JDK7 - - 1.7 - - - -jdk7 - - - APIviz @@ -449,8 +439,8 @@ org.jboss.apiviz.APIviz org.jboss.apiviz - apiviz${jdk.classifier} - 1.3.1.GA + apiviz + 1.3.2.GA -sourceclasspath ${project.build.outputDirectory} @@ -484,40 +474,41 @@ - 1.10.8 + 1.10.19 1.3 - 4.11 + 4.12 - 1.7.7 - 1.1.2 + 1.7.21 + 1.1.7 3.0.1 4.0 - 1.18.2 + 1.19.1 1.2.0 9.2.10.v20150310 + 9.2.10.v20150310 1.0.0-SNAPSHOT 1.4.0-RC2 - 3.4.1.201406201815-r + 4.4.0.201606070830-r-scm1 1.8.5-scm3 16.0.1 + 2.2.3 1.8 - 1.8 UTF-8 SCM-BSD - \ No newline at end of file + diff --git a/scm-clients/scm-cli-client/pom.xml b/scm-clients/scm-cli-client/pom.xml index 1be6036351..e5605878f8 100644 --- a/scm-clients/scm-cli-client/pom.xml +++ b/scm-clients/scm-cli-client/pom.xml @@ -168,7 +168,7 @@ org.eclipse.jetty jetty-maven-plugin - ${jetty.version} + ${jetty.maven.version} 8085 STOP diff --git a/scm-clients/scm-client-api/pom.xml b/scm-clients/scm-client-api/pom.xml index 3a88bedcc2..d2f8692ca7 100644 --- a/scm-clients/scm-client-api/pom.xml +++ b/scm-clients/scm-client-api/pom.xml @@ -35,4 +35,4 @@ - \ No newline at end of file + diff --git a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/FileObjectWrapper.java b/scm-clients/scm-client-api/src/main/java/sonia/scm/client/FileObjectWrapper.java index 1c6c0aaeda..ddbeef69a3 100644 --- a/scm-clients/scm-client-api/src/main/java/sonia/scm/client/FileObjectWrapper.java +++ b/scm-clients/scm-client-api/src/main/java/sonia/scm/client/FileObjectWrapper.java @@ -66,6 +66,7 @@ public class FileObjectWrapper String revision, FileObject file) { this.repositoryBrowser = repositoryBrowser; + this.revision = revision; this.file = file; } diff --git a/scm-clients/scm-client-impl/pom.xml b/scm-clients/scm-client-impl/pom.xml index 3a1f09ce40..68ff65317f 100644 --- a/scm-clients/scm-client-impl/pom.xml +++ b/scm-clients/scm-client-impl/pom.xml @@ -176,7 +176,7 @@ org.eclipse.jetty jetty-maven-plugin - ${jetty.version} + ${jetty.maven.version} 8085 STOP @@ -224,4 +224,4 @@ - \ No newline at end of file + diff --git a/scm-core/pom.xml b/scm-core/pom.xml index 5712172e07..f08de28f26 100644 --- a/scm-core/pom.xml +++ b/scm-core/pom.xml @@ -164,7 +164,7 @@ http://download.oracle.com/javase/6/docs/api/ http://download.oracle.com/docs/cd/E17802_01/products/products/servlet/2.5/docs/servlet-2_5-mr2/ http://jersey.java.net/nonav/apidocs/${jersey.version}/jersey/ - http://google-guice.googlecode.com/svn/tags/${guice.version}/javadoc/ + https://google.github.io/guice/api-docs/${guice.version}/javadoc http://www.slf4j.org/api/ http://shiro.apache.org/static/${shiro.version}/apidocs/ diff --git a/scm-core/src/main/java/sonia/scm/BasicContextProvider.java b/scm-core/src/main/java/sonia/scm/BasicContextProvider.java index 35dd2dcca0..f6507fc453 100644 --- a/scm-core/src/main/java/sonia/scm/BasicContextProvider.java +++ b/scm-core/src/main/java/sonia/scm/BasicContextProvider.java @@ -96,7 +96,7 @@ public class BasicContextProvider implements SCMContextProvider version = loadVersion(); stage = loadProjectStage(); } - catch (Throwable ex) + catch (Exception ex) { this.startupError = ex; diff --git a/scm-core/src/main/java/sonia/scm/ModificationHandlerEvent.java b/scm-core/src/main/java/sonia/scm/ModificationHandlerEvent.java new file mode 100644 index 0000000000..ce072ba7b1 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/ModificationHandlerEvent.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm; + +import sonia.scm.event.HandlerEvent; + +/** + * Extension to the {@link ModificationHandlerEvent}. + * + * @param type of changed item + * + * @author Sebastian Sdorra + * @since 1.48 + */ +public interface ModificationHandlerEvent extends HandlerEvent +{ + /** + * Returns item, before it was modified. + * + * @return item before modification + */ + public T getItemBeforeModification(); +} diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java index e6c96a903b..e0916f2460 100644 --- a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java @@ -134,6 +134,7 @@ public class ScmConfiguration this.skipFailedAuthenticators = other.skipFailedAuthenticators; this.loginAttemptLimit = other.loginAttemptLimit; this.loginAttemptLimitTimeout = other.loginAttemptLimitTimeout; + this.enabledXsrfProtection = other.enabledXsrfProtection; } //~--- get methods ---------------------------------------------------------- @@ -325,6 +326,19 @@ public class ScmConfiguration return disableGroupingGrid; } + /** + * Returns {@code true} if the cookie xsrf protection is enabled. + * + * @see Issue 793 + * @return {@code true} if the cookie xsrf protection is enabled + * + * @since 1.47 + */ + public boolean isEnabledXsrfProtection() + { + return enabledXsrfProtection; + } + /** * Returns true if proxy is enabled. * @@ -611,6 +625,19 @@ public class ScmConfiguration this.skipFailedAuthenticators = skipFailedAuthenticators; } + /** + * Set {@code true} to enable xsrf cookie protection. + * + * @param enabledXsrfProtection {@code true} to enable xsrf protection + * @see Issue 793 + * + * @since 1.47 + */ + public void setEnabledXsrfProtection(boolean enabledXsrfProtection) + { + this.enabledXsrfProtection = enabledXsrfProtection; + } + //~--- fields --------------------------------------------------------------- /** Field description */ @@ -700,4 +727,12 @@ public class ScmConfiguration /** Field description */ private boolean anonymousAccessEnabled = false; + + /** + * Enables xsrf cookie protection. + * + * @since 1.47 + */ + @XmlElement(name = "xsrf-protection") + private boolean enabledXsrfProtection = false; } diff --git a/scm-core/src/main/java/sonia/scm/group/AbstractGroupManager.java b/scm-core/src/main/java/sonia/scm/group/AbstractGroupManager.java index 6715f322a7..12dcb8cd17 100644 --- a/scm-core/src/main/java/sonia/scm/group/AbstractGroupManager.java +++ b/scm-core/src/main/java/sonia/scm/group/AbstractGroupManager.java @@ -48,18 +48,18 @@ public abstract class AbstractGroupManager implements GroupManager { /** - * Send a {@link GroupEvent} to the {@link ScmEventBus}. + * Creates a {@link GroupEvent} and send it to the {@link ScmEventBus}. * * @param event type of change event * @param group group that has changed */ protected void fireEvent(HandlerEventType event, Group group) { - ScmEventBus.getInstance().post(new GroupEvent(event, group)); + fireEvent(new GroupEvent(event, group)); } /** - * Send a {@link GroupEvent} to the {@link ScmEventBus}. + * Creates a {@link GroupModificationEvent} and send it to the {@link ScmEventBus}. * * @param event type of change event * @param group group that has changed @@ -67,6 +67,16 @@ public abstract class AbstractGroupManager implements GroupManager */ protected void fireEvent(HandlerEventType event, Group group, Group oldGroup) { - ScmEventBus.getInstance().post(new GroupEvent(event, group, oldGroup)); + fireEvent(new GroupModificationEvent(event, group, oldGroup)); + } + + /** + * Sends a {@link GroupEvent} to the {@link ScmEventBus}. + * + * @param event group event + */ + protected void fireEvent(GroupEvent event) + { + ScmEventBus.getInstance().post(event); } } diff --git a/scm-core/src/main/java/sonia/scm/group/GroupEvent.java b/scm-core/src/main/java/sonia/scm/group/GroupEvent.java index 33ef97d208..7328736ea7 100644 --- a/scm-core/src/main/java/sonia/scm/group/GroupEvent.java +++ b/scm-core/src/main/java/sonia/scm/group/GroupEvent.java @@ -47,7 +47,7 @@ import sonia.scm.event.Event; * @since 1.23 */ @Event -public final class GroupEvent extends AbstractHandlerEvent +public class GroupEvent extends AbstractHandlerEvent { /** diff --git a/scm-core/src/main/java/sonia/scm/group/GroupModificationEvent.java b/scm-core/src/main/java/sonia/scm/group/GroupModificationEvent.java new file mode 100644 index 0000000000..796800b14d --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/group/GroupModificationEvent.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.group; + +import sonia.scm.HandlerEventType; +import sonia.scm.ModificationHandlerEvent; + +/** + * Event which is fired whenever a group is modified. + * + * @author Sebastian Sdorra + * @since 1.48 + */ +public class GroupModificationEvent extends GroupEvent implements ModificationHandlerEvent +{ + + private final Group itemBeforeModification; + + /** + * Constructs a new {@link GroupModificationEvent}. + * + * @param eventType type of event + * @param item changed group + * @param itemBeforeModification changed group before it was modified + */ + public GroupModificationEvent(HandlerEventType eventType, Group item, Group itemBeforeModification) + { + super(eventType, item); + this.itemBeforeModification = itemBeforeModification; + } + + @Override + public Group getItemBeforeModification() + { + return itemBeforeModification; + } + +} diff --git a/scm-core/src/main/java/sonia/scm/net/HttpClient.java b/scm-core/src/main/java/sonia/scm/net/HttpClient.java index b9b891f557..6af89bcb71 100644 --- a/scm-core/src/main/java/sonia/scm/net/HttpClient.java +++ b/scm-core/src/main/java/sonia/scm/net/HttpClient.java @@ -48,7 +48,10 @@ import java.util.Map; * @apiviz.landmark * @apiviz.uses sonia.scm.net.HttpRequest * @apiviz.uses sonia.scm.net.HttpResponse + * + * @deprecated use {@link sonia.scm.net.ahc.AdvancedHttpClient} instead. */ +@Deprecated public interface HttpClient { diff --git a/scm-core/src/main/java/sonia/scm/net/HttpRequest.java b/scm-core/src/main/java/sonia/scm/net/HttpRequest.java index f3ccf9719c..3f171a3fa7 100644 --- a/scm-core/src/main/java/sonia/scm/net/HttpRequest.java +++ b/scm-core/src/main/java/sonia/scm/net/HttpRequest.java @@ -51,7 +51,11 @@ import java.util.Map; * * @author Sebastian Sdorra * @since 1.9 + * + * @deprecated use {@link sonia.scm.net.ahc.AdvancedHttpRequest} or + * {@link sonia.scm.net.ahc.AdvancedHttpRequestWithBody} instead. */ +@Deprecated public class HttpRequest { diff --git a/scm-core/src/main/java/sonia/scm/net/HttpResponse.java b/scm-core/src/main/java/sonia/scm/net/HttpResponse.java index 83cdf9ee61..ef297c2809 100644 --- a/scm-core/src/main/java/sonia/scm/net/HttpResponse.java +++ b/scm-core/src/main/java/sonia/scm/net/HttpResponse.java @@ -41,12 +41,16 @@ import java.io.InputStream; import java.util.List; import java.util.Map; +import sonia.scm.net.ahc.AdvancedHttpResponse; /** * Response of a {@link HttpRequest} execute by the {@link HttpClient}. * * @author Sebastian Sdorra + * + * @deprecated use {@link AdvancedHttpResponse} instead */ +@Deprecated public interface HttpResponse extends Closeable { diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/AdvancedHttpClient.java b/scm-core/src/main/java/sonia/scm/net/ahc/AdvancedHttpClient.java new file mode 100644 index 0000000000..aa660f21ca --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/ahc/AdvancedHttpClient.java @@ -0,0 +1,202 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.IOException; + +/** + * Advanced client for http operations. The {@link AdvancedHttpClient} replaces + * the much more simpler implementation {@link sonia.scm.net.HttpClient}. The + * {@link AdvancedHttpClient} offers a fluid interface for handling most common + * http operations. The {@link AdvancedHttpClient} can be injected by the + * default injection mechanism of SCM-Manager. + *

 

+ * Http GET example: + * + *

+ * AdvancedHttpResponse response = client.get("https://www.scm-manager.org")
+ *                                       .decodeGZip(true)
+ *                                       .request();
+ *
+ * System.out.println(response.contentAsString());
+ * 
+ * + *

 

+ * Http POST example: + * + *

+ * AdvancedHttpResponse response = client.post("https://www.scm-manager.org")
+ *                                       .formContent()
+ *                                       .field("firstname", "Tricia")
+ *                                       .field("lastname", "McMillan")
+ *                                       .build()
+ *                                       .request();
+ *
+ * if (response.isSuccessful()){
+ *   System.out.println("success");
+ * }
+ * 
+ * + * @author Sebastian Sdorra + * @since 1.46 + * + * @apiviz.landmark + */ +public abstract class AdvancedHttpClient +{ + + /** + * Creates a {@link ContentTransformer} for the given Content-Type. + * + * @param type object type + * @param contentType content-type + * @throws ContentTransformerNotFoundException if no + * {@link ContentTransformer} could be found for the content-type + * + * @return {@link ContentTransformer} + */ + protected abstract ContentTransformer createTransformer(Class type, + String contentType); + + /** + * Executes the given request and returns the http response. Implementation + * have to check, if the instance if from type + * {@link AdvancedHttpRequestWithBody} in order to handle request contents. + * + * + * @param request request to execute + * + * @return http response + * + * @throws IOException + */ + protected abstract AdvancedHttpResponse request(BaseHttpRequest request) + throws IOException; + + /** + * Returns a builder for a DELETE request. + * + * + * @param url request url + * + * @return request builder + */ + public AdvancedHttpRequestWithBody delete(String url) + { + return new AdvancedHttpRequestWithBody(this, HttpMethod.DELETE, url); + } + + /** + * Returns a builder for a HEAD request. + * + * + * @param url request url + * + * @return request builder + */ + public AdvancedHttpRequest head(String url) + { + return new AdvancedHttpRequest(this, HttpMethod.HEAD, url); + } + + /** + * Returns a request builder with a custom method. Note: not + * every method is supported by the underlying implementation of the http + * client. + * + * + * @param method http method + * @param url request url + * + * @return request builder + */ + public AdvancedHttpRequestWithBody method(String method, String url) + { + return new AdvancedHttpRequestWithBody(this, method, url); + } + + /** + * Returns a builder for a OPTIONS request. + * + * + * @param url request url + * + * @return request builder + */ + public AdvancedHttpRequestWithBody options(String url) + { + return new AdvancedHttpRequestWithBody(this, HttpMethod.OPTIONS, url); + } + + /** + * Returns a builder for a POST request. + * + * + * @param url request url + * + * @return request builder + */ + public AdvancedHttpRequestWithBody post(String url) + { + return new AdvancedHttpRequestWithBody(this, HttpMethod.POST, url); + } + + /** + * Returns a builder for a PUT request. + * + * + * @param url request url + * + * @return request builder + */ + public AdvancedHttpRequestWithBody put(String url) + { + return new AdvancedHttpRequestWithBody(this, HttpMethod.PUT, url); + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Returns a builder for a GET request. + * + * + * @param url request url + * + * @return request builder + */ + public AdvancedHttpRequest get(String url) + { + return new AdvancedHttpRequest(this, HttpMethod.GET, url); + } +} diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/AdvancedHttpRequest.java b/scm-core/src/main/java/sonia/scm/net/ahc/AdvancedHttpRequest.java new file mode 100644 index 0000000000..b8940628b4 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/ahc/AdvancedHttpRequest.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +//~--- JDK imports ------------------------------------------------------------ + +/** + * An http request without {@link Content} for example a GET or HEAD request. + * + * @author Sebastian Sdorra + * @since 1.46 + */ +public class AdvancedHttpRequest extends BaseHttpRequest +{ + + /** + * Constructs a new {@link AdvancedHttpRequest} + * + * + * @param client + * @param method + * @param url + */ + AdvancedHttpRequest(AdvancedHttpClient client, String method, + String url) + { + super(client, method, url); + } + + //~--- methods -------------------------------------------------------------- + + /** + * Returns {@code this}. + * + * + * @return {@code this} + */ + @Override + protected AdvancedHttpRequest self() + { + return this; + } +} diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBody.java b/scm-core/src/main/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBody.java new file mode 100644 index 0000000000..dd5ec6f425 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBody.java @@ -0,0 +1,271 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Charsets; +import com.google.common.io.ByteSource; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.File; + +import java.nio.charset.Charset; + +/** + * Http request with body. + * + * @author Sebastian Sdorra + * @since 1.46 + */ +public class AdvancedHttpRequestWithBody + extends BaseHttpRequest +{ + + /** + * Constructs a new {@link AdvancedHttpRequestWithBody}. + * + * + * @param client http client + * @param method http method + * @param url url + */ + AdvancedHttpRequestWithBody(AdvancedHttpClient client, String method, + String url) + { + super(client, method, url); + } + + //~--- methods -------------------------------------------------------------- + + /** + * Sets the content length for the request. + * + * + * @param length content length + * + * @return {@code this} + */ + public AdvancedHttpRequestWithBody contentLength(long length) + { + return header("Content-Length", String.valueOf(length)); + } + + /** + * Sets the content type for the request. + * + * + * @param contentType content type + * + * @return {@code this} + */ + public AdvancedHttpRequestWithBody contentType(String contentType) + { + return header("Content-Type", contentType); + } + + /** + * Sets the content of the file as request content. + * + * + * @param file file + * + * @return {@code this} + */ + public AdvancedHttpRequestWithBody fileContent(File file) + { + this.content = new FileContent(file); + + return this; + } + + /** + * Returns a {@link FormContentBuilder}. The builder can be used to add form + * parameters as content for the request. Note: you have to + * call {@link FormContentBuilder#build()} in order to apply the form content + * to the request. + * + * @return form content builder + */ + public FormContentBuilder formContent() + { + return new FormContentBuilder(this); + } + + /** + * Transforms the given object to a xml string and set this string as request + * content. + * + * @param object object to transform + * @throws ContentTransformerNotFoundException if no + * {@link ContentTransformer} could be found for the json content-type + * + * @return {@code this} + */ + public AdvancedHttpRequestWithBody jsonContent(Object object) + { + return transformedContent(ContentType.JSON, object); + } + + /** + * Sets the raw data as request content. + * + * + * @param data raw data + * + * @return {@code this} + */ + public AdvancedHttpRequestWithBody rawContent(byte[] data) + { + this.content = new RawContent(data); + + return this; + } + + /** + * Sets the raw data as request content. + * + * + * @param source byte source + * + * @return {@code this} + */ + public AdvancedHttpRequestWithBody rawContent(ByteSource source) + { + this.content = new ByteSourceContent(source); + + return this; + } + + /** + * Sets the string as request content. + * + * + * @param content string content + * + * @return {@code this} + */ + public AdvancedHttpRequestWithBody stringContent(String content) + { + return stringContent(content, Charsets.UTF_8); + } + + /** + * Sets the string as request content. + * + * + * @param content string content + * @param charset charset of content + * + * @return {@code this} + */ + public AdvancedHttpRequestWithBody stringContent(String content, + Charset charset) + { + this.content = new StringContent(content, charset); + + return this; + } + + /** + * Transforms the given object to a string and set this string as request + * content. The content-type is used to pick the right + * {@link ContentTransformer}. The method will throw an exception if no + * {@link ContentTransformer} for the content-type could be found. + * + * @param contentType content-type + * @param object object to transform + * @throws ContentTransformerNotFoundException if no + * {@link ContentTransformer} could be found for the given content-type + * + * @return {@code this} + */ + public AdvancedHttpRequestWithBody transformedContent(String contentType, + Object object) + { + ContentTransformer transformer = + client.createTransformer(object.getClass(), contentType); + ByteSource value = transformer.marshall(object); + + contentType(contentType); + + return rawContent(value); + } + + /** + * Transforms the given object to a xml string and set this string as request + * content. + * + * @param object object to transform + * @throws ContentTransformerNotFoundException if no + * {@link ContentTransformer} could be found for the xml content-type + * + * @return {@code this} + */ + public AdvancedHttpRequestWithBody xmlContent(Object object) + { + return transformedContent(ContentType.XML, object); + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Returns the content or the request. + * + * + * @return request content + */ + public Content getContent() + { + return content; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Returns {@code this}. + * + * + * @return {@code this} + */ + @Override + protected AdvancedHttpRequestWithBody self() + { + return this; + } + + //~--- fields --------------------------------------------------------------- + + /** request content */ + private Content content; +} diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/AdvancedHttpResponse.java b/scm-core/src/main/java/sonia/scm/net/ahc/AdvancedHttpResponse.java new file mode 100644 index 0000000000..1cb087710b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/ahc/AdvancedHttpResponse.java @@ -0,0 +1,312 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Charsets; +import com.google.common.base.Strings; +import com.google.common.collect.Iterables; +import com.google.common.collect.Multimap; +import com.google.common.io.ByteSource; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; + +/** + * Http response. The response of a {@link AdvancedHttpRequest} or + * {@link AdvancedHttpRequestWithBody}. + * + * @author Sebastian Sdorra + * @since 1.46 + */ +public abstract class AdvancedHttpResponse +{ + + /** + * Returns the response content as byte source. + * + * + * @return response content as byte source + * @throws IOException + */ + public abstract ByteSource contentAsByteSource() throws IOException; + + //~--- get methods ---------------------------------------------------------- + + /** + * Returns the response headers. + * + * + * @return response headers + */ + public abstract Multimap getHeaders(); + + /** + * Returns the status code of the response. + * + * + * @return status code + */ + public abstract int getStatus(); + + /** + * Returns the status text of the response. + * + * + * @return status text + */ + public abstract String getStatusText(); + + //~--- methods -------------------------------------------------------------- + + /** + * Creates a {@link ContentTransformer} for the given Content-Type. + * + * @param type object type + * @param contentType content-type + * @throws ContentTransformerNotFoundException if no + * {@link ContentTransformer} could be found for the content-type + * + * @return {@link ContentTransformer} + */ + protected abstract ContentTransformer createTransformer(Class type, + String contentType); + + /** + * Returns the content of the response as byte array. + * + * + * @return content as byte array + * + * @throws IOException + */ + public byte[] content() throws IOException + { + ByteSource content = contentAsByteSource(); + byte[] data = null; + + if (content != null) + { + data = content.read(); + } + + return data; + } + + /** + * Returns a reader for the content of the response. + * + * + * @return read of response content + * + * @throws IOException + */ + public BufferedReader contentAsReader() throws IOException + { + ByteSource content = contentAsByteSource(); + BufferedReader reader = null; + + if (content != null) + { + reader = content.asCharSource(Charsets.UTF_8).openBufferedStream(); + } + + return reader; + } + + /** + * Returns response content as stream. + * + * + * @return response content as stram + * + * @throws IOException + */ + public InputStream contentAsStream() throws IOException + { + ByteSource content = contentAsByteSource(); + InputStream stream = null; + + if (content != null) + { + stream = content.openBufferedStream(); + } + + return stream; + } + + /** + * Returns the response content as string. + * + * + * @return response content + * + * @throws IOException + */ + public String contentAsString() throws IOException + { + ByteSource content = contentAsByteSource(); + String value = null; + + if (content != null) + { + value = content.asCharSource(Charsets.UTF_8).read(); + } + + return value; + } + + /** + * Transforms the response content from json to the given type. + * + * @param object type + * @param type object type + * + * @throws ContentTransformerNotFoundException if no + * {@link ContentTransformer} could be found for the json content-type + * + * @return transformed object + * + * @throws IOException + */ + public T contentFromJson(Class type) throws IOException + { + return contentTransformed(type, ContentType.JSON); + } + + /** + * Transforms the response content from xml to the given type. + * + * @param object type + * @param type object type + * + * @throws ContentTransformerNotFoundException if no + * {@link ContentTransformer} could be found for the xml content-type + * + * @return transformed object + * + * @throws IOException + */ + public T contentFromXml(Class type) throws IOException + { + return contentTransformed(type, ContentType.XML); + } + + /** + * Transforms the response content to the given type. The method uses the + * content-type header to pick the right {@link ContentTransformer}. + * + * @param object type + * @param type object type + * + * @throws ContentTransformerNotFoundException if no + * {@link ContentTransformer} could be found for the content-type + * + * @return transformed object + * + * @throws IOException + */ + public T contentTransformed(Class type) throws IOException + { + String contentType = getFirstHeader("Content-Type"); + + if (Strings.isNullOrEmpty(contentType)) + { + throw new ContentTransformerException( + "response does not return a Content-Type header"); + } + + return contentTransformed(type, contentType); + } + + /** + * Transforms the response content from xml to the given type. + * + * @param object type + * @param type object type + * @param contentType type to pick {@link ContentTransformer} + * + * @throws ContentTransformerNotFoundException if no + * {@link ContentTransformer} could be found for the content-type + * + * @return transformed object + * + * @throws IOException + */ + public T contentTransformed(Class type, String contentType) + throws IOException + { + T object = null; + ByteSource source = contentAsByteSource(); + + if (source != null) + { + ContentTransformer transformer = createTransformer(type, contentType); + + object = transformer.unmarshall(type, contentAsByteSource()); + } + + return object; + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Returns the first header value for the given header name or {@code null}. + * + * + * @param name header name + * + * @return header value or {@code null} + */ + public String getFirstHeader(String name) + { + return Iterables.getFirst(getHeaders().get(name), null); + } + + /** + * Returns {@code true} if the response was successful. A response is + * successful, if the status code is greater than 199 and lower than 400. + * + * @return {@code true} if the response was successful + */ + public boolean isSuccessful() + { + int status = getStatus(); + + return (status > 199) && (status < 400); + } +} diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/BaseHttpRequest.java b/scm-core/src/main/java/sonia/scm/net/ahc/BaseHttpRequest.java new file mode 100644 index 0000000000..c80ae9b1c4 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/ahc/BaseHttpRequest.java @@ -0,0 +1,409 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Charsets; +import com.google.common.base.Strings; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; + +import org.apache.shiro.codec.Base64; + +import sonia.scm.util.HttpUtil; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.IOException; + +/** + * Base class for http requests. + * + * @author Sebastian Sdorra + * @param request implementation + * + * @since 1.46 + */ +public abstract class BaseHttpRequest +{ + + /** + * Constructs a new {@link BaseHttpRequest}. + * + * + * @param client http client + * @param method http method + * @param url url + */ + public BaseHttpRequest(AdvancedHttpClient client, String method, String url) + { + this.client = client; + this.method = method; + this.url = url; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Executes the request and returns the http response. + * + * + * @return http response + * + * @throws IOException + */ + public AdvancedHttpResponse request() throws IOException + { + return client.request(this); + } + + /** + * Implementing classes should return {@code this}. + * + * + * @return request instance + */ + protected abstract T self(); + + /** + * Enabled http basic authentication. + * + * + * @param username username for http basic authentication + * @param password password for http basic authentication + * + * @return http request instance + */ + public T basicAuth(String username, String password) + { + String auth = Strings.nullToEmpty(username).concat(":").concat( + Strings.nullToEmpty(password)); + + auth = Base64.encodeToString(auth.getBytes(Charsets.ISO_8859_1)); + headers.put("Authorization", "Basic ".concat(auth)); + + return self(); + } + + /** + * Enable or disabled gzip decoding. The default value is false. + * + * + * @param decodeGZip true to enable gzip decoding + * + * @return request instance + */ + public T decodeGZip(boolean decodeGZip) + { + this.decodeGZip = decodeGZip; + + return self(); + } + + /** + * Enable or disable certificate validation of ssl certificates. The default + * value is false. + * + * + * @param disableCertificateValidation true to disable certificate validation + * + * @return request instance + */ + public T disableCertificateValidation(boolean disableCertificateValidation) + { + this.disableCertificateValidation = disableCertificateValidation; + + return self(); + } + + /** + * Enable or disable the validation of ssl hostnames. The default value is + * false. + * + * + * @param disableHostnameValidation true to disable ssl hostname validation + * + * @return request instance + */ + public T disableHostnameValidation(boolean disableHostnameValidation) + { + this.disableHostnameValidation = disableHostnameValidation; + + return self(); + } + + /** + * Add http headers to request. + * + * + * @param name header name + * @param values header values + * + * @return request instance + */ + public T header(String name, Object... values) + { + for (Object v : values) + { + headers.put(name, toString(v)); + } + + return self(); + } + + /** + * Add http headers to request. + * + * + * @param name header name + * @param values header values + * + * @return request instance + */ + public T headers(String name, Iterable values) + { + for (Object v : values) + { + headers.put(name, toString(v)); + } + + return self(); + } + + /** + * Ignore proxy settings. The default value is false. + * + * + * @param ignoreProxySettings true to ignore proxy settings. + * + * @return request instance + */ + public T ignoreProxySettings(boolean ignoreProxySettings) + { + this.ignoreProxySettings = ignoreProxySettings; + + return self(); + } + + /** + * Appends a query parameter to the request. + * + * + * @param name name of query parameter + * @param values query parameter values + * + * @return request instance + */ + public T queryString(String name, Object... values) + { + for (Object v : values) + { + appendQueryString(name, v); + } + + return self(); + } + + /** + * Appends a query parameter to the request. + * + * + * @param name name of query parameter + * @param values query parameter values + * + * @return request instance + */ + public T queryStrings(String name, Iterable values) + { + for (Object v : values) + { + appendQueryString(name, v); + } + + return self(); + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Return a map with http headers used for the request. + * + * + * @return map with http headers + */ + public Multimap getHeaders() + { + return headers; + } + + /** + * Returns the http method for the request. + * + * + * @return http method of request + */ + public String getMethod() + { + return method; + } + + /** + * Returns the url for the request. + * + * + * @return url of the request + */ + public String getUrl() + { + return url; + } + + /** + * Returns true if the request decodes gzip compression. + * + * + * @return true if the request decodes gzip compression + */ + public boolean isDecodeGZip() + { + return decodeGZip; + } + + /** + * Returns true if the verification of ssl certificates is disabled. + * + * + * @return true if certificate verification is disabled + */ + public boolean isDisableCertificateValidation() + { + return disableCertificateValidation; + } + + /** + * Returns true if the ssl hostname validation is disabled. + * + * + * @return true if the ssl hostname validation is disabled + */ + public boolean isDisableHostnameValidation() + { + return disableHostnameValidation; + } + + /** + * Returns true if the proxy settings are ignored. + * + * + * @return true if the proxy settings are ignored + */ + public boolean isIgnoreProxySettings() + { + return ignoreProxySettings; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Returns the value url encoded. + * + * + * @param value value to encode + * + * @return encoded value + */ + protected String encoded(Object value) + { + return HttpUtil.encode(Strings.nullToEmpty(toString(value))); + } + + /** + * Returns string representation of the given object or {@code null}, if the + * object is {@code null}. + * + * + * @param object object + * + * @return string representation or {@code null} + */ + protected String toString(Object object) + { + return (object != null) + ? object.toString() + : null; + } + + private void appendQueryString(String name, Object value) + { + StringBuilder buffer = new StringBuilder(); + + if (url.contains("?")) + { + buffer.append("&"); + } + else + { + buffer.append("?"); + } + + buffer.append(encoded(name)).append("=").append(encoded(value)); + url = url.concat(buffer.toString()); + } + + //~--- fields --------------------------------------------------------------- + + /** http client */ + protected final AdvancedHttpClient client; + + /** http header */ + private final Multimap headers = LinkedHashMultimap.create(); + + /** http method */ + private final String method; + + /** ignore proxy settings */ + private boolean ignoreProxySettings = false; + + /** disable ssl hostname validation */ + private boolean disableHostnameValidation = false; + + /** disable ssl certificate validation */ + private boolean disableCertificateValidation = false; + + /** decode gzip */ + private boolean decodeGZip = false; + + /** url of request */ + private String url; +} diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/ByteSourceContent.java b/scm-core/src/main/java/sonia/scm/net/ahc/ByteSourceContent.java new file mode 100644 index 0000000000..f4182c0975 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/ahc/ByteSourceContent.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.io.ByteSource; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.IOException; +import java.io.OutputStream; + +/** + * {@link ByteSource} content for {@link AdvancedHttpRequestWithBody}. + * + * @author Sebastian Sdorra + * @since 1.46 + */ +public class ByteSourceContent implements Content +{ + + /** + * Constructs a new {@link ByteSourceContent}. + * + * + * @param byteSource byte source + */ + public ByteSourceContent(ByteSource byteSource) + { + this.byteSource = byteSource; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Sets the content length for the request. + * + * + * @param request http request + * + * @throws IOException + */ + @Override + public void prepare(AdvancedHttpRequestWithBody request) throws IOException + { + request.contentLength(byteSource.size()); + } + + /** + * Copies the content of the byte source to the output stream. + * + * + * @param output output stream + * + * @throws IOException + */ + @Override + public void process(OutputStream output) throws IOException + { + byteSource.copyTo(output); + } + + //~--- fields --------------------------------------------------------------- + + /** byte source */ + private final ByteSource byteSource; +} diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/Content.java b/scm-core/src/main/java/sonia/scm/net/ahc/Content.java new file mode 100644 index 0000000000..0f73e7b0d6 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/ahc/Content.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Content of a {@link AdvancedHttpRequestWithBody}. + * + * @author Sebastian Sdorra + * @since 1.46 + */ +public interface Content +{ + + /** + * Prepares the {@link AdvancedHttpRequestWithBody} for the request content. + * Implementations can set the content type, content length or custom headers + * for the request. + * + * + * @param request request + * + * @throws IOException + */ + public void prepare(AdvancedHttpRequestWithBody request) throws IOException; + + /** + * Copies the content to the output stream. + * + * + * @param output output stream + * + * @throws IOException + */ + public void process(OutputStream output) throws IOException; +} diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/ContentTransformer.java b/scm-core/src/main/java/sonia/scm/net/ahc/ContentTransformer.java new file mode 100644 index 0000000000..0902e2ff79 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/ahc/ContentTransformer.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +import com.google.common.io.ByteSource; +import sonia.scm.plugin.ExtensionPoint; + +/** + * Transforms {@link ByteSource} content to an object and vice versa. This class + * is an extension point, this means that plugins can define their own + * {@link ContentTransformer} implementations by implementing the interface and + * annotate the implementation with the {@link sonia.scm.plugin.ext.Extension} + * annotation. + * + * @author Sebastian Sdorra + * @since 1.46 + */ +@ExtensionPoint +public interface ContentTransformer +{ + + /** + * Returns {@code true} if the transformer is responsible for the given + * object and content type. + * + * @param type object type + * @param contentType content type + * + * @return {@code true} if the transformer is responsible + */ + public boolean isResponsible(Class type, String contentType); + + /** + * Marshalls the given object into a {@link ByteSource}. + * + * + * @param object object to marshall + * + * @return string content + */ + public ByteSource marshall(Object object); + + /** + * Unmarshall the {@link ByteSource} content to an object of the given type. + * + * + * @param type type of result object + * @param content content + * @param type of result object + * + * @return + */ + public T unmarshall(Class type, ByteSource content); +} diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/ContentTransformerException.java b/scm-core/src/main/java/sonia/scm/net/ahc/ContentTransformerException.java new file mode 100644 index 0000000000..d4e2299206 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/ahc/ContentTransformerException.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +/** + * A {@link ContentTransformerException} is thrown if an error occurs during + * content transformation by an {@link ContentTransformer}. + * + * @author Sebastian Sdorra + * + * @since 1.46 + */ +public class ContentTransformerException extends RuntimeException +{ + + /** Field description */ + private static final long serialVersionUID = 367333504151208563L; + + //~--- constructors --------------------------------------------------------- + + /** + * Constructs a new {@link ContentTransformerException}. + * + * + * @param message exception message + */ + public ContentTransformerException(String message) + { + super(message); + } + + /** + * Constructs a new {@link ContentTransformerException}. + * + * + * @param message exception message + * @param cause exception cause + */ + public ContentTransformerException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/ContentTransformerNotFoundException.java b/scm-core/src/main/java/sonia/scm/net/ahc/ContentTransformerNotFoundException.java new file mode 100644 index 0000000000..a8e85cdaf2 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/ahc/ContentTransformerNotFoundException.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +/** + * The ContentTransformerNotFoundException is thrown, if no + * {@link ContentTransformer} could be found for the given type. + * + * @author Sebastian Sdorra + * @since 1.46 + */ +public class ContentTransformerNotFoundException + extends ContentTransformerException +{ + + /** Field description */ + private static final long serialVersionUID = 6374525163951460938L; + + //~--- constructors --------------------------------------------------------- + + /** + * Constructs a new ContentTransformerNotFoundException. + * + * + * @param message exception message + */ + public ContentTransformerNotFoundException(String message) + { + super(message); + } +} diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/ContentType.java b/scm-core/src/main/java/sonia/scm/net/ahc/ContentType.java new file mode 100644 index 0000000000..8e3b299f96 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/ahc/ContentType.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +/** + * Content-Types. + * + * @author Sebastian Sdorra + * @since 1.46 + */ +public final class ContentType +{ + + /** json content type */ + public static final String JSON = "application/json"; + + /** xml content type */ + public static final String XML = "application/xml"; + + //~--- constructors --------------------------------------------------------- + + private ContentType() {} +} diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/FileContent.java b/scm-core/src/main/java/sonia/scm/net/ahc/FileContent.java new file mode 100644 index 0000000000..51f1af6c21 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/ahc/FileContent.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.io.ByteStreams; +import com.google.common.io.Closeables; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Sets the content of the file to the request. + * + * @author Sebastian Sdorra + * @since 1.46 + */ +public class FileContent implements Content +{ + + /** + * Constructs a new {@link FileContent}. + * + * + * @param file file + */ + public FileContent(File file) + { + this.file = file; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Sets the content length of the file as request header. + * + * + * @param request request + */ + @Override + public void prepare(AdvancedHttpRequestWithBody request) + { + request.contentLength(file.length()); + } + + /** + * Copies the content of the file to the output stream. + * + * + * @param output output stream + * + * @throws IOException + */ + @Override + public void process(OutputStream output) throws IOException + { + InputStream stream = null; + + try + { + stream = new FileInputStream(file); + ByteStreams.copy(stream, output); + } + finally + { + Closeables.close(stream, true); + } + } + + //~--- fields --------------------------------------------------------------- + + /** file */ + private final File file; +} diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/FormContentBuilder.java b/scm-core/src/main/java/sonia/scm/net/ahc/FormContentBuilder.java new file mode 100644 index 0000000000..d25c12ce94 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/ahc/FormContentBuilder.java @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Strings; + +import sonia.scm.util.HttpUtil; + +/** + * The form builder is able to add form parameters to a request. + * + * @author Sebastian Sdorra + * @since 1.46 + */ +public class FormContentBuilder +{ + + /** + * Constructs a new {@link FormContentBuilder}. + * + * + * @param request request + */ + public FormContentBuilder(AdvancedHttpRequestWithBody request) + { + this.request = request; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Build the formular content and append it to the request. + * + * + * @return request instance + */ + public AdvancedHttpRequestWithBody build() + { + request.contentType("application/x-www-form-urlencoded"); + request.stringContent(builder.toString()); + + return request; + } + + /** + * Adds a formular parameter. + * + * + * @param name parameter name + * @param values parameter values + * + * @return {@code this} + */ + public FormContentBuilder fields(String name, Iterable values) + { + for (Object v : values) + { + append(name, v); + } + + return this; + } + + /** + * Adds a formular parameter. + * + * + * @param name parameter name + * @param values parameter values + * + * @return {@code this} + */ + public FormContentBuilder field(String name, Object... values) + { + for (Object v : values) + { + append(name, v); + } + + return this; + } + + private void append(String name, Object value) + { + if (!Strings.isNullOrEmpty(name)) + { + if (builder.length() > 0) + { + builder.append("&"); + } + + builder.append(HttpUtil.encode(name)).append("="); + + if (value != null) + { + builder.append(HttpUtil.encode(value.toString())); + } + } + } + + //~--- fields --------------------------------------------------------------- + + /** content builder */ + private final StringBuilder builder = new StringBuilder(); + + /** request */ + private final AdvancedHttpRequestWithBody request; +} diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/HttpMethod.java b/scm-core/src/main/java/sonia/scm/net/ahc/HttpMethod.java new file mode 100644 index 0000000000..41a23dcccb --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/ahc/HttpMethod.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +/** + * Http methods. + * + * @author Sebastian Sdorra + * @since 1.46 + */ +public final class HttpMethod +{ + + /** http delete method */ + public static final String DELETE = "DELETE"; + + /** http get method */ + public static final String GET = "GET"; + + /** http head method */ + public static final String HEAD = "HEAD"; + + /** http options method */ + public static final String OPTIONS = "OPTIONS"; + + /** http post method */ + public static final String POST = "POST"; + + /** http put method */ + public static final String PUT = "PUT"; + + //~--- constructors --------------------------------------------------------- + + private HttpMethod() {} +} diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/RawContent.java b/scm-core/src/main/java/sonia/scm/net/ahc/RawContent.java new file mode 100644 index 0000000000..0b5e99bc49 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/ahc/RawContent.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.annotations.VisibleForTesting; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Byte array content for {@link AdvancedHttpRequestWithBody}. + * + * @author Sebastian Sdorra + * @since 1.46 + */ +public class RawContent implements Content +{ + + /** + * Constructs a new {@link RawContent}. + * + * + * @param data data + */ + public RawContent(byte[] data) + { + this.data = data; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Sets the length of the byte array as http header. + * + * + * @param request request + */ + @Override + public void prepare(AdvancedHttpRequestWithBody request) + { + request.contentLength(data.length); + } + + /** + * Writes the byte array to the output stream. + * + * + * @param output output stream + * + * @throws IOException + */ + @Override + public void process(OutputStream output) throws IOException + { + output.write(data); + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Returns content data. + * + * + * @return content data + */ + @VisibleForTesting + byte[] getData() + { + return data; + } + + //~--- fields --------------------------------------------------------------- + + /** byte array */ + private final byte[] data; +} diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/StringContent.java b/scm-core/src/main/java/sonia/scm/net/ahc/StringContent.java new file mode 100644 index 0000000000..07473447a7 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/ahc/StringContent.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +//~--- JDK imports ------------------------------------------------------------ + +import java.nio.charset.Charset; + +/** + * String content for {@link AdvancedHttpRequestWithBody}. + * + * @author Sebastian Sdorra + * @since 1.46 + */ +public class StringContent extends RawContent +{ + + /** + * Constructs a new {@link StringContent}. + * + * + * @param content content + * @param charset charset + */ + public StringContent(String content, Charset charset) + { + super(content.getBytes(charset)); + } +} diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/package-info.java b/scm-core/src/main/java/sonia/scm/net/ahc/package-info.java new file mode 100644 index 0000000000..d11a603b5a --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/ahc/package-info.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + +/** + * Advanced http client. + */ +package sonia.scm.net.ahc; diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryManager.java b/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryManager.java index d6da4ec43c..418316e7e7 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryManager.java +++ b/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryManager.java @@ -80,7 +80,7 @@ public abstract class AbstractRepositoryManager implements RepositoryManager protected void fireEvent(HandlerEventType event, Repository repository, Repository oldRepository) { - ScmEventBus.getInstance().post(new RepositoryEvent(event, repository, + ScmEventBus.getInstance().post(new RepositoryModificationEvent(event, repository, oldRepository)); } diff --git a/scm-core/src/main/java/sonia/scm/repository/Branch.java b/scm-core/src/main/java/sonia/scm/repository/Branch.java index 9c16259d91..ac8a064c71 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Branch.java +++ b/scm-core/src/main/java/sonia/scm/repository/Branch.java @@ -73,12 +73,28 @@ public final class Branch implements Serializable * * * @param name name of the branch + * + * @deprecated use {@link Branch#Branch(String, String)} instead */ + @Deprecated public Branch(String name) { this.name = name; } + /** + * Constructs a new branch. + * + * + * @param name name of the branch + * @param revision latest revision of the branch + */ + public Branch(String name, String revision) + { + this.name = name; + this.revision = revision; + } + //~--- methods -------------------------------------------------------------- /** @@ -104,7 +120,8 @@ public final class Branch implements Serializable final Branch other = (Branch) obj; - return Objects.equal(name, other.name); + return Objects.equal(name, other.name) + && Objects.equal(revision, other.revision); } /** @@ -116,7 +133,7 @@ public final class Branch implements Serializable @Override public int hashCode() { - return Objects.hashCode(name); + return Objects.hashCode(name, revision); } /** @@ -131,6 +148,7 @@ public final class Branch implements Serializable //J- return Objects.toStringHelper(this) .add("name", name) + .add("revision", revision) .toString(); //J+ } @@ -148,8 +166,21 @@ public final class Branch implements Serializable return name; } + /** + * Returns the latest revision of the branch. + * + * @return latest revision of branch + */ + public String getRevision() + { + return revision; + } + //~--- fields --------------------------------------------------------------- /** name of the branch */ private String name; + + /** Field description */ + private String revision; } diff --git a/scm-core/src/main/java/sonia/scm/repository/Changeset.java b/scm-core/src/main/java/sonia/scm/repository/Changeset.java index dc12dc382f..fecfd74cc9 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Changeset.java +++ b/scm-core/src/main/java/sonia/scm/repository/Changeset.java @@ -132,7 +132,9 @@ public class Changeset extends BasicPropertiesAware final Changeset other = (Changeset) obj; - return Objects.equal(id, other.id) && Objects.equal(date, other.date) + //J- + return Objects.equal(id, other.id) + && Objects.equal(date, other.date) && Objects.equal(author, other.author) && Objects.equal(description, other.description) && Objects.equal(parents, other.parents) @@ -140,6 +142,7 @@ public class Changeset extends BasicPropertiesAware && Objects.equal(branches, other.branches) && Objects.equal(modifications, other.modifications) && Objects.equal(properties, other.properties); + //J+ } /** @@ -206,7 +209,9 @@ public class Changeset extends BasicPropertiesAware } /** - * Returns the branches of the changeset. + * Returns the branches of the changeset. In the most cases a changeset is + * only related to one branch, but in the case of receive hooks it is possible + * that a changeset is related to more than a branch. * * * @return branches of the changeset diff --git a/scm-core/src/main/java/sonia/scm/repository/ClearRepositoryCacheEvent.java b/scm-core/src/main/java/sonia/scm/repository/ClearRepositoryCacheEvent.java new file mode 100644 index 0000000000..13c228bd34 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/ClearRepositoryCacheEvent.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.repository; + +import sonia.scm.event.Event; + +/** + * Event which causes clearing of repository cache. + * + * @author Sebastian Sdorra + * @since 1.50 + */ +@Event +public class ClearRepositoryCacheEvent { + + private final Repository repository; + + /** + * Constructs a new instance. + * + * @param repository repository + */ + public ClearRepositoryCacheEvent(Repository repository) { + this.repository = repository; + } + + /** + * Returns repository. + * + * @return repository + */ + public Repository getRepository() { + return repository; + } + +} diff --git a/scm-core/src/main/java/sonia/scm/repository/Permission.java b/scm-core/src/main/java/sonia/scm/repository/Permission.java index 60c6628d61..b5de810f75 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Permission.java +++ b/scm-core/src/main/java/sonia/scm/repository/Permission.java @@ -136,8 +136,9 @@ public class Permission implements PermissionObject, Serializable final Permission other = (Permission) obj; - return Objects.equal(name, other.name) && Objects.equal(type, other.type) - && Objects.equal(groupPermission, groupPermission); + return Objects.equal(name, other.name) + && Objects.equal(type, other.type) + && Objects.equal(groupPermission, other.groupPermission); } /** diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryEvent.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryEvent.java index 8fad49a5d9..0e50ef10bc 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryEvent.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryEvent.java @@ -35,12 +35,10 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.base.Objects; import sonia.scm.HandlerEventType; import sonia.scm.event.AbstractHandlerEvent; import sonia.scm.event.Event; -import sonia.scm.event.HandlerEvent; /** * The RepositoryEvent is fired if a {@link Repository} object changes. @@ -49,7 +47,7 @@ import sonia.scm.event.HandlerEvent; * @since 1.23 */ @Event -public final class RepositoryEvent extends AbstractHandlerEvent +public class RepositoryEvent extends AbstractHandlerEvent { /** diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryModificationEvent.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryModificationEvent.java new file mode 100644 index 0000000000..45e6ffd646 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryModificationEvent.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.repository; + +import sonia.scm.HandlerEventType; +import sonia.scm.ModificationHandlerEvent; +import sonia.scm.event.Event; + +/** + * Event which is fired whenever a repository is modified. + * + * @author Sebastian Sdorra + * @since 1.48 + */ +@Event +public final class RepositoryModificationEvent extends RepositoryEvent implements ModificationHandlerEvent +{ + + private final Repository itemBeforeModification; + + /** + * Constructs a new {@link RepositoryModificationEvent}. + * + * @param eventType event type + * @param item changed repository + * @param itemBeforeModification repository before it was modified + */ + public RepositoryModificationEvent(HandlerEventType eventType, Repository item, Repository itemBeforeModification) + { + super(eventType, item); + this.itemBeforeModification = itemBeforeModification; + } + + @Override + public Repository getItemBeforeModification() + { + return itemBeforeModification; + } + +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/HookBranchProvider.java b/scm-core/src/main/java/sonia/scm/repository/api/HookBranchProvider.java new file mode 100644 index 0000000000..3ad52b995c --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/HookBranchProvider.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.repository.api; + +//~--- JDK imports ------------------------------------------------------------ + +import java.util.List; + +/** + * The HookBranchProvider returns informations about branch changes during the + * current hook. + * + * @author Sebastian Sdorra + * @since 1.45 + */ +public interface HookBranchProvider +{ + + /** + * Returns the list of created or modified branch names. + * + * @return list of created or modified branches + */ + public List getCreatedOrModified(); + + /** + * Returns the list deleted or closed branch names. + * + * @return list of deleted or closed branches + */ + public List getDeletedOrClosed(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/HookContext.java b/scm-core/src/main/java/sonia/scm/repository/api/HookContext.java index b7e3e9c5e6..a7c7e8354e 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/HookContext.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/HookContext.java @@ -43,8 +43,9 @@ import sonia.scm.repository.spi.HookContextProvider; /** * The context for all repository hooks. With the {@link HookContext} class it - * is able to send messages back to the client and retrieve {@link Changeset}s - * which are added during this push/commit. + * is able to send messages back to the client, retrieve {@link Changeset}s + * which are added during this push/commit and gives informations about changed + * branches and tags. * * @author Sebastian Sdorra * @since 1.33 @@ -78,12 +79,59 @@ public final class HookContext //~--- get methods ---------------------------------------------------------- + /** + * Returns a {@link HookBranchProvider} which is able to return informations + * about changed branches during the current hook. + * + * @return {@link HookBranchProvider} + * + * @throws HookFeatureIsNotSupportedException if the feature is not supported + * by the underlying provider + * + * @since 1.45 + */ + public HookBranchProvider getBranchProvider() + { + if (logger.isDebugEnabled()) + { + logger.debug("create branch provider for repository {}", + repository.getName()); + } + + return provider.getBranchProvider(); + } + + /** + * Returns a {@link HookTagProvider} which is able to return informations + * about changed tags during the current hook. + * + * @return {@link HookTagProvider} + * + * @throws HookFeatureIsNotSupportedException if the feature is not supported + * by the underlying provider + * + * @since 1.50 + */ + public HookTagProvider getTagProvider() + { + if (logger.isDebugEnabled()) + { + logger.debug("create tag provider for repository {}", + repository.getName()); + } + + return provider.getTagProvider(); + } + /** * Returns a {@link HookChangesetBuilder} which is able to return all * {@link Changeset}'s during this push/commit. * * * @return {@link HookChangesetBuilder} + * + * @throws HookFeatureIsNotSupportedException if the feature is not supported + * by the underlying provider */ public HookChangesetBuilder getChangesetProvider() { @@ -111,6 +159,9 @@ public final class HookContext * * @return {@link HookMessageProvider} which is able to send message back to * the scm client + * + * @throws HookFeatureIsNotSupportedException if the feature is not supported + * by the underlying provider */ public HookMessageProvider getMessageProvider() { @@ -138,12 +189,12 @@ public final class HookContext //~--- fields --------------------------------------------------------------- - /** Field description */ - private PreProcessorUtil preProcessorUtil; + /** pre processor util */ + private final PreProcessorUtil preProcessorUtil; - /** Field description */ - private HookContextProvider provider; + /** hook context provider */ + private final HookContextProvider provider; - /** Field description */ - private Repository repository; + /** repository */ + private final Repository repository; } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/HookFeature.java b/scm-core/src/main/java/sonia/scm/repository/api/HookFeature.java index 082f315a35..15982022de 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/HookFeature.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/HookFeature.java @@ -37,4 +37,30 @@ package sonia.scm.repository.api; * @author Sebastian Sdorra * @since 1.33 */ -public enum HookFeature { MESSAGE_PROVIDER, CHANGESET_PROVIDER; } +public enum HookFeature +{ + + /** + * Hook message provider + */ + MESSAGE_PROVIDER, + + /** + * Hook changeset provider + */ + CHANGESET_PROVIDER, + + /** + * Hook branch provider + * + * @since 1.45 + */ + BRANCH_PROVIDER, + + /** + * Hook tag provider + * + * @since 1.50 + */ + TAG_PROVIDER; +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/HookTagProvider.java b/scm-core/src/main/java/sonia/scm/repository/api/HookTagProvider.java new file mode 100644 index 0000000000..695cba9d2d --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/HookTagProvider.java @@ -0,0 +1,59 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.repository.api; + +import java.util.List; +import sonia.scm.repository.Tag; + +/** + * The HookTagProvider returns informations about tags during the + * current hook. + * + * @since 1.50 + * @author Sebastian Sdorra + */ +public interface HookTagProvider { + + /** + * Return all tags which are delivered during the current hook. + * + * @return all tags of current hook + */ + public List getCreatedTags(); + + /** + * Return all tags which are deleted during the current hook. + * + * @return all deleted tags of current hook + */ + public List getDeletedTags(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java index a8fbbaa69d..e07eee44f0 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java @@ -40,6 +40,7 @@ import com.github.legman.Subscribe; import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import com.google.common.collect.Sets; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -50,11 +51,6 @@ import sonia.scm.HandlerEventType; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; import sonia.scm.config.ScmConfiguration; -import sonia.scm.event.ScmEventBus; -import sonia.scm.repository.BlameResult; -import sonia.scm.repository.Branches; -import sonia.scm.repository.BrowserResult; -import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.PostReceiveRepositoryHookEvent; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.Repository; @@ -63,7 +59,6 @@ import sonia.scm.repository.RepositoryEvent; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryNotFoundException; import sonia.scm.repository.RepositoryPermissions; -import sonia.scm.repository.Tags; import sonia.scm.repository.spi.RepositoryServiceProvider; import sonia.scm.repository.spi.RepositoryServiceResolver; import sonia.scm.security.ScmSecurityException; @@ -71,6 +66,8 @@ import sonia.scm.security.ScmSecurityException; //~--- JDK imports ------------------------------------------------------------ import java.util.Set; +import sonia.scm.event.ScmEventBus; +import sonia.scm.repository.ClearRepositoryCacheEvent; /** * The {@link RepositoryServiceFactory} is the entrypoint of the repository api. @@ -285,38 +282,44 @@ public final class RepositoryServiceFactory //~--- inner classes -------------------------------------------------------- /** - * TODO find a more elegant way - * - * - * @version Enter version here..., 12/06/16 - * @author Enter your name here... + * Hook and listener to clear all relevant repository caches. */ private static class CacheClearHook { + + private final Set> caches = Sets.newHashSet(); /** - * Constructs ... + * Constructs a new instance and collect all repository relevant + * caches from the {@link CacheManager}. * - * - * @param cacheManager + * @param cacheManager cache manager */ public CacheClearHook(CacheManager cacheManager) { - this.blameCache = cacheManager.getCache(BlameCommandBuilder.CACHE_NAME); - this.browseCache = cacheManager.getCache(BrowseCommandBuilder.CACHE_NAME); - this.logCache = cacheManager.getCache(LogCommandBuilder.CACHE_NAME); - this.tagsCache = cacheManager.getCache(TagsCommandBuilder.CACHE_NAME); - this.branchesCache = - cacheManager.getCache(BranchesCommandBuilder.CACHE_NAME); + this.caches.add(cacheManager.getCache(BlameCommandBuilder.CACHE_NAME)); + this.caches.add(cacheManager.getCache(BrowseCommandBuilder.CACHE_NAME)); + this.caches.add(cacheManager.getCache(LogCommandBuilder.CACHE_NAME)); + this.caches.add(cacheManager.getCache(TagsCommandBuilder.CACHE_NAME)); + this.caches.add(cacheManager.getCache(BranchesCommandBuilder.CACHE_NAME)); } //~--- methods ------------------------------------------------------------ /** - * Method description + * Clear caches on explicit repository cache clear event. + * + * @param event clear event + */ + @Subscribe + public void onEvent(ClearRepositoryCacheEvent event) { + clearCaches(event.getRepository().getId()); + } + + /** + * Clear caches on repository push. * - * - * @param event + * @param event hook event */ @Subscribe(referenceType = ReferenceType.STRONG) public void onEvent(PostReceiveRepositoryHookEvent event) @@ -332,11 +335,10 @@ public final class RepositoryServiceFactory } /** - * Method description + * Clear caches on repository delete event. * - * - * @param repository - * @param event + * @param repository changed repository + * @param event repository event */ @Subscribe(referenceType = ReferenceType.STRONG) public void onEvent(RepositoryEvent event) @@ -346,13 +348,7 @@ public final class RepositoryServiceFactory clearCaches(event.getItem().getId()); } } - - /** - * Method description - * - * - * @param repositoryId - */ + @SuppressWarnings("unchecked") private void clearCaches(final String repositoryId) { @@ -361,32 +357,11 @@ public final class RepositoryServiceFactory logger.debug("clear caches for repository id {}", repositoryId); } - RepositoryCacheKeyPredicate filter = - new RepositoryCacheKeyPredicate(repositoryId); - - blameCache.removeAll(filter); - browseCache.removeAll(filter); - logCache.removeAll(filter); - tagsCache.removeAll(filter); - branchesCache.removeAll(filter); + RepositoryCacheKeyPredicate predicate = new RepositoryCacheKeyPredicate(repositoryId); + caches.forEach((cache) -> { + cache.removeAll(predicate); + }); } - - //~--- fields ------------------------------------------------------------- - - /** blame cache */ - private final Cache blameCache; - - /** branches cache */ - private final Cache branchesCache; - - /** browse cache */ - private final Cache browseCache; - - /** log cache */ - private final Cache logCache; - - /** tags cache */ - private final Cache tagsCache; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/HookContextProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/HookContextProvider.java index 0bc91679b7..2419cd45ac 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/HookContextProvider.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/HookContextProvider.java @@ -33,16 +33,20 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- +import sonia.scm.repository.api.HookBranchProvider; import sonia.scm.repository.api.HookException; import sonia.scm.repository.api.HookFeature; import sonia.scm.repository.api.HookFeatureIsNotSupportedException; import sonia.scm.repository.api.HookMessageProvider; +import sonia.scm.repository.api.HookContext; +import sonia.scm.repository.api.HookTagProvider; //~--- JDK imports ------------------------------------------------------------ import java.util.Set; /** + * Repository type specific provider for {@link HookContext}. * * @author Sebastian Sdorra * @since 1.33 @@ -51,10 +55,10 @@ public abstract class HookContextProvider { /** - * Method description - * - * - * @return + * Return the provider specific {@link HookMessageProvider} or throws a {@link HookFeatureIsNotSupportedException}. + * The method will throw a {@link HookException} if the client is already disconnected. + * + * @return provider specific {@link HookMessageProvider} */ public final HookMessageProvider getMessageProvider() { @@ -70,7 +74,7 @@ public abstract class HookContextProvider //~--- methods -------------------------------------------------------------- /** - * Method description + * Mark client connection as disconnected. * */ final void handleClientDisconnect() @@ -81,32 +85,52 @@ public abstract class HookContextProvider //~--- get methods ---------------------------------------------------------- /** - * Method description + * Returns a set of supported hook features of the client. * - * - * @return + * @return supported features */ public abstract Set getSupportedFeatures(); /** - * Method description - * - * - * @return + * Return the provider specific {@link HookBranchProvider} or throws a {@link HookFeatureIsNotSupportedException}. + * + * @return provider specific {@link HookBranchProvider} + * + * @since 1.45 + */ + public HookBranchProvider getBranchProvider() + { + throw new HookFeatureIsNotSupportedException(HookFeature.BRANCH_PROVIDER); + } + + /** + * Return the provider specific {@link HookTagProvider} or throws a {@link HookFeatureIsNotSupportedException}. + * + * @return provider specific {@link HookTagProvider} + * + * @since 1.50 + */ + public HookTagProvider getTagProvider() + { + throw new HookFeatureIsNotSupportedException(HookFeature.TAG_PROVIDER); + } + + /** + * Return the provider specific {@link HookChangesetProvider} or throws a {@link HookFeatureIsNotSupportedException}. + * + * @return provider specific {@link HookChangesetProvider} */ public HookChangesetProvider getChangesetProvider() { - throw new HookFeatureIsNotSupportedException( - HookFeature.CHANGESET_PROVIDER); + throw new HookFeatureIsNotSupportedException(HookFeature.CHANGESET_PROVIDER); } //~--- methods -------------------------------------------------------------- /** - * Method description - * - * - * @return + * Creates a new provider specific {@link HookMessageProvider} or throws a {@link HookFeatureIsNotSupportedException}. + * + * @return provider specific {@link HookChangesetProvider} */ protected HookMessageProvider createMessageProvider() { @@ -115,6 +139,5 @@ public abstract class HookContextProvider //~--- fields --------------------------------------------------------------- - /** Field description */ private boolean clientDisconnected = false; } diff --git a/scm-core/src/main/java/sonia/scm/schedule/Scheduler.java b/scm-core/src/main/java/sonia/scm/schedule/Scheduler.java new file mode 100644 index 0000000000..dfad4baeed --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/schedule/Scheduler.java @@ -0,0 +1,65 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.schedule; + +import java.io.Closeable; + +/** + * Scheduler is able to run tasks on the future in a background thread. Task can be scheduled with cron like expression. + * Note: Task are always executed in an administrative context. + * @author Sebastian Sdorra + * @since 1.47 + */ +public interface Scheduler extends Closeable { + + /** + * Schedule a new task for future execution. + * + * @param expression cron expression + * @param runnable action + * + * @return cancelable task + */ + public Task schedule(String expression, Runnable runnable); + + /** + * Schedule a new task for future execution. The method will create a new instance of the runnable for every + * execution. The runnable can use injection. + * + * @param expression cron expression + * @param runnable action class + * + * @return cancelable task + */ + public Task schedule(String expression, Class runnable); + +} diff --git a/scm-core/src/main/java/sonia/scm/schedule/Task.java b/scm-core/src/main/java/sonia/scm/schedule/Task.java new file mode 100644 index 0000000000..dc601bcb82 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/schedule/Task.java @@ -0,0 +1,47 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.schedule; + +/** + * Tasks are executed in the future and can be running more than once. A task execution can be canceled. + * + * @author Sebastian Sdorra + * @since 1.47 + */ +public interface Task { + + /** + * Cancel task execution. + */ + public void cancel(); + +} diff --git a/scm-core/src/main/java/sonia/scm/security/CipherHandler.java b/scm-core/src/main/java/sonia/scm/security/CipherHandler.java index 8463f4a4e3..a27c4b4e28 100644 --- a/scm-core/src/main/java/sonia/scm/security/CipherHandler.java +++ b/scm-core/src/main/java/sonia/scm/security/CipherHandler.java @@ -35,7 +35,8 @@ package sonia.scm.security; /** - * + * Encrypts and decrypts string values. + * * @author Sebastian Sdorra * @since 1.7 */ @@ -43,22 +44,20 @@ public interface CipherHandler { /** - * Method description + * Decrypts the given value. * + * @param value encrypted value * - * @param value - * - * @return + * @return decrypted value */ public String decode(String value); /** - * Method description + * Encrypts the given value. * + * @param value plain text value to encrypt. * - * @param value - * - * @return + * @return encrypted value */ public String encode(String value); } diff --git a/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java b/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java index 4b82563687..ddb8a699a7 100644 --- a/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java +++ b/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java @@ -53,7 +53,7 @@ import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; -import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -66,32 +66,32 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; /** - * + * Default implementation of the {@link CipherHandler}, which uses AES for + * encryption and decryption. + * * @author Sebastian Sdorra * @since 1.7 */ -public class DefaultCipherHandler implements CipherHandler -{ +public class DefaultCipherHandler implements CipherHandler { - /** Field description */ + /** used cipher type */ public static final String CIPHER_TYPE = "AES/CTR/PKCS5PADDING"; - /** Field description */ + /** digest type for key generation */ public static final String DIGEST_TYPE = "SHA-512"; - /** Field description */ + /** string encoding */ public static final String ENCODING = "UTF-8"; - /** Field description */ + /** default key length */ public static final int KEY_LENGTH = 16; - /** Field description */ + /** default salt length */ public static final int SALT_LENGTH = 16; - /** Field description */ - private static final String CIPHERKEY_FILENAME = ".cipherkey"; + @VisibleForTesting + static final String CIPHERKEY_FILENAME = ".cipherkey"; - /** Field description */ private static final char[] KEY_BASE = new char[] { '1', '4', '7', '3', 'F', '2', '1', 'E', '-', 'C', '4', 'C', '4', '-', '4', @@ -99,96 +99,72 @@ public class DefaultCipherHandler implements CipherHandler 'E', 'C', '7', '7', '2', 'E' }; - /** Field description */ private static final String KEY_TYPE = "AES"; /** the logger for DefaultCipherHandler */ - private static final Logger logger = - LoggerFactory.getLogger(DefaultCipherHandler.class); - - //~--- constructors --------------------------------------------------------- - + private static final Logger logger = LoggerFactory.getLogger(DefaultCipherHandler.class); + + private final SecureRandom random = new SecureRandom(); + + private final char[] key; + /** * Constructs a new DefaultCipherHandler. Note this constructor is only for * unit tests. * - * - * @param key + * @param key default encryption key * * @since 1.38 */ @VisibleForTesting - protected DefaultCipherHandler(String key) - { + protected DefaultCipherHandler(String key) { this.key = key.toCharArray(); } /** - * Constructs ... - * - * - * @param context - * @param keyGenerator - * + * Constructs a new instance and reads the default key from the scm home directory, + * if the key file does not exists it will be generated with the {@link KeyGenerator}. * + * @param context SCM-Manager context provider + * @param keyGenerator key generator for default key generation */ - public DefaultCipherHandler(SCMContextProvider context, - KeyGenerator keyGenerator) - { + public DefaultCipherHandler(SCMContextProvider context, KeyGenerator keyGenerator) { File configDirectory = new File(context.getBaseDirectory(), "config"); IOUtil.mkdirs(configDirectory); - cipherKeyFile = new File(configDirectory, CIPHERKEY_FILENAME); + File cipherKeyFile = new File(configDirectory, CIPHERKEY_FILENAME); - try - { - if (cipherKeyFile.exists()) - { - loadKey(); - } - else - { + try { + if (cipherKeyFile.exists()) { + key = loadKey(cipherKeyFile); + } else { key = keyGenerator.createKey().toCharArray(); - storeKey(); + storeKey(cipherKeyFile); } - } - catch (IOException ex) - { + } catch (IOException ex) { throw new CipherException("could not create CipherHandler", ex); } } //~--- methods -------------------------------------------------------------- - /** - * Method description - * - * - * @param value - * - * @return - */ @Override - public String decode(String value) - { + public String decode(String value) { return decode(key, value); } /** - * Method description + * Decodes the given value with the provided key. * + * @param plainKey key which is used for decoding + * @param value encrypted value * - * @param plainKey - * @param value - * - * @return + * @return decrypted value */ - public String decode(char[] plainKey, String value) - { + public String decode(char[] plainKey, String value) { String result = null; - try - { + try { byte[] encodedInput = Base64.decode(value); byte[] salt = new byte[SALT_LENGTH]; byte[] encoded = new byte[encodedInput.length - SALT_LENGTH]; @@ -206,52 +182,35 @@ public class DefaultCipherHandler implements CipherHandler byte[] decoded = cipher.doFinal(encoded); result = new String(decoded, ENCODING); - } - catch (Exception ex) - { - logger.error("could not decode string", ex); - - throw new CipherException(ex); + } catch (IOException | GeneralSecurityException ex) { + throw new CipherException("could not decode string", ex); } return result; } - /** - * Method description - * - * - * @param value - * - * @return - */ @Override - public String encode(String value) - { + public String encode(String value) { return encode(key, value); } /** - * Method description + * Encrypts the given value with the provided key. * + * @param plainKey key which is used for encoding + * @param value plain text value to encrypt * - * @param plainKey - * @param value - * - * @return + * @return encrypted value */ - public String encode(char[] plainKey, String value) - { + public String encode(char[] plainKey, String value) { String res = null; - - try - { + try { byte[] salt = new byte[SALT_LENGTH]; random.nextBytes(salt); IvParameterSpec iv = new IvParameterSpec(salt); - SecretKey secretKey = buildSecretKey(key); + SecretKey secretKey = buildSecretKey(plainKey); javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CIPHER_TYPE); cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, secretKey, iv); @@ -264,31 +223,14 @@ public class DefaultCipherHandler implements CipherHandler System.arraycopy(encodedInput, 0, result, SALT_LENGTH, result.length - SALT_LENGTH); res = new String(Base64.encode(result), ENCODING); - } - catch (Exception ex) - { - logger.error("could not encode string", ex); - - throw new CipherException(ex); + } catch (IOException | GeneralSecurityException ex) { + throw new CipherException("could not encode string", ex); } return res; } - /** - * Method description - * - * - * @param plainKey - * - * @return - * - * @throws NoSuchAlgorithmException - * @throws UnsupportedEncodingException - */ - private SecretKey buildSecretKey(char[] plainKey) - throws UnsupportedEncodingException, NoSuchAlgorithmException - { + private SecretKey buildSecretKey(char[] plainKey) throws IOException, NoSuchAlgorithmException { byte[] raw = new String(plainKey).getBytes(ENCODING); MessageDigest digest = MessageDigest.getInstance(DIGEST_TYPE); @@ -298,60 +240,18 @@ public class DefaultCipherHandler implements CipherHandler return new SecretKeySpec(raw, KEY_TYPE); } - /** - * Method description - * - * - * @throws IOException - */ - private void loadKey() throws IOException - { - BufferedReader reader = null; - - try - { - reader = new BufferedReader(new FileReader(cipherKeyFile)); - + private char[] loadKey(File cipherKeyFile) throws IOException { + try (BufferedReader reader = new BufferedReader(new FileReader(cipherKeyFile))) { String line = reader.readLine(); - key = decode(KEY_BASE, line).toCharArray(); - } - finally - { - IOUtil.close(reader); + return decode(KEY_BASE, line).toCharArray(); } } - /** - * Method description - * - * - * @throws FileNotFoundException - */ - private void storeKey() throws FileNotFoundException - { + private void storeKey(File cipherKeyFile) throws FileNotFoundException { String storeKey = encode(KEY_BASE, new String(key)); - PrintWriter output = null; - - try - { - output = new PrintWriter(cipherKeyFile); + try (PrintWriter output = new PrintWriter(cipherKeyFile)) { output.write(storeKey); } - finally - { - IOUtil.close(output); - } } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private File cipherKeyFile; - - /** Field description */ - private char[] key = null; - - /** Field description */ - private SecureRandom random = new SecureRandom(); } diff --git a/scm-core/src/main/java/sonia/scm/user/AbstractUserManager.java b/scm-core/src/main/java/sonia/scm/user/AbstractUserManager.java index 80187e87a9..e659ff08e8 100644 --- a/scm-core/src/main/java/sonia/scm/user/AbstractUserManager.java +++ b/scm-core/src/main/java/sonia/scm/user/AbstractUserManager.java @@ -56,17 +56,28 @@ public abstract class AbstractUserManager implements UserManager */ protected void fireEvent(HandlerEventType event, User user, User oldUser) { - ScmEventBus.getInstance().post(new UserEvent(event, user, oldUser)); + fireEvent(new UserModificationEvent(event, user, oldUser)); } /** - * Send a {@link UserEvent} to the {@link ScmEventBus}. + * Creates a new {@link UserEvent} and calls {@link #fireEvent(sonia.scm.user.UserEvent)}. * * @param user user that has changed * @param event type of change event */ protected void fireEvent(HandlerEventType event, User user) { - ScmEventBus.getInstance().post(new UserEvent(event, user)); + fireEvent(new UserEvent(event, user)); + } + + /** + * Send a {@link UserEvent} to the {@link ScmEventBus}. + * + * @param event user event + * @since 1.48 + */ + protected void fireEvent(UserEvent event) + { + ScmEventBus.getInstance().post(event); } } diff --git a/scm-core/src/main/java/sonia/scm/user/UserEvent.java b/scm-core/src/main/java/sonia/scm/user/UserEvent.java index 18e242d525..abe24cbb59 100644 --- a/scm-core/src/main/java/sonia/scm/user/UserEvent.java +++ b/scm-core/src/main/java/sonia/scm/user/UserEvent.java @@ -46,7 +46,7 @@ import sonia.scm.event.Event; * @since 1.23 */ @Event -public final class UserEvent extends AbstractHandlerEvent +public class UserEvent extends AbstractHandlerEvent { /** diff --git a/scm-core/src/main/java/sonia/scm/user/UserModificationEvent.java b/scm-core/src/main/java/sonia/scm/user/UserModificationEvent.java new file mode 100644 index 0000000000..f3c7ec11ba --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/user/UserModificationEvent.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.user; + +import sonia.scm.HandlerEventType; +import sonia.scm.ModificationHandlerEvent; +import sonia.scm.event.Event; + +/** + * Event which is fired whenever a user is modified. + * + * @author Sebastian Sdorra + * @since 1.48 + */ +@Event +public class UserModificationEvent extends UserEvent implements ModificationHandlerEvent +{ + + private final User itemBeforeModification; + + /** + * Constructs a new {@link UserModificationEvent}. + * + * @param eventType type of event + * @param item changed user + * @param itemBeforeModification changed user before it was modified + */ + public UserModificationEvent(HandlerEventType eventType, User item, User itemBeforeModification) + { + super(eventType, item); + this.itemBeforeModification = itemBeforeModification; + } + + @Override + public User getItemBeforeModification() + { + return itemBeforeModification; + } + +} diff --git a/scm-core/src/main/java/sonia/scm/util/HttpUtil.java b/scm-core/src/main/java/sonia/scm/util/HttpUtil.java index daa0f2c245..89d1646d5e 100644 --- a/scm-core/src/main/java/sonia/scm/util/HttpUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/HttpUtil.java @@ -35,7 +35,9 @@ package sonia.scm.util; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.CharMatcher; +import com.google.common.base.Objects; import com.google.common.base.Strings; import org.slf4j.Logger; @@ -96,6 +98,12 @@ public final class HttpUtil * @since 2.0.0 */ public static final String HEADER_AUTHORIZATION = "Authorization"; + + /** + * content-length header + * @since 1.46 + */ + public static final String HEADER_CONTENT_LENGTH = "Content-Length"; /** * location header @@ -115,6 +123,24 @@ public final class HttpUtil /** authentication header */ public static final String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate"; + /** + * The original host requested by the client in the Host HTTP request header. + * @since 1.47 + */ + public static final String HEADER_X_FORWARDED_HOST = "X-Forwarded-Host"; + + /** + * The original port requested by the client. + * @since 1.47 + */ + public static final String HEADER_X_FORWARDED_PORT = "X-Forwarded-Port"; + + /** + * The original protocol (http or https) requested by the client. + * @since 1.47 + */ + public static final String HEADER_X_FORWARDED_PROTO = "X-Forwarded-Proto"; + /** * Default http port * @since 1.5 @@ -315,8 +341,26 @@ public final class HttpUtil } /** - * Url decode. + * Creates the value for the content-disposition attachment header. The method + * creates the filename as specified in rfc6266. * + * @param name attachment name + * @see rfc6266 section 5 + * @return value of content-disposition header + * @since 1.46 + */ + public static String createContentDispositionAttachmentHeader(String name) + { + StringBuilder buffer = new StringBuilder("attachment; "); + + buffer.append("filename=\"").append(name).append("\"; "); + buffer.append("filename*=utf-8''").append(encode(name)); + + return buffer.toString(); + } + + /** + * Url decode. * * @param value value to decode * @@ -578,21 +622,31 @@ public final class HttpUtil //~--- get methods ---------------------------------------------------------- /** - * Returns an absolute url with context path. + * Returns an absolute url with context path. The method creates the url from + * forwarding request headers, if they are available. * * * @param request http client request * @param pathSegments * * @return absolute url with context path + * + * @see Issue 748 * @since 1.16 */ public static String getCompleteUrl(HttpServletRequest request, String... pathSegments) { - String baseUrl = - request.getRequestURL().toString().replace(request.getRequestURI(), - Util.EMPTY_STRING).concat(request.getContextPath()); + String baseUrl; + + if (isForwarded(request)) + { + baseUrl = createForwardedBaseUrl(request); + } + else + { + baseUrl = createBaseUrl(request); + } if (Util.isNotEmpty(pathSegments)) { @@ -622,6 +676,24 @@ public final class HttpUtil return append(configuration.getBaseUrl(), path); } + /** + * Method description + * + * + * @param request + * @param header + * @param defaultValue + * + * @return + */ + public static String getHeader(HttpServletRequest request, String header, + String defaultValue) + { + String value = request.getHeader(header); + + return Objects.firstNonNull(value, defaultValue); + } + /** * Returns the port of the url parameter. * @@ -776,6 +848,21 @@ public final class HttpUtil return "chunked".equals(request.getHeader("Transfer-Encoding")); } + /** + * Returns {@code true} if the request is forwarded by a reverse proxy. The + * method uses the X-Forwarded-Host header to identify a forwarded request. + * + * @param request servlet request + * + * @return {@code true} if the request is forwarded + * + * @since 1.47 + */ + public static boolean isForwarded(HttpServletRequest request) + { + return !Strings.isNullOrEmpty(request.getHeader(HEADER_X_FORWARDED_HOST)); + } + /** * Returns true if the http request is send by the scm-manager web interface. * @@ -790,4 +877,74 @@ public final class HttpUtil return SCM_CLIENT_WUI.equalsIgnoreCase( request.getHeader(HEADER_SCM_CLIENT)); } + + //~--- methods -------------------------------------------------------------- + + /** + * Creates base url for request url. + * + * @param request http servlet request + * + * @return base url from request + * + * @since 1.47 + */ + @VisibleForTesting + static String createBaseUrl(HttpServletRequest request) + { + return request.getRequestURL().toString().replace(request.getRequestURI(), + Util.EMPTY_STRING).concat(request.getContextPath()); + } + + /** + * Creates base url from forwarding request headers. + * + * @param request http servlet request + * + * @return base url from forward headers + * + * @since 1.47 + */ + @VisibleForTesting + static String createForwardedBaseUrl(HttpServletRequest request) + { + String proto = getHeader(request, HEADER_X_FORWARDED_PROTO, + request.getScheme()); + String host; + String fhost = getHeader(request, HEADER_X_FORWARDED_HOST, + request.getScheme()); + String port = request.getHeader(HEADER_X_FORWARDED_PORT); + int s = fhost.indexOf(SEPARATOR_PORT); + + if (s > 0) + { + host = fhost.substring(0, s); + + if (Strings.isNullOrEmpty(port)) + { + port = fhost.substring(s + 1); + } + } + else + { + host = fhost; + } + + StringBuilder buffer = new StringBuilder(proto); + + buffer.append(SEPARATOR_SCHEME).append(host).append(SEPARATOR_PORT); + + if (Strings.isNullOrEmpty(port)) + { + buffer.append(String.valueOf(request.getServerPort())); + } + else + { + buffer.append(port); + } + + buffer.append(request.getContextPath()); + + return buffer.toString(); + } } diff --git a/scm-core/src/main/java/sonia/scm/util/UrlBuilder.java b/scm-core/src/main/java/sonia/scm/util/UrlBuilder.java index cd39723ec1..c844f65f11 100644 --- a/scm-core/src/main/java/sonia/scm/util/UrlBuilder.java +++ b/scm-core/src/main/java/sonia/scm/util/UrlBuilder.java @@ -35,6 +35,7 @@ package sonia.scm.util; //~--- JDK imports ------------------------------------------------------------ +import com.google.common.net.UrlEscapers; import java.net.MalformedURLException; import java.net.URL; @@ -138,8 +139,11 @@ public class UrlBuilder { if (Util.isNotEmpty(name) && Util.isNotEmpty(value)) { - url = new StringBuilder(url).append(separator).append(name).append( - HttpUtil.SEPARATOR_PARAMETER_VALUE).append(value).toString(); + url = new StringBuilder(url) + .append(separator).append(name) + .append(HttpUtil.SEPARATOR_PARAMETER_VALUE) + .append(UrlEscapers.urlFragmentEscaper().escape(value)) + .toString(); separator = HttpUtil.SEPARATOR_PARAMETER; parameterAdded = true; } diff --git a/scm-core/src/main/java/sonia/scm/web/UserAgent.java b/scm-core/src/main/java/sonia/scm/web/UserAgent.java new file mode 100644 index 0000000000..7efd1bd1c2 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/web/UserAgent.java @@ -0,0 +1,257 @@ +/** +* Copyright (c) 2010, Sebastian Sdorra +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* 3. Neither the name of SCM-Manager; nor the names of its +* contributors may be used to endorse or promote products derived from this +* software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY +* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +* +* http://bitbucket.org/sdorra/scm-manager +* +*/ + + + +package sonia.scm.web; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Charsets; +import com.google.common.base.Objects; + +import static com.google.common.base.Preconditions.*; + +//~--- JDK imports ------------------------------------------------------------ + +import java.nio.charset.Charset; + +/** + * The software agent that is acting on behalf of a user. The user agent + * represents a browser or one of the repository client (svn, git or hg). + * + * @author Sebastian Sdorra + * @since 1.45 + */ +public final class UserAgent +{ + + /** + * Constructs a new user agent + * + * + * @param name + * @param browser + * @param basicAuthenticationCharset + */ + private UserAgent(String name, boolean browser, + Charset basicAuthenticationCharset) + { + this.name = checkNotNull(name); + this.browser = browser; + this.basicAuthenticationCharset = checkNotNull(basicAuthenticationCharset); + } + + //~--- methods -------------------------------------------------------------- + + /** + * Returns the {@link Builder} for the UserAgent. + * + * + * @param name name of the UserAgent + * + * @return builder for UserAgent + */ + public static Builder builder(String name) + { + return new Builder(name); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) + { + if (obj == null) + { + return false; + } + + if (getClass() != obj.getClass()) + { + return false; + } + + final UserAgent other = (UserAgent) obj; + + return Objects.equal(name, other.name) + && Objects.equal(browser, other.browser) + && Objects.equal(basicAuthenticationCharset, basicAuthenticationCharset); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() + { + return Objects.hashCode(name, browser, basicAuthenticationCharset); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() + { + //J- + return Objects.toStringHelper(this) + .add("name", name) + .add("browser", browser) + .add("basicAuthenticationCharset", basicAuthenticationCharset) + .toString(); + //J+ + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Returns the {@link Charset}, which is used to decode the basic + * authentication header. + * + * @return {@link Charset} for basic authentication + */ + public Charset getBasicAuthenticationCharset() + { + return basicAuthenticationCharset; + } + + /** + * Returns the name of UserAgent. + * + * + * @return name of UserAgent + */ + public String getName() + { + return name; + } + + /** + * Returns {@code true} if UserAgent is a browser. + * + * + * @return {@code true} if UserAgent is a browser + */ + public boolean isBrowser() + { + return browser; + } + + //~--- inner classes -------------------------------------------------------- + + /** + * Builder class for {@link UserAgent}. + */ + public static class Builder + { + + /** + * Constructs a new UserAgent builder. + * + * + * @param name name of the UserAgent + */ + public Builder(String name) + { + this.name = name; + } + + //~--- methods ------------------------------------------------------------ + + /** + * Sets {@link Charset} which is used to decode the basic authentication. + * + * + * @param basicAuthenticationCharset charset for basic authentication + * + * @return {@code this} + */ + public Builder basicAuthenticationCharset( + Charset basicAuthenticationCharset) + { + this.basicAuthenticationCharset = + checkNotNull(basicAuthenticationCharset); + + return this; + } + + /** + * Set to {@code true} if the {@link UserAgent} is a browser. + * + * + * @param browser {@code true} for a browser + * + * @return {@code this} + */ + public Builder browser(boolean browser) + { + this.browser = browser; + + return this; + } + + /** + * Builds the {@link UserAgent}. + * + * + * @return new {@link UserAgent} + */ + public UserAgent build() + { + return new UserAgent(name, browser, basicAuthenticationCharset); + } + + //~--- fields ------------------------------------------------------------- + + /** name of UserAgent */ + private final String name; + + /** indicator for browsers */ + private boolean browser = true; + + /** basic authentication charset */ + private Charset basicAuthenticationCharset = Charsets.ISO_8859_1; + } + + + //~--- fields --------------------------------------------------------------- + + /** basic authentication charset */ + private final Charset basicAuthenticationCharset; + + /** indicator for browsers */ + private final boolean browser; + + /** name of UserAgent */ + private final String name; +} diff --git a/scm-core/src/main/java/sonia/scm/web/UserAgentParser.java b/scm-core/src/main/java/sonia/scm/web/UserAgentParser.java new file mode 100644 index 0000000000..d1c0353e5f --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/web/UserAgentParser.java @@ -0,0 +1,157 @@ +/** +* Copyright (c) 2010, Sebastian Sdorra +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* 3. Neither the name of SCM-Manager; nor the names of its +* contributors may be used to endorse or promote products derived from this +* software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY +* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +* +* http://bitbucket.org/sdorra/scm-manager +* +*/ + + + +package sonia.scm.web; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import sonia.scm.cache.Cache; +import sonia.scm.cache.CacheManager; +import sonia.scm.util.HttpUtil; + +//~--- JDK imports ------------------------------------------------------------ + +import java.util.Locale; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +/** + * Parser for User-Agent header. The UserAgentParser parses the User-Agent + * header and returns a {@link UserAgent} object. + * + * @author Sebastian Sdorra + * @since 1.45 + */ +@Singleton +public final class UserAgentParser +{ + + /** name of the cache */ + @VisibleForTesting + static final String CACHE_NAME = "sonia.scm.user-agent"; + + /** unknown UserAgent */ + @VisibleForTesting + static final UserAgent UNKNOWN = UserAgent.builder("UNKNOWN").build(); + + /** logger */ + private static final Logger logger = + LoggerFactory.getLogger(UserAgentParser.class); + + //~--- constructors --------------------------------------------------------- + + /** + * Constructs a new UserAgentParser. + * + * + * @param providers set of providers + * @param cacheManager cache manager + */ + @Inject + public UserAgentParser(Set providers, + CacheManager cacheManager) + { + this.providers = providers; + this.cache = cacheManager.getCache(CACHE_NAME); + } + + //~--- methods -------------------------------------------------------------- + + /** + * Extracts the User-Agent header and returns an {@link UserAgent} object. + * + * + * @param request http request + * + * @return {@link UserAgent} object + */ + public UserAgent parse(HttpServletRequest request) + { + return parse(request.getHeader(HttpUtil.HEADER_USERAGENT)); + } + + /** + * Parses the User-Agent header and returns a {@link UserAgent} object. + * + * + * @param userAgent User-Agent header + * + * @return {@link UserAgent} object + */ + public UserAgent parse(String userAgent) + { + String uas = Strings.nullToEmpty(userAgent).toLowerCase(Locale.ENGLISH); + UserAgent ua = cache.get(uas); + + if (ua == null) + { + for (UserAgentProvider provider : providers) + { + ua = provider.parseUserAgent(uas); + + if (ua != null) + { + break; + } + } + + if (ua == null) + { + ua = UNKNOWN; + } + + cache.put(uas, ua); + } + + logger.trace("return user-agent {} for {}", ua, userAgent); + + return ua; + } + + //~--- fields --------------------------------------------------------------- + + /** cache for parsed UserAgents */ + private final Cache cache; + + /** set of providers */ + private final Set providers; +} diff --git a/scm-core/src/main/java/sonia/scm/web/UserAgentProvider.java b/scm-core/src/main/java/sonia/scm/web/UserAgentProvider.java new file mode 100644 index 0000000000..2eb58a8a36 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/web/UserAgentProvider.java @@ -0,0 +1,58 @@ +/** +* Copyright (c) 2010, Sebastian Sdorra +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* 3. Neither the name of SCM-Manager; nor the names of its +* contributors may be used to endorse or promote products derived from this +* software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY +* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +* +* http://bitbucket.org/sdorra/scm-manager +* +*/ + + + +package sonia.scm.web; + +import sonia.scm.plugin.ExtensionPoint; + +/** + * Provider to parse User-Agent header and returns an {@link UserAgent} object. + * The {@link UserAgentProvider} is used by the {@link UserAgentParser}. + * + * @author Sebastian Sdorra + * @since 1.45 + */ +@ExtensionPoint(multi = true) +public interface UserAgentProvider +{ + + /** + * Parses the User-Agent header and returns a {@link UserAgent} object. + * + * + * @param userAgentString User-Agent header + * + * @return {@link UserAgent} object + */ + public UserAgent parseUserAgent(String userAgentString); +} diff --git a/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java index ec95d665a4..b88dda8a97 100644 --- a/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java +++ b/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java @@ -93,9 +93,7 @@ public class AuthenticationFilter extends HttpFilter * @param tokenGenerators web token generators */ @Inject - public AuthenticationFilter(ScmConfiguration configuration, - Set tokenGenerators) - { + public AuthenticationFilter(ScmConfiguration configuration, Set tokenGenerators) { this.configuration = configuration; this.tokenGenerators = tokenGenerators; } @@ -106,7 +104,6 @@ public class AuthenticationFilter extends HttpFilter * Handles authentication, if a one of the {@link WebTokenGenerator} returns * an {@link AuthenticationToken}. * - * * @param request servlet request * @param response servlet response * @param chain filter chain @@ -222,7 +219,6 @@ public class AuthenticationFilter extends HttpFilter private AuthenticationToken createToken(HttpServletRequest request) { AuthenticationToken token = null; - for (WebTokenGenerator generator : tokenGenerators) { token = generator.createToken(request); diff --git a/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java index ad19971359..7517f4848c 100644 --- a/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java +++ b/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java @@ -144,33 +144,24 @@ public abstract class PermissionFilter extends HttpFilter if (hasPermission(repository, writeRequest)) { - if (logger.isTraceEnabled()) - { - logger.trace("{} access to repository {} for user {} granted", - getActionAsString(writeRequest), repository.getName(), - getUserName(subject)); - } + logger.trace("{} access to repository {} for user {} granted", + getActionAsString(writeRequest), repository.getName(), + getUserName(subject)); chain.doFilter(request, response); } else { - if (logger.isInfoEnabled()) - { - logger.info("{} access to repository {} for user {} denied", - getActionAsString(writeRequest), repository.getName(), - getUserName(subject)); - } - + logger.info("{} access to repository {} for user {} denied", + getActionAsString(writeRequest), repository.getName(), + getUserName(subject)); + sendAccessDenied(request, response, subject); } } else { - if (logger.isDebugEnabled()) - { - logger.debug("repository not found"); - } + logger.debug("repository not found"); response.sendError(HttpServletResponse.SC_NOT_FOUND); } @@ -193,12 +184,7 @@ public abstract class PermissionFilter extends HttpFilter } catch (ScmSecurityException | AuthorizationException ex) { - if (logger.isWarnEnabled()) - { - logger.warn("user {} has not enough permissions", - subject.getPrincipal()); - } - + logger.warn("user " + subject.getPrincipal() + " has not enough permissions", ex); sendAccessDenied(request, response, subject); } diff --git a/scm-core/src/main/java/sonia/scm/web/filter/SecurityHttpServletRequestWrapper.java b/scm-core/src/main/java/sonia/scm/web/filter/SecurityHttpServletRequestWrapper.java index 2fdbdd546a..2cd78ce807 100644 --- a/scm-core/src/main/java/sonia/scm/web/filter/SecurityHttpServletRequestWrapper.java +++ b/scm-core/src/main/java/sonia/scm/web/filter/SecurityHttpServletRequestWrapper.java @@ -45,7 +45,7 @@ import javax.servlet.http.HttpServletRequestWrapper; public class SecurityHttpServletRequestWrapper extends HttpServletRequestWrapper { - /** + /** * Constructs ... * * @@ -61,9 +61,6 @@ public class SecurityHttpServletRequestWrapper extends HttpServletRequestWrapper //~--- get methods ---------------------------------------------------------- - /** - * {@inheritDoc} - */ @Override public String getRemoteUser() { diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpClientTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpClientTest.java new file mode 100644 index 0000000000..59549c24dd --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpClientTest.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.net.ahc; + +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +/** + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class AdvancedHttpClientTest { + + @Mock(answer = Answers.CALLS_REAL_METHODS) + private AdvancedHttpClient client; + + private static final String URL = "https://www.scm-manager.org"; + + @Test + public void testGet() + { + AdvancedHttpRequest request = client.get(URL); + assertEquals(URL, request.getUrl()); + assertEquals(HttpMethod.GET, request.getMethod()); + } + + @Test + public void testDelete() + { + AdvancedHttpRequestWithBody request = client.delete(URL); + assertEquals(URL, request.getUrl()); + assertEquals(HttpMethod.DELETE, request.getMethod()); + } + + @Test + public void testPut() + { + AdvancedHttpRequestWithBody request = client.put(URL); + assertEquals(URL, request.getUrl()); + assertEquals(HttpMethod.PUT, request.getMethod()); + } + + @Test + public void testPost() + { + AdvancedHttpRequestWithBody request = client.post(URL); + assertEquals(URL, request.getUrl()); + assertEquals(HttpMethod.POST, request.getMethod()); + } + + @Test + public void testOptions() + { + AdvancedHttpRequestWithBody request = client.options(URL); + assertEquals(URL, request.getUrl()); + assertEquals(HttpMethod.OPTIONS, request.getMethod()); + } + + @Test + public void testHead() + { + AdvancedHttpRequest request = client.head(URL); + assertEquals(URL, request.getUrl()); + assertEquals(HttpMethod.HEAD, request.getMethod()); + } + + @Test + public void testMethod() + { + AdvancedHttpRequestWithBody request = client.method("PROPFIND", URL); + assertEquals(URL, request.getUrl()); + assertEquals("PROPFIND", request.getMethod()); + } +} \ No newline at end of file diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestTest.java new file mode 100644 index 0000000000..5b2d675e97 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestTest.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.net.ahc; + +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +/** + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class AdvancedHttpRequestTest { + + @Mock + private AdvancedHttpClient ahc; + + @Test + public void testSelf() + { + AdvancedHttpRequest ahr = new AdvancedHttpRequest(ahc, HttpMethod.GET, "https://www.scm-manager.org"); + assertEquals(AdvancedHttpRequest.class, ahr.self().getClass()); + } + +} \ No newline at end of file diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBodyTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBodyTest.java new file mode 100644 index 0000000000..5a7c55a46d --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBodyTest.java @@ -0,0 +1,169 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.net.ahc; + +import com.google.common.base.Charsets; +import com.google.common.io.ByteSource; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.*; +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +/** + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class AdvancedHttpRequestWithBodyTest { + + @Mock + private AdvancedHttpClient ahc; + + @Mock + private ContentTransformer transformer; + + private AdvancedHttpRequestWithBody request; + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Before + public void before(){ + request = new AdvancedHttpRequestWithBody(ahc, HttpMethod.PUT, "https://www.scm-manager.org"); + } + + @Test + public void testContentLength() + { + request.contentLength(12l); + assertEquals("12", request.getHeaders().get("Content-Length").iterator().next()); + } + + @Test + public void testContentType(){ + request.contentType("text/plain"); + assertEquals("text/plain", request.getHeaders().get("Content-Type").iterator().next()); + } + + @Test + public void testFileContent() throws IOException{ + File file = tempFolder.newFile(); + request.fileContent(file); + assertThat(request.getContent(), instanceOf(FileContent.class)); + } + + @Test + public void testRawContent() throws IOException { + request.rawContent("test".getBytes(Charsets.UTF_8)); + assertThat(request.getContent(), instanceOf(RawContent.class)); + } + + @Test + public void testRawContentWithByteSource() throws IOException { + ByteSource bs = ByteSource.wrap("test".getBytes(Charsets.UTF_8)); + request.rawContent(bs); + assertThat(request.getContent(), instanceOf(ByteSourceContent.class)); + } + + @Test + public void testFormContent(){ + FormContentBuilder builder = request.formContent(); + assertNotNull(builder); + builder.build(); + assertThat(request.getContent(), instanceOf(StringContent.class)); + } + + @Test + public void testStringContent(){ + request.stringContent("test"); + assertThat(request.getContent(), instanceOf(StringContent.class)); + } + + @Test + public void testStringContentWithCharset(){ + request.stringContent("test", Charsets.UTF_8); + assertThat(request.getContent(), instanceOf(StringContent.class)); + } + + @Test + public void testXmlContent() throws IOException{ + when(ahc.createTransformer(String.class, ContentType.XML)).thenReturn(transformer); + when(transformer.marshall("")).thenReturn(ByteSource.wrap("".getBytes(Charsets.UTF_8))); + Content content = request.xmlContent("").getContent(); + assertThat(content, instanceOf(ByteSourceContent.class)); + ByteSourceContent bsc = (ByteSourceContent) content; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + bsc.process(baos); + assertEquals("", baos.toString("UTF-8")); + } + + @Test + public void testJsonContent() throws IOException{ + when(ahc.createTransformer(String.class, ContentType.JSON)).thenReturn(transformer); + when(transformer.marshall("{}")).thenReturn(ByteSource.wrap("{'root': {}}".getBytes(Charsets.UTF_8))); + Content content = request.jsonContent("{}").getContent(); + assertThat(content, instanceOf(ByteSourceContent.class)); + ByteSourceContent bsc = (ByteSourceContent) content; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + bsc.process(baos); + assertEquals("{'root': {}}", baos.toString("UTF-8")); + } + + @Test + public void testTransformedContent() throws IOException{ + when(ahc.createTransformer(String.class, "text/plain")).thenReturn(transformer); + when(transformer.marshall("hello")).thenReturn(ByteSource.wrap("hello world".getBytes(Charsets.UTF_8))); + Content content = request.transformedContent("text/plain", "hello").getContent(); + assertThat(content, instanceOf(ByteSourceContent.class)); + ByteSourceContent bsc = (ByteSourceContent) content; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + bsc.process(baos); + assertEquals("hello world", baos.toString("UTF-8")); + } + + @Test + public void testSelf() + { + assertEquals(AdvancedHttpRequestWithBody.class, request.self().getClass()); + } + +} \ No newline at end of file diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpResponseTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpResponseTest.java new file mode 100644 index 0000000000..6fc149f36c --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpResponseTest.java @@ -0,0 +1,214 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.net.ahc; + +import com.google.common.base.Charsets; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import com.google.common.io.ByteSource; +import com.google.common.io.ByteStreams; +import com.google.common.io.CharStreams; +import java.io.IOException; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import static org.mockito.Mockito.*; +import org.mockito.runners.MockitoJUnitRunner; + +/** + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class AdvancedHttpResponseTest { + + @Mock(answer = Answers.CALLS_REAL_METHODS) + private AdvancedHttpResponse response; + + @Mock + private ContentTransformer transformer; + + @Test + public void testContent() throws IOException + { + ByteSource bs = ByteSource.wrap("test123".getBytes(Charsets.UTF_8)); + when(response.contentAsByteSource()).thenReturn(bs); + byte[] data = response.content(); + assertEquals("test123", new String(data, Charsets.UTF_8)); + } + + @Test + public void testContentWithoutByteSource() throws IOException + { + assertNull(response.content()); + } + + @Test + public void testContentAsString() throws IOException + { + ByteSource bs = ByteSource.wrap("123test".getBytes(Charsets.UTF_8)); + when(response.contentAsByteSource()).thenReturn(bs); + assertEquals("123test", response.contentAsString()); + } + + @Test + public void testContentAsStingWithoutByteSource() throws IOException + { + assertNull(response.contentAsString()); + } + + @Test + public void testContentAsReader() throws IOException + { + ByteSource bs = ByteSource.wrap("abc123".getBytes(Charsets.UTF_8)); + when(response.contentAsByteSource()).thenReturn(bs); + assertEquals("abc123", CharStreams.toString(response.contentAsReader())); + } + + @Test + public void testContentAsReaderWithoutByteSource() throws IOException + { + assertNull(response.contentAsReader()); + } + + @Test + public void testContentAsStream() throws IOException + { + ByteSource bs = ByteSource.wrap("cde456".getBytes(Charsets.UTF_8)); + when(response.contentAsByteSource()).thenReturn(bs); + byte[] data = ByteStreams.toByteArray(response.contentAsStream()); + assertEquals("cde456", new String(data, Charsets.UTF_8)); + } + + @Test + public void testContentAsStreamWithoutByteSource() throws IOException + { + assertNull(response.contentAsStream()); + } + + @Test + public void testContentFromJson() throws IOException{ + ByteSource bs = ByteSource.wrap("{}".getBytes(Charsets.UTF_8)); + when(response.contentAsByteSource()).thenReturn(bs); + when(response.createTransformer(String.class, ContentType.JSON)).thenReturn(transformer); + when(transformer.unmarshall(String.class, bs)).thenReturn("{root: null}"); + String c = response.contentFromJson(String.class); + assertEquals("{root: null}", c); + } + + @Test + public void testContentFromXml() throws IOException{ + ByteSource bs = ByteSource.wrap("".getBytes(Charsets.UTF_8)); + when(response.contentAsByteSource()).thenReturn(bs); + when(response.createTransformer(String.class, ContentType.XML)).thenReturn(transformer); + when(transformer.unmarshall(String.class, bs)).thenReturn(""); + String c = response.contentFromXml(String.class); + assertEquals("", c); + } + + @Test(expected = ContentTransformerException.class) + public void testContentTransformedWithoutHeader() throws IOException{ + Multimap map = LinkedHashMultimap.create(); + when(response.getHeaders()).thenReturn(map); + response.contentTransformed(String.class); + } + + @Test + public void testContentTransformedFromHeader() throws IOException{ + Multimap map = LinkedHashMultimap.create(); + map.put("Content-Type", "text/plain"); + when(response.getHeaders()).thenReturn(map); + when(response.createTransformer(String.class, "text/plain")).thenReturn( + transformer); + ByteSource bs = ByteSource.wrap("hello".getBytes(Charsets.UTF_8)); + when(response.contentAsByteSource()).thenReturn(bs); + when(transformer.unmarshall(String.class, bs)).thenReturn("hello world"); + String v = response.contentTransformed(String.class); + assertEquals("hello world", v); + } + + @Test + public void testContentTransformed() throws IOException{ + when(response.createTransformer(String.class, "text/plain")).thenReturn( + transformer); + ByteSource bs = ByteSource.wrap("hello".getBytes(Charsets.UTF_8)); + when(response.contentAsByteSource()).thenReturn(bs); + when(transformer.unmarshall(String.class, bs)).thenReturn("hello world"); + String v = response.contentTransformed(String.class, "text/plain"); + assertEquals("hello world", v); + } + + @Test + public void testContentTransformedWithoutByteSource() throws IOException{ + assertNull(response.contentTransformed(String.class, "text/plain")); + } + + @Test + public void testGetFirstHeader() throws IOException + { + Multimap mm = LinkedHashMultimap.create(); + mm.put("Test", "One"); + mm.put("Test-2", "One"); + mm.put("Test-2", "Two"); + when(response.getHeaders()).thenReturn(mm); + assertEquals("One", response.getFirstHeader("Test")); + assertEquals("One", response.getFirstHeader("Test-2")); + assertNull(response.getFirstHeader("Test-3")); + } + + @Test + public void testIsSuccessful() throws IOException + { + // successful + when(response.getStatus()).thenReturn(200); + assertTrue(response.isSuccessful()); + when(response.getStatus()).thenReturn(201); + assertTrue(response.isSuccessful()); + when(response.getStatus()).thenReturn(204); + assertTrue(response.isSuccessful()); + when(response.getStatus()).thenReturn(301); + assertTrue(response.isSuccessful()); + + // not successful + when(response.getStatus()).thenReturn(400); + assertFalse(response.isSuccessful()); + when(response.getStatus()).thenReturn(404); + assertFalse(response.isSuccessful()); + when(response.getStatus()).thenReturn(500); + assertFalse(response.isSuccessful()); + when(response.getStatus()).thenReturn(199); + assertFalse(response.isSuccessful()); + } + +} \ No newline at end of file diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/BaseHttpRequestTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/BaseHttpRequestTest.java new file mode 100644 index 0000000000..9b3ce995f5 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/net/ahc/BaseHttpRequestTest.java @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.net.ahc; + +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; +import java.io.IOException; +import java.util.Collection; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.hamcrest.Matchers.*; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import static org.mockito.Mockito.*; +import org.mockito.runners.MockitoJUnitRunner; + +/** + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class BaseHttpRequestTest { + + @Mock + private AdvancedHttpClient ahc; + + private BaseHttpRequest request; + + @Before + public void before(){ + request = new AdvancedHttpRequest(ahc, HttpMethod.GET, "https://www.scm-manager.org"); + } + + @Test + public void testBasicAuth() + { + request.basicAuth("tricia", "mcmillian123"); + Multimap headers = request.getHeaders(); + assertEquals("Basic dHJpY2lhOm1jbWlsbGlhbjEyMw==", headers.get("Authorization").iterator().next()); + } + + @Test + public void testQueryString(){ + request.queryString("a", "b"); + assertEquals("https://www.scm-manager.org?a=b", request.getUrl()); + } + + @Test + public void testQueryStringMultiple(){ + request.queryString("a", "b"); + request.queryString("c", "d", "e"); + assertEquals("https://www.scm-manager.org?a=b&c=d&c=e", request.getUrl()); + } + + @Test + public void testQueryStringEncoded(){ + request.queryString("a", "äüö"); + assertEquals("https://www.scm-manager.org?a=%C3%A4%C3%BC%C3%B6", request.getUrl()); + } + + @Test + public void testQueryStrings(){ + Iterable i1 = Lists.newArrayList("b"); + Iterable i2 = Lists.newArrayList("d", "e"); + request.queryStrings("a", i1); + request.queryStrings("c", i2); + assertEquals("https://www.scm-manager.org?a=b&c=d&c=e", request.getUrl()); + } + + @Test + public void testQuerqStringNullValue(){ + request.queryString("a", null, "b"); + assertEquals("https://www.scm-manager.org?a=&a=b", request.getUrl()); + } + + @Test + public void testHeader(){ + request.header("a", "b"); + assertEquals("b", request.getHeaders().get("a").iterator().next()); + } + + @Test + public void testHeaderMultiple(){ + request.header("a", "b", "c", "d"); + Collection values = request.getHeaders().get("a"); + assertThat(values, contains("b", "c", "d")); + } + + @Test + public void testRequest() throws IOException{ + request.request(); + verify(ahc).request(request); + } + + @Test + public void testBuilderMethods(){ + Iterable i1 = Lists.newArrayList("b"); + assertThat(request.decodeGZip(true), instanceOf(AdvancedHttpRequest.class)); + assertTrue(request.isDecodeGZip()); + assertThat(request.disableCertificateValidation(true), instanceOf(AdvancedHttpRequest.class)); + assertTrue(request.isDisableCertificateValidation()); + assertThat(request.disableHostnameValidation(true), instanceOf(AdvancedHttpRequest.class)); + assertTrue(request.isDisableHostnameValidation()); + assertThat(request.ignoreProxySettings(true), instanceOf(AdvancedHttpRequest.class)); + assertTrue(request.isIgnoreProxySettings()); + assertThat(request.header("a", "b"), instanceOf(AdvancedHttpRequest.class)); + assertThat(request.headers("a", i1), instanceOf(AdvancedHttpRequest.class)); + assertThat(request.queryString("a", "b"), instanceOf(AdvancedHttpRequest.class)); + assertThat(request.queryStrings("a", i1), instanceOf(AdvancedHttpRequest.class)); + } + +} \ No newline at end of file diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/ByteSourceContentTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/ByteSourceContentTest.java new file mode 100644 index 0000000000..24487f6582 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/net/ahc/ByteSourceContentTest.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.net.ahc; + +import com.google.common.base.Charsets; +import com.google.common.io.ByteSource; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import static org.mockito.Mockito.*; +import org.mockito.runners.MockitoJUnitRunner; + +/** + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class ByteSourceContentTest { + + @Mock + private AdvancedHttpRequestWithBody request; + + @Test + public void testPrepareRequest() throws IOException + { + ByteSource source = ByteSource.wrap("abc".getBytes(Charsets.UTF_8)); + ByteSourceContent content = new ByteSourceContent(source); + content.prepare(request); + verify(request).contentLength(3l); + } + + @Test + public void testProcess() throws IOException{ + ByteSource source = ByteSource.wrap("abc".getBytes(Charsets.UTF_8)); + ByteSourceContent content = new ByteSourceContent(source); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + content.process(baos); + assertEquals("abc", baos.toString("UTF-8")); + } + +} \ No newline at end of file diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/FileContentTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/FileContentTest.java new file mode 100644 index 0000000000..95b381b880 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/net/ahc/FileContentTest.java @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Charsets; +import com.google.common.io.Files; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.*; + +import static org.mockito.Mockito.*; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; + +/** + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class FileContentTest +{ + + /** + * Method description + * + * + * @throws IOException + */ + @Test + public void testPrepareRequest() throws IOException + { + FileContent content = create("abc"); + + content.prepare(request); + verify(request).contentLength(3l); + } + + /** + * Method description + * + * + * @throws IOException + */ + @Test + public void testProcess() throws IOException + { + FileContent content = create("abc"); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + content.process(baos); + assertEquals("abc", baos.toString("UTF-8")); + } + + private FileContent create(String value) throws IOException + { + File file = temp.newFile(); + + Files.write("abc", file, Charsets.UTF_8); + + return new FileContent(file); + } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + /** Field description */ + @Mock + private AdvancedHttpRequestWithBody request; +} diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/FormContentBuilderTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/FormContentBuilderTest.java new file mode 100644 index 0000000000..56cc6d7379 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/net/ahc/FormContentBuilderTest.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.net.ahc; + +import com.google.common.collect.Lists; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import static org.mockito.Mockito.*; +import org.mockito.runners.MockitoJUnitRunner; + +/** + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class FormContentBuilderTest { + + @Mock + private AdvancedHttpRequestWithBody request; + + @InjectMocks + private FormContentBuilder builder; + + @Test + public void testFieldEncoding() + { + builder.field("a", "ü", "ä", "ö").build(); + assertContent("a=%C3%BC&a=%C3%A4&a=%C3%B6"); + } + + @Test + public void testBuild() + { + builder.field("a", "b").build(); + assertContent("a=b"); + verify(request).contentType("application/x-www-form-urlencoded"); + } + + @Test + public void testFieldWithArray() + { + builder.field("a", "b").field("c", "d", "e").build(); + assertContent("a=b&c=d&c=e"); + } + + @Test + public void testFieldWithIterable() + { + Iterable i1 = Lists.newArrayList("b"); + builder.fields("a", i1) + .fields("c", Lists.newArrayList("d", "e")) + .build(); + assertContent("a=b&c=d&c=e"); + } + + private void assertContent(String content){ + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(request).stringContent(captor.capture()); + assertEquals(content, captor.getValue()); + } + +} \ No newline at end of file diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/RawContentTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/RawContentTest.java new file mode 100644 index 0000000000..f4ee8b7031 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/net/ahc/RawContentTest.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.net.ahc; + +import com.google.common.base.Charsets; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import static org.mockito.Mockito.*; +import org.mockito.runners.MockitoJUnitRunner; + +/** + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class RawContentTest { + + + @Mock + private AdvancedHttpRequestWithBody request; + + @Test + public void testPrepareRequest() + { + RawContent raw = new RawContent("abc".getBytes(Charsets.UTF_8)); + raw.prepare(request); + verify(request).contentLength(3); + } + + @Test + public void testProcess() throws IOException + { + RawContent raw = new RawContent("abc".getBytes(Charsets.UTF_8)); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + raw.process(baos); + assertEquals("abc", baos.toString("UTF-8")); + } + +} \ No newline at end of file diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/StringContentTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/StringContentTest.java new file mode 100644 index 0000000000..bd191438ca --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/net/ahc/StringContentTest.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.net.ahc; + +import com.google.common.base.Charsets; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * + * @author Sebastian Sdorra + */ +public class StringContentTest { + + + @Test + public void testStringContent() + { + StringContent sc = new StringContent("abc", Charsets.UTF_8); + assertEquals("abc", new String(sc.getData())); + } + + @Test + public void testStringContentWithCharset() + { + StringContent sc = new StringContent("üäö", Charsets.ISO_8859_1); + assertEquals("üäö", new String(sc.getData(), Charsets.ISO_8859_1)); + } + +} \ No newline at end of file diff --git a/scm-core/src/test/java/sonia/scm/repository/api/HookContextTest.java b/scm-core/src/test/java/sonia/scm/repository/api/HookContextTest.java new file mode 100644 index 0000000000..2f746ec5ad --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/api/HookContextTest.java @@ -0,0 +1,141 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.repository.api; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import java.util.List; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Person; +import sonia.scm.repository.PreProcessorUtil; +import sonia.scm.repository.Repository; +import sonia.scm.repository.spi.HookChangesetProvider; +import sonia.scm.repository.spi.HookChangesetRequest; +import sonia.scm.repository.spi.HookChangesetResponse; +import sonia.scm.repository.spi.HookContextProvider; + +/** + * Unit tests for {@link HookContext}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class HookContextTest { + + @Mock + private HookContextProvider provider; + + @Mock + private Repository repository; + + @Mock + private PreProcessorUtil preProcessorUtil; + + @Mock + private HookChangesetProvider changesetProvider; + + @InjectMocks + private HookContext context; + + /** + * Set up mocks for upcoming test. + */ + @Before + public void setUpMocks(){ + when(repository.getName()).thenReturn("test"); + when(provider.getChangesetProvider()).thenReturn(changesetProvider); + when(provider.getSupportedFeatures()).thenReturn(Sets.newHashSet(HookFeature.CHANGESET_PROVIDER)); + + List changesets = Lists.newArrayList(new Changeset("1", Long.MIN_VALUE, new Person("Trillian"))); + HookChangesetResponse response = new HookChangesetResponse(changesets); + when(changesetProvider.handleRequest(any(HookChangesetRequest.class))).thenReturn(response); + } + + /** + * Tests {@link HookContext#getBranchProvider()}. + */ + @Test + public void testGetBranchProvider() { + context.getBranchProvider(); + + verify(provider).getBranchProvider(); + } + + /** + * Tests {@link HookContext#getTagProvider()}. + */ + @Test + public void testGetTagProvider() { + context.getTagProvider(); + + verify(provider).getTagProvider(); + } + + /** + * Tests {@link HookContext#getMessageProvider()}. + */ + @Test + public void testGetMessageProvider() { + context.getMessageProvider(); + + verify(provider).getMessageProvider(); + } + + /** + * Tests {@link HookContext#getChangesetProvider()}. + */ + @Test + public void testGetChangesetProvider() { + HookChangesetBuilder builder = context.getChangesetProvider(); + List changesets = builder.getChangesetList(); + assertNotNull(changesets); + assertEquals("1", changesets.get(0).getId()); + } + + /** + * Tests {@link HookContext#isFeatureSupported(sonia.scm.repository.api.HookFeature)}. + */ + @Test + public void testIsFeatureSupported(){ + assertTrue(context.isFeatureSupported(HookFeature.CHANGESET_PROVIDER)); + assertFalse(context.isFeatureSupported(HookFeature.BRANCH_PROVIDER)); + } + +} \ No newline at end of file diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/HookContextProviderTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/HookContextProviderTest.java new file mode 100644 index 0000000000..c6abae594e --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/spi/HookContextProviderTest.java @@ -0,0 +1,136 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.repository.spi; + +import java.util.Collections; +import java.util.Set; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.hamcrest.Matchers.*; +import org.junit.Rule; +import org.junit.rules.ExpectedException; +import sonia.scm.repository.api.HookException; +import sonia.scm.repository.api.HookFeature; +import sonia.scm.repository.api.HookFeatureIsNotSupportedException; + +/** + * Unit tests for {@link HookContextProvider}. + * + * @author Sebastian Sdorra + */ +public class HookContextProviderTest { + + /** + * Expected exception rule. + */ + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private final HookContextProvider simpleHookContextProvider = new HookContextProvider() { + + @Override + public Set getSupportedFeatures() { + return Collections.emptySet(); + } + + }; + + /** + * Tests {@link HookContextProvider#getSupportedFeatures()}. + */ + @Test + public void testGetSupportedFeatures() { + assertThat(simpleHookContextProvider.getSupportedFeatures(), empty()); + } + + /** + * Tests {@link HookContextProvider#getBranchProvider()}. + */ + @Test + public void testGetBranchProvider(){ + expectNotSupported(HookFeature.BRANCH_PROVIDER); + simpleHookContextProvider.getBranchProvider(); + } + + /** + * Tests {@link HookContextProvider#getTagProvider()}. + */ + @Test + public void testGetTagProvider(){ + expectNotSupported(HookFeature.TAG_PROVIDER); + simpleHookContextProvider.getTagProvider(); + } + + /** + * Tests {@link HookContextProvider#getChangesetProvider()}. + */ + @Test + public void testGetChangesetProvider(){ + expectNotSupported(HookFeature.CHANGESET_PROVIDER); + simpleHookContextProvider.getChangesetProvider(); + } + + /** + * Tests {@link HookContextProvider#createMessageProvider()}. + */ + @Test + public void testCreateMessageProvider(){ + expectNotSupported(HookFeature.MESSAGE_PROVIDER); + simpleHookContextProvider.createMessageProvider(); + } + + /** + * Tests {@link HookContextProvider#getMessageProvider()}. + */ + @Test + public void testGetMessageProvider(){ + expectNotSupported(HookFeature.MESSAGE_PROVIDER); + simpleHookContextProvider.getMessageProvider(); + } + + /** + * Tests {@link HookContextProvider#getMessageProvider()} with disconnected client. + */ + @Test + public void testGetMessageProviderDisconnected(){ + expectedException.expect(HookException.class); + expectedException.expectMessage(containsString("message provider")); + simpleHookContextProvider.handleClientDisconnect(); + simpleHookContextProvider.getMessageProvider(); + } + + private void expectNotSupported(HookFeature feature){ + expectedException.expect(HookFeatureIsNotSupportedException.class); + expectedException.expectMessage(containsString(feature.toString())); + } + +} \ No newline at end of file diff --git a/scm-core/src/test/java/sonia/scm/security/DefaultCipherHandlerTest.java b/scm-core/src/test/java/sonia/scm/security/DefaultCipherHandlerTest.java new file mode 100644 index 0000000000..1fe78191de --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/security/DefaultCipherHandlerTest.java @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.security; + +import java.io.File; +import java.io.IOException; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import static org.mockito.Mockito.*; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.SCMContextProvider; + +/** + * Unit tests for {@link DefaultCipherHandler}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class DefaultCipherHandlerTest { + + @Mock + private SCMContextProvider context; + + @Mock + private KeyGenerator keyGenerator; + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + /** + * Tests loading and storing default key. + * + * @throws IOException + */ + @Test + public void testLoadingAndStoringDefaultKey() throws IOException { + File baseDirectory = tempFolder.newFolder(); + when(context.getBaseDirectory()).thenReturn(baseDirectory); + when(keyGenerator.createKey()).thenReturn("secret"); + + DefaultCipherHandler cipher = new DefaultCipherHandler(context, keyGenerator); + File configDirectory = new File(baseDirectory, "config"); + assertTrue(new File(configDirectory, DefaultCipherHandler.CIPHERKEY_FILENAME).exists()); + + // plain text for assertion + String plain = "hallo123"; + + // encrypt value with new generated key + String encrypted = cipher.encode(plain); + + // load key from disk + cipher = new DefaultCipherHandler(context, keyGenerator); + + // decrypt with loaded key + assertEquals(plain, cipher.decode(encrypted)); + } + + /** + * Test encode and decode method with a separate key. + */ + @Test + public void testEncodeDecodeWithSeparateKey(){ + char[] key = "testkey".toCharArray(); + DefaultCipherHandler cipher = new DefaultCipherHandler("somekey"); + assertEquals("hallo123", cipher.decode(key, cipher.encode(key, "hallo123"))); + } + + /** + * Test encode and decode method with the default key. + */ + @Test + public void testEncodeDecodeWithDefaultKey() { + DefaultCipherHandler cipher = new DefaultCipherHandler("testkey"); + assertEquals("hallo123", cipher.decode(cipher.encode("hallo123"))); + } + +} \ No newline at end of file diff --git a/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java b/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java index 585bc0e005..b85221f8c8 100644 --- a/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java +++ b/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java @@ -168,6 +168,136 @@ public class HttpUtilTest HttpUtil.checkForCRLFInjection("abcka"); } + /** + * Method description + * + */ + @Test + public void testCreateBaseUrl() + { + HttpServletRequest request = mock(HttpServletRequest.class); + String url = "https://www.scm-manager.org/test/as/db"; + + when(request.getRequestURL()).thenReturn(new StringBuffer(url)); + when(request.getRequestURI()).thenReturn("/test/as/db"); + when(request.getContextPath()).thenReturn("/scm"); + assertEquals("https://www.scm-manager.org/scm", + HttpUtil.createBaseUrl(request)); + } + + /** + * Method description + * + */ + @Test + public void testCreateForwardedUrl() + { + HttpServletRequest request = mock(HttpServletRequest.class); + + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_HOST)).thenReturn( + "www.scm-manager.org"); + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_PROTO)).thenReturn( + "https"); + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_PORT)).thenReturn("443"); + when(request.getContextPath()).thenReturn("/scm"); + assertEquals("https://www.scm-manager.org:443/scm", + HttpUtil.createForwardedBaseUrl(request)); + } + + /** + * Method description + * + */ + @Test + public void testCreateForwardedUrlWithPortAndProtoFromRequest() + { + HttpServletRequest request = mock(HttpServletRequest.class); + + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_HOST)).thenReturn( + "www.scm-manager.org"); + when(request.getScheme()).thenReturn("https"); + when(request.getServerPort()).thenReturn(443); + when(request.getContextPath()).thenReturn("/scm"); + assertEquals("https://www.scm-manager.org:443/scm", + HttpUtil.createForwardedBaseUrl(request)); + } + + /** + * Method description + * + */ + @Test + public void testCreateForwardedUrlWithPortInHost() + { + HttpServletRequest request = mock(HttpServletRequest.class); + + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_HOST)).thenReturn( + "www.scm-manager.org:443"); + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_PROTO)).thenReturn( + "https"); + when(request.getContextPath()).thenReturn("/scm"); + assertEquals("https://www.scm-manager.org:443/scm", + HttpUtil.createForwardedBaseUrl(request)); + } + + /** + * Method description + * + */ + @Test + public void testCreateForwardedUrlWithPortInHostAndPortHeader() + { + HttpServletRequest request = mock(HttpServletRequest.class); + + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_HOST)).thenReturn( + "www.scm-manager.org:80"); + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_PROTO)).thenReturn( + "https"); + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_PORT)).thenReturn("443"); + when(request.getContextPath()).thenReturn("/scm"); + assertEquals("https://www.scm-manager.org:443/scm", + HttpUtil.createForwardedBaseUrl(request)); + } + + /** + * Method description + * + */ + @Test + public void testGetCompleteUrl() + { + HttpServletRequest request = mock(HttpServletRequest.class); + String url = "https://www.scm-manager.org/test/as/db"; + + when(request.getRequestURL()).thenReturn(new StringBuffer(url)); + when(request.getRequestURI()).thenReturn("/test/as/db"); + when(request.getScheme()).thenReturn("https"); + when(request.getServerPort()).thenReturn(443); + when(request.getContextPath()).thenReturn("/scm"); + assertEquals("https://www.scm-manager.org/scm", + HttpUtil.getCompleteUrl(request)); + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_HOST)).thenReturn( + "scm.scm-manager.org"); + assertEquals("https://scm.scm-manager.org:443/scm", + HttpUtil.getCompleteUrl(request)); + } + + /** + * Method description + * + */ + @Test + public void testIsForwarded() + { + HttpServletRequest request = mock(HttpServletRequest.class); + + assertFalse(HttpUtil.isForwarded(request)); + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_HOST)).thenReturn(""); + assertFalse(HttpUtil.isForwarded(request)); + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_HOST)).thenReturn("ser"); + assertTrue(HttpUtil.isForwarded(request)); + } + /** * Method description * diff --git a/scm-core/src/test/java/sonia/scm/util/UrlBuilderTest.java b/scm-core/src/test/java/sonia/scm/util/UrlBuilderTest.java index fe64cb2d4f..16d4b59ffc 100644 --- a/scm-core/src/test/java/sonia/scm/util/UrlBuilderTest.java +++ b/scm-core/src/test/java/sonia/scm/util/UrlBuilderTest.java @@ -57,8 +57,9 @@ public class UrlBuilderTest builder.appendParameter("i", 123).appendParameter("s", "abc"); builder.appendParameter("b", true).appendParameter("l", 321l); - assertEquals("http://www.short.de?i=123&s=abc&b=true&l=321", - builder.toString()); + assertEquals("http://www.short.de?i=123&s=abc&b=true&l=321", builder.toString()); + builder.appendParameter("c", "a b"); + assertEquals("http://www.short.de?i=123&s=abc&b=true&l=321&c=a%20b", builder.toString()); } /** diff --git a/scm-core/src/test/java/sonia/scm/web/UserAgentParserTest.java b/scm-core/src/test/java/sonia/scm/web/UserAgentParserTest.java new file mode 100644 index 0000000000..d3f917932c --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/web/UserAgentParserTest.java @@ -0,0 +1,187 @@ +/** +* Copyright (c) 2010, Sebastian Sdorra +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* 3. Neither the name of SCM-Manager; nor the names of its +* contributors may be used to endorse or promote products derived from this +* software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY +* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +* +* http://bitbucket.org/sdorra/scm-manager +* +*/ + + + +package sonia.scm.web; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Charsets; +import com.google.common.collect.Sets; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import sonia.scm.cache.Cache; +import sonia.scm.cache.CacheManager; +import sonia.scm.util.HttpUtil; + +import static org.junit.Assert.*; + +import static org.mockito.Mockito.*; + +//~--- JDK imports ------------------------------------------------------------ + +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +/** + * + * @author Sebastian Sdorra + */ +@SuppressWarnings("unchecked") +@RunWith(MockitoJUnitRunner.class) +public class UserAgentParserTest +{ + + /** Field description */ + private static final String UA_1 = "mozilla/5.0"; + + /** Field description */ + private static final String UA_2 = "wget/1.5.3"; + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + */ + @Before + public void prepare() + { + Set providers = Sets.newHashSet(provider1, provider2); + when(cacheManager.getCache(UserAgentParser.CACHE_NAME)).thenReturn(cache); + parser = new UserAgentParser(providers, cacheManager); + } + + /** + * Method description + * + */ + @Test + public void testDefaultValues() + { + UserAgent ua = parser.parse(UA_1); + + assertEquals(Charsets.ISO_8859_1, ua.getBasicAuthenticationCharset()); + assertTrue(ua.isBrowser()); + } + + /** + * Method description + * + */ + @Test + public void testParse() + { + UserAgent ua = UserAgent.builder("UA1").build(); + + when(provider1.parseUserAgent(UA_1)).thenReturn(ua); + + UserAgent ua2 = UserAgent.builder("UA2").build(); + + when(provider2.parseUserAgent(UA_2)).thenReturn(ua2); + + assertEquals(ua, parser.parse(UA_1)); + assertEquals(ua2, parser.parse(UA_2)); + } + + /** + * Method description + * + */ + @Test + public void testParseHttpServletRequest() + { + when(request.getHeader(HttpUtil.HEADER_USERAGENT)).thenReturn(UA_2); + + UserAgent ua = UserAgent.builder("UA2").build(); + + when(provider1.parseUserAgent(UA_2)).thenReturn(ua); + assertEquals(ua, parser.parse(request)); + } + + /** + * Method description + * + */ + @Test + public void testParseNotFound() + { + assertEquals(UserAgentParser.UNKNOWN, parser.parse(UA_1)); + assertEquals(UserAgentParser.UNKNOWN, parser.parse(UA_2)); + } + + /** + * Method description + * + */ + @Test + public void testParseWithCache() + { + UserAgent ua = UserAgent.builder("UA").build(); + + when(cache.get(UA_1)).thenReturn(ua); + assertEquals(ua, parser.parse(UA_1)); + assertEquals(UserAgentParser.UNKNOWN, parser.parse(UA_2)); + } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + @Mock + private Cache cache; + + /** Field description */ + @Mock + private CacheManager cacheManager; + + /** Field description */ + private UserAgentParser parser; + + /** Field description */ + @Mock + private UserAgentProvider provider1; + + /** Field description */ + @Mock + private UserAgentProvider provider2; + + /** Field description */ + @Mock + private HttpServletRequest request; +} diff --git a/scm-plugins/pom.xml b/scm-plugins/pom.xml index edb857cd0a..5b61882815 100644 --- a/scm-plugins/pom.xml +++ b/scm-plugins/pom.xml @@ -142,4 +142,4 @@ - \ No newline at end of file + diff --git a/scm-plugins/scm-git-plugin/pom.xml b/scm-plugins/scm-git-plugin/pom.xml index aa8e783962..4fff4ddd27 100644 --- a/scm-plugins/scm-git-plugin/pom.xml +++ b/scm-plugins/scm-git-plugin/pom.xml @@ -37,12 +37,6 @@ org.eclipse.jgit.http.server ${jgit.version} - - - sonia.jgit - org.eclipse.jgit.java7 - ${jgit.version} - commons-lang diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java index ab588816ce..a59e3b5754 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java @@ -160,6 +160,23 @@ public class GitChangesetConverter implements Closeable return createChangeset(commit, branches); } + /** + * Method description + * + * + * @param commit + * @param branch + * + * @return + * + * @throws IOException + */ + public Changeset createChangeset(RevCommit commit, String branch) + throws IOException + { + return createChangeset(commit, Lists.newArrayList(branch)); + } + /** * Method description * diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java index 9f508effe6..80fe8907ac 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java @@ -35,6 +35,9 @@ package sonia.scm.repository; //~--- JDK imports ------------------------------------------------------------ +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; /** @@ -42,4 +45,15 @@ import javax.xml.bind.annotation.XmlRootElement; * @author Sebastian Sdorra */ @XmlRootElement(name = "config") -public class GitConfig extends SimpleRepositoryConfig {} +@XmlAccessorType(XmlAccessType.FIELD) +public class GitConfig extends SimpleRepositoryConfig { + + @XmlElement(name = "gc-expression") + private String gcExpression; + + public String getGcExpression() + { + return gcExpression; + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConstants.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConstants.java new file mode 100644 index 0000000000..6d833577e1 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConstants.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.repository; + +/** + * Constants for Git. + * + * @author Sebastian Sdorra + * @since 1.50 + */ +public final class GitConstants { + + /** + * Default branch repository property. + */ + public static final String PROPERTY_DEFAULT_BRANCH = "git.default-branch"; + + private GitConstants() { + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitGcTask.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitGcTask.java new file mode 100644 index 0000000000..248eae92d1 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitGcTask.java @@ -0,0 +1,164 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.repository; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Stopwatch; +import com.google.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.util.Map; +import java.util.Properties; +import org.eclipse.jgit.api.GarbageCollectCommand; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Executes git gc on every git repository. Statistics of the gc process are logged to the info level. The task is + * disabled by default and must be enabled through the global git configuration. + * + * @author Sebastian Sdorra + * @since 1.47 + */ +public class GitGcTask implements Runnable { + + private static final String SP = System.getProperty("line.seperator", "\n"); + + private static final Logger logger = LoggerFactory.getLogger(GitGcTask.class); + + private final RepositoryManager repositoryManager; + private final RepositoryDirectoryHandler repositoryHandler; + + @Inject + public GitGcTask(RepositoryManager repositoryManager) + { + this.repositoryManager = repositoryManager; + this.repositoryHandler = (RepositoryDirectoryHandler) repositoryManager.getHandler(GitRepositoryHandler.TYPE_NAME); + } + + @Override + public void run() + { + for (Repository repository : repositoryManager.getAll()) + { + handle(repository); + } + } + + private void handle(Repository repository){ + if (GitRepositoryHandler.TYPE_NAME.equals(repository.getType())) + { + if (repository.isValid() && repository.isHealthy()) + { + logger.info("start git gc for repository {}", repository.getName()); + Stopwatch sw = Stopwatch.createStarted(); + gc(repository); + logger.debug("gc of repository {} has finished after {}", repository.getName(), sw.stop()); + } + else + { + logger.debug("skip non valid/healthy repository {}", repository.getName()); + } + } + else + { + logger.trace("skip non git repository {}", repository.getName()); + } + } + + private void appendProperties(StringBuilder buffer, Properties properties){ + for (Map.Entry entry : properties.entrySet()){ + buffer.append(SP).append(" - ").append(entry.getKey()).append(" = ").append(entry.getValue()); + } + } + + private String message(Repository repository, Properties statistics, String span){ + StringBuilder buffer = new StringBuilder("gc statistics for "); + buffer.append(repository.getName()).append(" ").append(span).append(" execution:"); + appendProperties(buffer, statistics); + return buffer.toString(); + } + + private void statistics(Repository repository, GarbageCollectCommand gcc) throws GitAPIException { + Properties properties = gcc.getStatistics(); + logger.info(message(repository, properties, "before")); + } + + private void execute(Repository repository, GarbageCollectCommand gcc) throws GitAPIException { + Properties properties = gcc.call(); + logger.info(message(repository, properties, "after")); + } + + private void gc(Repository repository){ + File file = repositoryHandler.getDirectory(repository); + Git git = null; + try { + git = open(file); + GarbageCollectCommand gcc = git.gc(); + // print statistics before execution, because it looks like + // jgit returns the statistics after gc has finished + statistics(repository, gcc); + execute(repository, gcc); + } + catch (IOException ex) + { + logger.warn("failed to open git repository", ex); + } + catch (GitAPIException ex) + { + logger.warn("failed running git gc command", ex); + } + finally + { + if (git != null){ + git.close(); + } + } + } + + /** + * Opens the git repository. This method is only visible for testing purposes. + * + * @param file repository directory + * + * @return git for repository + * + * @throws IOException + */ + @VisibleForTesting + protected Git open(File file) throws IOException { + return Git.open(file); + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHookChangesetCollector.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHookChangesetCollector.java index 7ba8b7106b..0acd7c1ad2 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHookChangesetCollector.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHookChangesetCollector.java @@ -36,6 +36,7 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.lib.ObjectId; @@ -49,12 +50,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.util.IOUtil; +import sonia.scm.web.CollectingPackParserListener; //~--- JDK imports ------------------------------------------------------------ import java.io.IOException; import java.util.List; +import java.util.Map; /** * @@ -72,7 +75,7 @@ public class GitHookChangesetCollector //~--- constructors --------------------------------------------------------- /** - * Constructs ... + * Constructs a new instance * * * @param rpack @@ -83,19 +86,19 @@ public class GitHookChangesetCollector { this.rpack = rpack; this.receiveCommands = receiveCommands; + this.listener = CollectingPackParserListener.get(rpack); } //~--- methods -------------------------------------------------------------- /** - * Method description + * Collect all new changesets from the received hook. * - * - * @return + * @return new changesets */ public List collectChangesets() { - List changesets = Lists.newArrayList(); + Map changesets = Maps.newLinkedHashMap(); org.eclipse.jgit.lib.Repository repository = rpack.getRepository(); @@ -110,7 +113,19 @@ public class GitHookChangesetCollector for (ReceiveCommand rc : receiveCommands) { - if (rc.getType() != ReceiveCommand.Type.DELETE) + String ref = rc.getRefName(); + + logger.trace("handle receive command, type={}, ref={}, result={}", rc.getType(), ref, rc.getResult()); + + if (rc.getType() == ReceiveCommand.Type.DELETE) + { + logger.debug("skip delete of ref {}", ref); + } + else if (! GitUtil.isBranch(ref)) + { + logger.debug("skip ref {}, because it is not a branch", ref); + } + else { try { @@ -124,13 +139,10 @@ public class GitHookChangesetCollector builder.append(rc.getType()).append(", ref="); builder.append(rc.getRefName()).append(", result="); builder.append(rc.getResult()); + logger.error(builder.toString(), ex); } } - else - { - logger.debug("skip delete of branch {}", rc.getRefName()); - } } } @@ -144,35 +156,13 @@ public class GitHookChangesetCollector GitUtil.release(walk); } - return changesets; + return Lists.newArrayList(changesets.values()); } - /** - * Method description - * - * - * @param changesets - * @param converter - * @param walk - * @param rc - * - * @throws IOException - * @throws IncorrectObjectTypeException - */ - private void collectChangesets(List changesets, + private void collectChangesets(Map changesets, GitChangesetConverter converter, RevWalk walk, ReceiveCommand rc) throws IncorrectObjectTypeException, IOException { - //J- - logger.trace("handle receive command, type={}, ref={}, result={}", - new Object[] { - rc.getType(), - rc.getRefName(), - rc.getResult() - } - ); - //J+ - ObjectId newId = rc.getNewId(); String branch = GitUtil.getBranch(rc.getRefName()); @@ -187,7 +177,7 @@ public class GitHookChangesetCollector ObjectId oldId = rc.getOldId(); - if ((oldId != null) &&!oldId.equals(ObjectId.zeroId())) + if ((oldId != null) && !oldId.equals(ObjectId.zeroId())) { logger.trace("mark {} as uninteresting for rev walk", oldId.getName()); @@ -196,19 +186,39 @@ public class GitHookChangesetCollector RevCommit commit = walk.next(); - List branches = Lists.newArrayList(branch); - while (commit != null) { + String id = commit.getId().name(); + Changeset changeset = changesets.get(id); - // parse commit body to avoid npe - walk.parseBody(commit); + if (changeset != null) + { + logger.trace( + "commit {} already received durring this push, add branch {} to the commit", + commit, branch); + changeset.getBranches().add(branch); + } + else + { - Changeset changeset = converter.createChangeset(commit, branches); + // only append new commits + if (listener.isNew(commit)) + { - logger.trace("retrive commit {} for hook", changeset.getId()); + // parse commit body to avoid npe + walk.parseBody(commit); - changesets.add(changeset); + changeset = converter.createChangeset(commit, branch); + + logger.trace("retrieve commit {} for hook", changeset.getId()); + + changesets.put(id, changeset); + } + else + { + logger.trace("commit {} was already received", commit.getId()); + } + } commit = walk.next(); } @@ -216,9 +226,10 @@ public class GitHookChangesetCollector //~--- fields --------------------------------------------------------------- - /** Field description */ + /** listener to track new objects */ + private final CollectingPackParserListener listener; + private final List receiveCommands; - /** Field description */ private final ReceivePack rpack; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java index 2264859e57..0c6d9b9f7e 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java @@ -35,6 +35,7 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.base.Strings; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -50,6 +51,11 @@ import sonia.scm.repository.spi.GitRepositoryServiceProvider; import java.io.File; import java.io.IOException; import sonia.scm.store.ConfigurationStoreFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContextProvider; +import sonia.scm.schedule.Scheduler; +import sonia.scm.schedule.Task; /** * @@ -66,19 +72,27 @@ public class GitRepositoryHandler /** Field description */ public static final String RESOURCE_VERSION = - "/sonia/scm/version/scm-git-plugin"; + "sonia/scm/version/scm-git-plugin"; /** Field description */ public static final String TYPE_DISPLAYNAME = "Git"; /** Field description */ public static final String TYPE_NAME = "git"; + + private static final Logger logger = LoggerFactory.getLogger(GitRepositoryHandler.class); /** Field description */ public static final Type TYPE = new RepositoryType(TYPE_NAME, TYPE_DISPLAYNAME, GitRepositoryServiceProvider.COMMANDS); + private static final Object LOCK = new Object(); + + private final Scheduler scheduler; + + private Task task; + //~--- constructors --------------------------------------------------------- /** @@ -87,15 +101,48 @@ public class GitRepositoryHandler * * @param storeFactory * @param fileSystem + * @param scheduler */ @Inject - public GitRepositoryHandler(ConfigurationStoreFactory storeFactory, FileSystem fileSystem) + public GitRepositoryHandler(ConfigurationStoreFactory storeFactory, FileSystem fileSystem, Scheduler scheduler) { super(storeFactory, fileSystem); + this.scheduler = scheduler; } //~--- get methods ---------------------------------------------------------- + @Override + public void init(SCMContextProvider context) + { + super.init(context); + scheduleGc(); + } + + @Override + public void setConfig(GitConfig config) + { + super.setConfig(config); + scheduleGc(); + } + + private void scheduleGc() + { + synchronized (LOCK){ + if ( task != null ){ + logger.debug("cancel existing git gc task"); + task.cancel(); + task = null; + } + String exp = getConfig().getGcExpression(); + if (!Strings.isNullOrEmpty(exp)) + { + logger.info("schedule git gc task with expression {}", exp); + task = scheduler.schedule(exp, GitGcTask.class); + } + } + } + /** * Method description * diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryModifyListener.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryModifyListener.java new file mode 100644 index 0000000000..db2b7b09ec --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryModifyListener.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.repository; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Objects; +import com.google.common.eventbus.Subscribe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.EagerSingleton; +import sonia.scm.HandlerEventType; +import sonia.scm.event.ScmEventBus; +import sonia.scm.plugin.Extension; + +/** + * Repository listener which handles git related repository events. + * + * @author Sebastian Sdorra + * @since 1.50 + */ +@Extension +@EagerSingleton +public class GitRepositoryModifyListener { + + /** + * the logger for GitRepositoryModifyListener + */ + private static final Logger logger = LoggerFactory.getLogger(GitRepositoryModifyListener.class); + + /** + * Receives {@link RepositoryModificationEvent} and fires a {@link ClearRepositoryCacheEvent} if + * the default branch of a git repository was modified. + * + * @param event repository modification event + */ + @Subscribe + public void handleEvent(RepositoryModificationEvent event){ + Repository repository = event.getItem(); + + if ( isModifyEvent(event) && + isGitRepository(event.getItem()) && + hasDefaultBranchChanged(event.getItemBeforeModification(), repository)) + { + logger.info("git default branch of repository {} has changed, sending clear cache event", repository.getId()); + sendClearRepositoryCacheEvent(repository); + } + } + + @VisibleForTesting + protected void sendClearRepositoryCacheEvent(Repository repository) { + ScmEventBus.getInstance().post(new ClearRepositoryCacheEvent(repository)); + } + + private boolean isModifyEvent(RepositoryEvent event) { + return event.getEventType() == HandlerEventType.MODIFY; + } + + private boolean isGitRepository(Repository repository) { + return GitRepositoryHandler.TYPE_NAME.equals(repository.getType()); + } + + private boolean hasDefaultBranchChanged(Repository old, Repository current) { + return !Objects.equal( + old.getProperty(GitConstants.PROPERTY_DEFAULT_BRANCH), + current.getProperty(GitConstants.PROPERTY_DEFAULT_BRANCH) + ); + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java index b2175f8989..75132deeb0 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java @@ -36,6 +36,7 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; @@ -262,7 +263,7 @@ public final class GitUtil { if (formatter != null) { - formatter.release(); + formatter.close(); } } @@ -276,7 +277,7 @@ public final class GitUtil { if (walk != null) { - walk.release(); + walk.close(); } } @@ -290,7 +291,7 @@ public final class GitUtil { if (walk != null) { - walk.release(); + walk.close();; } } @@ -335,6 +336,20 @@ public final class GitUtil return branch; } + + /** + * Returns {@code true} if the provided reference name is a branch name. + * + * @param refName reference name + * + * @return {@code true} if the name is a branch name + * + * @since 1.50 + */ + public static boolean isBranch(String refName) + { + return Strings.nullToEmpty(refName).startsWith(PREFIX_HEADS); + } /** * Method description @@ -631,6 +646,26 @@ public final class GitUtil return String.format(REMOTE_REF, repository.getId(), branch); } + /** + * Returns the name of the tag or {@code null} if the the ref is not a tag. + * + * @param refName ref name + * + * @return name of tag or {@link null} + * + * @since 1.50 + */ + public static String getTagName(String refName) + { + String tagName = null; + if (refName.startsWith(PREFIX_TAG)) + { + tagName = refName.substring(PREFIX_TAG.length()); + } + + return tagName; + } + /** * Method description * diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookBranchProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookBranchProvider.java new file mode 100644 index 0000000000..23b3ce0196 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookBranchProvider.java @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.repository.api; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; + +import org.eclipse.jgit.transport.ReceiveCommand; +import org.eclipse.jgit.transport.ReceiveCommand.Type; + +import sonia.scm.repository.GitUtil; + +//~--- JDK imports ------------------------------------------------------------ + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Collects created, modified and deleted git branches during a hook. + * + * @author Sebastian Sdorra + */ +public class GitHookBranchProvider implements HookBranchProvider +{ + + private static final Logger logger = LoggerFactory.getLogger(GitHookBranchProvider.class); + + /** + * Constructs a new instance. + * + * + * @param commands received git commands + */ + public GitHookBranchProvider(List commands) + { + Builder createdOrModifiedBuilder = ImmutableList.builder(); + Builder deletedOrClosedBuilder = ImmutableList.builder(); + + for (ReceiveCommand command : commands) + { + Type type = command.getType(); + String ref = command.getRefName(); + String branch = GitUtil.getBranch(ref); + + if (Strings.isNullOrEmpty(branch)) + { + logger.debug("ref {} is not a branch", ref); + } + else if (isCreateOrUpdate(type)) + { + createdOrModifiedBuilder.add(branch); + } + else if (command.getType() == Type.DELETE) + { + deletedOrClosedBuilder.add(branch); + } + } + + createdOrModified = createdOrModifiedBuilder.build(); + deletedOrClosed = deletedOrClosedBuilder.build(); + } + + private boolean isCreateOrUpdate(Type type){ + return type == Type.CREATE || type == Type.UPDATE || type == Type.UPDATE_NONFASTFORWARD; + } + + //~--- get methods ---------------------------------------------------------- + + @Override + public List getCreatedOrModified() + { + return createdOrModified; + } + + @Override + public List getDeletedOrClosed() + { + return deletedOrClosed; + } + + //~--- fields --------------------------------------------------------------- + + private final List createdOrModified; + + private final List deletedOrClosed; +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookTagProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookTagProvider.java new file mode 100644 index 0000000000..e7a75a0ff4 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookTagProvider.java @@ -0,0 +1,92 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.repository.api; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import java.util.List; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.GitUtil; +import sonia.scm.repository.Tag; + +/** + * Git provider implementation of {@link HookTagProvider}. + * + * @since 1.50 + * @author Sebastian Sdorra + */ +public class GitHookTagProvider implements HookTagProvider { + + private static final Logger logger = LoggerFactory.getLogger(GitHookTagProvider.class); + + private final List createdTags; + private final List deletedTags; + + /** + * Constructs new instance. + * + * @param commands received commands + */ + public GitHookTagProvider(List commands) { + ImmutableList.Builder createdTagBuilder = ImmutableList.builder(); + ImmutableList.Builder deletedTagBuilder = ImmutableList.builder(); + + for ( ReceiveCommand rc : commands ){ + String refName = rc.getRefName(); + String tag = GitUtil.getTagName(refName); + + if (Strings.isNullOrEmpty(tag)){ + logger.debug("received ref name {} is not a tag", refName); + } else if (rc.getType() == ReceiveCommand.Type.CREATE) { + createdTagBuilder.add(new Tag(tag, GitUtil.getId(rc.getNewId()))); + } else if (rc.getType() == ReceiveCommand.Type.DELETE){ + deletedTagBuilder.add(new Tag(tag, GitUtil.getId(rc.getOldId()))); + } + } + + createdTags = createdTagBuilder.build(); + deletedTags = deletedTagBuilder.build(); + } + + @Override + public List getCreatedTags() { + return createdTags; + } + + @Override + public List getDeletedTags() { + return deletedTags; + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java index 9bf825c0e2..d098c30b4e 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java @@ -34,11 +34,18 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; import org.eclipse.jgit.lib.Repository; //~--- JDK imports ------------------------------------------------------------ import java.io.IOException; +import org.eclipse.jgit.lib.ObjectId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.GitConstants; +import sonia.scm.repository.GitUtil; /** * @@ -46,6 +53,11 @@ import java.io.IOException; */ public class AbstractGitCommand { + + /** + * the logger for AbstractGitCommand + */ + private static final Logger logger = LoggerFactory.getLogger(AbstractGitCommand.class); /** * Constructs ... @@ -75,6 +87,38 @@ public class AbstractGitCommand { return context.open(); } + + protected ObjectId getCommitOrDefault(Repository gitRepository, String requestedCommit) throws IOException { + ObjectId commit; + if ( Strings.isNullOrEmpty(requestedCommit) ) { + commit = getDefaultBranch(gitRepository); + } else { + commit = gitRepository.resolve(requestedCommit); + } + return commit; + } + + protected ObjectId getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException { + ObjectId head; + if ( Strings.isNullOrEmpty(requestedBranch) ) { + head = getDefaultBranch(gitRepository); + } else { + head = GitUtil.getBranchId(gitRepository, requestedBranch); + } + return head; + } + + protected ObjectId getDefaultBranch(Repository gitRepository) throws IOException { + ObjectId head; + String defaultBranchName = repository.getProperty(GitConstants.PROPERTY_DEFAULT_BRANCH); + if (!Strings.isNullOrEmpty(defaultBranchName)) { + head = GitUtil.getBranchId(gitRepository, defaultBranchName); + } else { + logger.trace("no default branch configured, use repository head as default"); + head = GitUtil.getRepositoryHead(gitRepository); + } + return head; + } //~--- fields --------------------------------------------------------------- diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java index d510cd3b51..e3fec1c8e8 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java @@ -138,7 +138,7 @@ public abstract class AbstractGitIncomingOutgoingCommand GitUtil.fetch(git, handler.getDirectory(remoteRepository), remoteRepository); - ObjectId localId = GitUtil.getRepositoryHead(git.getRepository()); + ObjectId localId = getDefaultBranch(git.getRepository()); ObjectId remoteId = null; Ref remoteBranch = getRemoteBranch(git.getRepository(), localId, diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java index a48b4b2b1e..c797fd70eb 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java @@ -124,7 +124,7 @@ public class GitBlameCommand extends AbstractGitCommand implements BlameCommand blame.setFilePath(request.getPath()); - ObjectId revId = GitUtil.getRevisionId(gr, request.getRevision()); + ObjectId revId = getCommitOrDefault(gr, request.getRevision()); blame.setStartCommit(revId); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java index 6ffa1f28de..df32c2b5eb 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java @@ -105,7 +105,7 @@ public class GitBranchesCommand extends AbstractGitCommand if (branchName != null) { - branch = new Branch(branchName); + branch = new Branch(branchName, GitUtil.getId(ref.getObjectId())); } return branch; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java index f4a7a06c23..413e3fbb14 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java @@ -71,7 +71,6 @@ import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import sonia.scm.util.IOUtil; /** @@ -96,8 +95,6 @@ public class GitBrowseCommand extends AbstractGitCommand /** * Constructs ... * - * - * * @param context * @param repository */ @@ -124,18 +121,15 @@ public class GitBrowseCommand extends AbstractGitCommand public BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException, RepositoryException { - if (logger.isDebugEnabled()) - { - logger.debug("try to create browse result for {}", request); - } + logger.debug("try to create browse result for {}", request); - BrowserResult result = null; + BrowserResult result; org.eclipse.jgit.lib.Repository repo = open(); - ObjectId revId = null; + ObjectId revId; if (Util.isEmpty(request.getRevision())) { - revId = GitUtil.getRepositoryHead(repo); + revId = getDefaultBranch(repo); } else { @@ -169,70 +163,6 @@ public class GitBrowseCommand extends AbstractGitCommand /** * Method description * - * - * @param files - * @param repo - * @param revId - * @param path - * - * @throws IOException - * @throws RepositoryException - */ - private void appendSubModules(List files, - org.eclipse.jgit.lib.Repository repo, ObjectId revId, String path) - throws IOException, RepositoryException - { - path = Util.nonNull(path); - - Map subRepositories = subrepositoryCache.get(revId); - - if (subRepositories == null) - { - subRepositories = getSubRepositories(repo, revId); - subrepositoryCache.put(revId, subRepositories); - } - - if (subRepositories != null) - { - for (Entry e : subRepositories.entrySet()) - { - String p = e.getKey(); - - if (p.startsWith(path)) - { - p = p.substring(path.length()); - - if (p.startsWith("/")) - { - p = p.substring(1); - } - - if (p.endsWith("/")) - { - p = p.substring(0, p.length() - 1); - } - - if (!p.contains("/")) - { - FileObject fo = new FileObject(); - - fo.setDirectory(true); - fo.setPath(path); - fo.setName(p); - fo.setSubRepository(e.getValue()); - files.add(fo); - } - } - } - } - } - - /** - * Method description - * - * - * - * * @param repo * @param request * @param revId @@ -244,9 +174,9 @@ public class GitBrowseCommand extends AbstractGitCommand */ private FileObject createFileObject(org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) - throws IOException + throws IOException, RepositoryException { - FileObject file = null; + FileObject file; try { @@ -257,26 +187,43 @@ public class GitBrowseCommand extends AbstractGitCommand file.setName(treeWalk.getNameString()); file.setPath(path); - ObjectLoader loader = repo.open(treeWalk.getObjectId(0)); + SubRepository sub = null; - file.setDirectory(loader.getType() == Constants.OBJ_TREE); - file.setLength(loader.getSize()); - - // don't show message and date for directories to improve performance - if (!file.isDirectory() &&!request.isDisableLastCommit()) + if (!request.isDisableSubRepositoryDetection()) { - logger.trace("fetch last commit for {} at {}", path, revId.getName()); + sub = getSubRepository(repo, revId, path); + } - RevCommit commit = getLatestCommit(repo, revId, path); + if (sub != null) + { + logger.trace("{} seems to be a sub repository", path); + file.setDirectory(true); + file.setSubRepository(sub); + } + else + { + ObjectLoader loader = repo.open(treeWalk.getObjectId(0)); - if (commit != null) + file.setDirectory(loader.getType() == Constants.OBJ_TREE); + file.setLength(loader.getSize()); + + // don't show message and date for directories to improve performance + if (!file.isDirectory() &&!request.isDisableLastCommit()) { - file.setLastModified(GitUtil.getCommitTime(commit)); - file.setDescription(commit.getShortMessage()); - } - else if (logger.isWarnEnabled()) - { - logger.warn("could not find latest commit for {} on {}", path, revId); + logger.trace("fetch last commit for {} at {}", path, revId.getName()); + + RevCommit commit = getLatestCommit(repo, revId, path); + + if (commit != null) + { + file.setLastModified(GitUtil.getCommitTime(commit)); + file.setDescription(commit.getShortMessage()); + } + else if (logger.isWarnEnabled()) + { + logger.warn("could not find latest commit for {} on {}", path, + revId); + } } } } @@ -386,11 +333,6 @@ public class GitBrowseCommand extends AbstractGitCommand String path = request.getPath(); - if (!request.isDisableSubRepositoryDetection()) - { - appendSubModules(files, repo, revId, path); - } - if (Util.isEmpty(path)) { while (treeWalk.next()) @@ -496,6 +438,28 @@ public class GitBrowseCommand extends AbstractGitCommand return subRepositories; } + private SubRepository getSubRepository(org.eclipse.jgit.lib.Repository repo, + ObjectId revId, String path) + throws IOException, RepositoryException + { + Map subRepositories = subrepositoryCache.get(revId); + + if (subRepositories == null) + { + subRepositories = getSubRepositories(repo, revId); + subrepositoryCache.put(revId, subRepositories); + } + + SubRepository sub = null; + + if (subRepositories != null) + { + sub = subRepositories.get(path); + } + + return sub; + } + //~--- fields --------------------------------------------------------------- /** Field description */ diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java index 17dad775b1..4420b9a22a 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java @@ -34,6 +34,7 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.base.Strings; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; @@ -78,7 +79,6 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand * * @param context * @param repository - * @param repositoryDirectory */ public GitCatCommand(GitContext context, sonia.scm.repository.Repository repository) @@ -102,17 +102,11 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand public void getCatResult(CatCommandRequest request, OutputStream output) throws IOException, RepositoryException { - if (logger.isDebugEnabled()) - { - logger.debug("try to read content for {}", request); - } - - org.eclipse.jgit.lib.Repository repo = null; - - repo = open(); - - ObjectId revId = GitUtil.getRevisionId(repo, request.getRevision()); + logger.debug("try to read content for {}", request); + org.eclipse.jgit.lib.Repository repo = open(); + + ObjectId revId = getCommitOrDefault(repo, request.getRevision()); getContent(repo, revId, request.getPath(), output); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java index a8edfa8848..83977ef290 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java @@ -79,7 +79,6 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand * * @param context * @param repository - * @param repositoryDirectory */ public GitDiffCommand(GitContext context, Repository repository) { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookContextProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookContextProvider.java index 824247e81f..0d3a30ed52 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookContextProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookContextProvider.java @@ -36,7 +36,9 @@ package sonia.scm.repository.spi; import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.transport.ReceivePack; +import sonia.scm.repository.api.GitHookBranchProvider; import sonia.scm.repository.api.GitHookMessageProvider; +import sonia.scm.repository.api.HookBranchProvider; import sonia.scm.repository.api.HookFeature; import sonia.scm.repository.api.HookMessageProvider; @@ -45,6 +47,8 @@ import sonia.scm.repository.api.HookMessageProvider; import java.util.EnumSet; import java.util.List; import java.util.Set; +import sonia.scm.repository.api.GitHookTagProvider; +import sonia.scm.repository.api.HookTagProvider; /** * @@ -55,33 +59,28 @@ public class GitHookContextProvider extends HookContextProvider /** Field description */ private static final Set SUPPORTED_FEATURES = - EnumSet.of(HookFeature.MESSAGE_PROVIDER, HookFeature.CHANGESET_PROVIDER); + EnumSet.of(HookFeature.MESSAGE_PROVIDER, HookFeature.CHANGESET_PROVIDER, + HookFeature.BRANCH_PROVIDER, HookFeature.TAG_PROVIDER); //~--- constructors --------------------------------------------------------- /** - * Constructs ... + * Constructs a new instance * - * - * @param receivePack - * @param receiveCommands + * @param receivePack git receive pack + * @param receiveCommands received commands */ public GitHookContextProvider(ReceivePack receivePack, List receiveCommands) { this.receivePack = receivePack; + this.receiveCommands = receiveCommands; this.changesetProvider = new GitHookChangesetProvider(receivePack, receiveCommands); } //~--- methods -------------------------------------------------------------- - /** - * Method description - * - * - * @return - */ @Override public HookMessageProvider createMessageProvider() { @@ -90,24 +89,23 @@ public class GitHookContextProvider extends HookContextProvider //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @return - */ + @Override + public HookBranchProvider getBranchProvider() + { + return new GitHookBranchProvider(receiveCommands); + } + + @Override + public HookTagProvider getTagProvider() { + return new GitHookTagProvider(receiveCommands); + } + @Override public HookChangesetProvider getChangesetProvider() { return changesetProvider; } - /** - * Method description - * - * - * @return - */ @Override public Set getSupportedFeatures() { @@ -117,8 +115,11 @@ public class GitHookContextProvider extends HookContextProvider //~--- fields --------------------------------------------------------------- /** Field description */ - private GitHookChangesetProvider changesetProvider; + private final GitHookChangesetProvider changesetProvider; /** Field description */ - private ReceivePack receivePack; + private final List receiveCommands; + + /** Field description */ + private final ReceivePack receivePack; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java index 35ba01f584..0643035e7a 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java @@ -221,17 +221,8 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand AndTreeFilter.create( PathFilter.create(request.getPath()), TreeFilter.ANY_DIFF)); } - - ObjectId head = null; - - if (!Strings.isNullOrEmpty(request.getBranch())) - { - head = GitUtil.getBranchId(gr, request.getBranch()); - } - else - { - head = GitUtil.getRepositoryHead(gr); - } + + ObjectId head = getBranchOrDefault(gr, request.getBranch()); if (head != null) { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/CollectingPackParserListener.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/CollectingPackParserListener.java new file mode 100644 index 0000000000..fdb96d18d7 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/CollectingPackParserListener.java @@ -0,0 +1,195 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.web; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableSet; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdSubclassMap; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.transport.BaseReceivePack; +import org.eclipse.jgit.transport.BaseReceivePack.PackParserListener; +import org.eclipse.jgit.transport.PackParser; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +//~--- JDK imports ------------------------------------------------------------ + +import java.util.Set; + +/** + * Implementation of {@link PackParserListener} to collect every object which is + * pushed with the reveive pack. The listener is used to find out which object + * is new and which was already pushed. + * + * @author Sebastian Sdorra + */ +public class CollectingPackParserListener implements PackParserListener +{ + + /** + * the logger for CollectingPackParserListener + */ + private static final Logger logger = + LoggerFactory.getLogger(CollectingPackParserListener.class); + + //~--- get methods ---------------------------------------------------------- + + /** + * Returns the listener from the receive pack. + * + * + * @param pack receive pack + * + * @return listener + */ + public static CollectingPackParserListener get(BaseReceivePack pack) + { + PackParserListener listener = pack.getPackParserListener(); + + if (listener == null) + { + throw new IllegalArgumentException( + "receive pack does not contain a listener"); + } + + Preconditions.checkArgument( + listener instanceof CollectingPackParserListener, + "listener is not a CollectingPackParserListener"); + + return (CollectingPackParserListener) listener; + } + + //~--- set methods ---------------------------------------------------------- + + /** + * Applies the listener to the receive pack. + * + * + * @param pack receive pack + */ + public static void set(BaseReceivePack pack) + { + logger.trace("apply collecting listener to receive pack"); + pack.setPackParserListener(new CollectingPackParserListener()); + } + + //~--- methods -------------------------------------------------------------- + + /** + * Collects all new object ids. + * + * + * @param parser pack parser + */ + @Override + public void after(PackParser parser) + { + logger.trace("retrieve new object ids from pack parser"); + + ObjectIdSubclassMap newObjectIdMap = parser.getNewObjectIds(); + + if (newObjectIdMap != null) + { + newObjectIds = ImmutableSet.copyOf(newObjectIdMap); + } + else + { + logger.warn("pack parser returned no newObjectIds"); + newObjectIds = ImmutableSet.of(); + } + + if (newObjectIds.isEmpty()) + { + logger.debug("new object ids are empty, we treat every commit as new"); + } + else + { + logger.debug("collected {} new object ids", newObjectIds.size()); + } + } + + /** + * Prepares the pack parser to retrieve the new object ids. + * + * + * @param parser pack parser + */ + @Override + public void before(PackParser parser) + { + logger.trace("prepare pack parser to collect new object ids"); + parser.setNeedNewObjectIds(true); + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Returns {@code true} if the object is a new object. The method will also + * return {@code true}, if the pack parser does not return a list with new + * object ids. + * + * + * @param object rev object + * + * @return {@code true} if the object is new + */ + public boolean isNew(RevObject object) + { + ensureAfterWasCalled(); + + return newObjectIds.isEmpty() || newObjectIds.contains(object.getId()); + } + + //~--- methods -------------------------------------------------------------- + + /** + * Throws an {@link IllegalStateException} if the after method was not called. + */ + private void ensureAfterWasCalled() + { + if (newObjectIds == null) + { + throw new IllegalStateException( "Pack parser seem not to be finished. " + + "The receive pack has not called the after method of the listener."); + } + } + + //~--- fields --------------------------------------------------------------- + + /** set of new object ids */ + private Set newObjectIds; +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitBasicAuthenticationFilter.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitBasicAuthenticationFilter.java index 1bc6ce00e1..f06fcfcd39 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitBasicAuthenticationFilter.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitBasicAuthenticationFilter.java @@ -35,27 +35,18 @@ package sonia.scm.web; import com.google.inject.Inject; -import org.eclipse.jgit.http.server.GitSmartHttpTools; - -import sonia.scm.ClientMessages; import sonia.scm.Priority; import sonia.scm.config.ScmConfiguration; import sonia.scm.filter.Filters; import sonia.scm.filter.WebElement; -import sonia.scm.repository.GitUtil; import sonia.scm.web.filter.AuthenticationFilter; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - import java.util.Set; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; /** - * + * Handles git specific basic authentication. + * * @author Sebastian Sdorra */ @Priority(Filters.PRIORITY_AUTHENTICATION) @@ -64,11 +55,10 @@ public class GitBasicAuthenticationFilter extends AuthenticationFilter { /** - * Constructs ... + * Constructs a new instance. * - * - * @param configuration - * @param webTokenGenerators + * @param configuration scm-manager main configuration + * @param webTokenGenerators web token generators */ @Inject public GitBasicAuthenticationFilter(ScmConfiguration configuration, @@ -76,32 +66,4 @@ public class GitBasicAuthenticationFilter extends AuthenticationFilter { super(configuration, webTokenGenerators); } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * @param response - * - * @throws IOException - */ - @Override - protected void sendFailedAuthenticationError(HttpServletRequest request, - HttpServletResponse response) - throws IOException - { - if (GitUtil.isGitClient(request)) - { - GitSmartHttpTools.sendError(request, response, - HttpServletResponse.SC_FORBIDDEN, - ClientMessages.get(request).failedAuthentication()); - } - else - { - super.sendFailedAuthenticationError(request, response); - } - } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java index 535f0a2714..25bbe04cfc 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java @@ -96,7 +96,9 @@ public class GitReceivePackFactory rpack.setPreReceiveHook(hook); rpack.setPostReceiveHook(hook); - + // apply collecting listener, to be able to check which commits are new + CollectingPackParserListener.set(rpack); + return rpack; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitUserAgentProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitUserAgentProvider.java new file mode 100644 index 0000000000..0fc6495066 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitUserAgentProvider.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.web; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Charsets; + +import sonia.scm.plugin.Extension; + +/** + * + * @author Sebastian Sdorra + * @since 1.45 + */ +@Extension +public class GitUserAgentProvider implements UserAgentProvider +{ + + /** Field description */ + @VisibleForTesting + static final UserAgent GIT = UserAgent.builder("Git").browser( + false).basicAuthenticationCharset( + Charsets.UTF_8).build(); + + /** Field description */ + @VisibleForTesting + static final UserAgent MSYSGIT = UserAgent.builder("msysGit").browser( + false).basicAuthenticationCharset( + Charsets.UTF_8).build(); + + /** Field description */ + private static final String PREFIX = "git/"; + + /** Field description */ + private static final String SUFFIX = "msysgit"; + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @param userAgentString + * + * @return + */ + @Override + public UserAgent parseUserAgent(String userAgentString) + { + UserAgent ua = null; + + if (userAgentString.startsWith(PREFIX)) + { + if (userAgentString.contains(SUFFIX)) + { + ua = MSYSGIT; + } + else + { + ua = GIT; + } + } + + return ua; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServlet.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServlet.java index dbdd12809a..686e4dfbd7 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServlet.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServlet.java @@ -54,6 +54,7 @@ import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import sonia.scm.repository.RepositoryException; /** * @@ -170,7 +171,11 @@ public class ScmGitServlet extends GitServlet { repositoryViewer.handleRequest(request, response, scmRepository); } - catch (Exception ex) + catch (RepositoryException ex) + { + throw new ServletException("could not create repository view", ex); + } + catch (IOException ex) { throw new ServletException("could not create repository view", ex); } @@ -184,11 +189,11 @@ public class ScmGitServlet extends GitServlet //~--- fields --------------------------------------------------------------- /** Field description */ - private RepositoryProvider repositoryProvider; + private final RepositoryProvider repositoryProvider; /** Field description */ - private RepositoryRequestListenerUtil repositoryRequestListenerUtil; + private final RepositoryRequestListenerUtil repositoryRequestListenerUtil; /** Field description */ - private GitRepositoryViewer repositoryViewer; + private final GitRepositoryViewer repositoryViewer; } diff --git a/scm-plugins/scm-git-plugin/src/main/resources/sonia/scm/git.config.js b/scm-plugins/scm-git-plugin/src/main/resources/sonia/scm/git.config.js index cb0aa9f16c..9e038e4dee 100644 --- a/scm-plugins/scm-git-plugin/src/main/resources/sonia/scm/git.config.js +++ b/scm-plugins/scm-git-plugin/src/main/resources/sonia/scm/git.config.js @@ -36,10 +36,23 @@ Sonia.git.ConfigPanel = Ext.extend(Sonia.config.SimpleConfigForm, { // labels titleText: 'Git Settings', repositoryDirectoryText: 'Repository directory', + gcExpressionText: 'Git GC Cron Expression', disabledText: 'Disabled', // helpTexts repositoryDirectoryHelpText: 'Location of the Git repositories.', + // TODO i18n + gcExpressionHelpText: '

Use Quartz Cron Expressions (SECOND MINUTE HOUR DAYOFMONTH MONTH DAYOFWEEK) to run git gc in intervals.

\n\ + \n\ + \n\ + \n\ + \n\ + \n\ + \n\ + \n\ +
SECONDSeconds within the minute (0–59)
MINUTEMinutes within the hour (0–59)
HOURThe hour of the day (0–23)
DAYOFMONTHThe day of the month (1–31)
MONTHThe month (1–12)
DAYOFWEEKThe day of the week (MON, TUE, WED, THU, FRI, SAT, SUN)
\n\ +

E.g.: To run the task on every sunday at two o\'clock in the morning: 0 0 2 ? * SUN

\n\ +

For more informations please have a look at Quartz CronTrigger

', disabledHelpText: 'Enable or disable the Git plugin.\n\ Note you have to reload the page, after changing this value.', @@ -54,6 +67,12 @@ Sonia.git.ConfigPanel = Ext.extend(Sonia.config.SimpleConfigForm, { fieldLabel: this.repositoryDirectoryText, helpText: this.repositoryDirectoryHelpText, allowBlank : false + },{ + xtype: 'textfield', + name: 'gc-expression', + fieldLabel: this.gcExpressionText, + helpText: this.gcExpressionHelpText, + allowBlank : true },{ xtype: 'checkbox', name: 'disabled', @@ -71,6 +90,93 @@ Sonia.git.ConfigPanel = Ext.extend(Sonia.config.SimpleConfigForm, { Ext.reg("gitConfigPanel", Sonia.git.ConfigPanel); +// add default branch chooser to settings panel +Sonia.git.GitSettingsFormPanel = Ext.extend(Sonia.repository.SettingsFormPanel, { + + defaultBranchText: 'Default Branch', + defaultBranchHelpText: 'The default branch which is show first on source or commit view.', + + modifyDefaultConfig: function(config){ + if (this.item) { + var position = -1; + for ( var i=0; i= 0) { + config.items.splice(position, 0, defaultBranchComboxBox); + } else { + config.items.push(defaultBranchComboxBox); + } + } + }, + + getDefaultBranch: function(item){ + if (item.properties) { + for ( var i=0; i{0}', xtype: 'repositoryExtendedInfoPanel' }); + main.registerSettingsForm('git', { + xtype: 'gitSettingsForm' + }); }); // register panel diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitGcTaskTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitGcTaskTest.java new file mode 100644 index 0000000000..57fdd59c2f --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitGcTaskTest.java @@ -0,0 +1,128 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.repository; + +import com.google.common.collect.Lists; +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Properties; +import org.eclipse.jgit.api.GarbageCollectCommand; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.junit.Test; +import static org.mockito.Mockito.*; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +/** + * Unit tests for {@link GitGcTask}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class GitGcTaskTest +{ + + @Mock + private RepositoryManager manager; + + @Mock + private RepositoryDirectoryHandler handler; + + @Mock + private GarbageCollectCommand gcc; + + @Mock + private Git git; + + private GitGcTask task; + + /** + * Setup mocks for tests. + * + * @throws GitAPIException + */ + @Before + public void setUp() throws GitAPIException + { + when(git.gc()).thenReturn(gcc); + when(gcc.getStatistics()).thenReturn(new Properties()); + when(gcc.call()).thenReturn(new Properties()); + when(manager.getHandler(GitRepositoryHandler.TYPE_NAME)).thenReturn(handler); + task = new GitGcTask(manager){ + + @Override + protected Git open(File file) throws IOException + { + return git; + } + + }; + } + + /** + * Tests {@link GitGcTask#run()}. + * + * @throws GitAPIException + */ + @Test + public void testRun() throws GitAPIException + { + // prepare repositories for task + Repository unhealthy = mock(Repository.class); + when(unhealthy.getType()).thenReturn("git"); + when(unhealthy.isHealthy()).thenReturn(Boolean.FALSE); + + Repository invalid = mock(Repository.class); + when(unhealthy.getType()).thenReturn("git"); + when(unhealthy.isValid()).thenReturn(Boolean.FALSE); + + List repositories = Lists.newArrayList( + RepositoryTestData.create42Puzzle("git"), + RepositoryTestData.createHeartOfGold("hg"), + unhealthy, + invalid + ); + when(manager.getAll()).thenReturn(repositories); + + // run + task.run(); + + // gc command should only be called once + verify(gcc).getStatistics(); + verify(gcc).call(); + } + +} \ No newline at end of file diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java index 5b146bc9c5..5d9b338875 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java @@ -43,14 +43,22 @@ import static org.junit.Assert.*; import java.io.File; import sonia.scm.store.ConfigurationStoreFactory; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.schedule.Scheduler; /** * * @author Sebastian Sdorra */ +@RunWith(MockitoJUnitRunner.class) public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { + @Mock + private Scheduler scheduler; + /** * Method description * @@ -90,7 +98,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase File directory) { GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory, - new DefaultFileSystem()); + new DefaultFileSystem(), scheduler); repositoryHandler.init(contextProvider); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryModifyListenerTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryModifyListenerTest.java new file mode 100644 index 0000000000..a542674484 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryModifyListenerTest.java @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.repository; + +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Before; +import sonia.scm.HandlerEventType; + +/** + * Unit tests for {@link GitRepositoryModifyListener}. + * + * @author Sebastian Sdorra + */ +public class GitRepositoryModifyListenerTest { + + private GitRepositoryModifyTestListener repositoryModifyListener; + + /** + * Set up test object. + */ + @Before + public void setUpObjectUnderTest(){ + repositoryModifyListener = new GitRepositoryModifyTestListener(); + } + + /** + * Tests happy path. + */ + @Test + public void testHandleEvent() { + Repository old = RepositoryTestData.createHeartOfGold("git"); + old.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "master"); + Repository current = RepositoryTestData.createHeartOfGold("git"); + current.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "develop"); + + RepositoryModificationEvent event = new RepositoryModificationEvent(HandlerEventType.MODIFY, current, old); + repositoryModifyListener.handleEvent(event); + + assertNotNull(repositoryModifyListener.repository); + assertSame(current, repositoryModifyListener.repository); + } + + /** + * Tests with new default branch. + */ + @Test + public void testWithNewDefaultBranch() { + Repository old = RepositoryTestData.createHeartOfGold("git"); + Repository current = RepositoryTestData.createHeartOfGold("git"); + current.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "develop"); + + RepositoryModificationEvent event = new RepositoryModificationEvent(HandlerEventType.MODIFY, current, old); + repositoryModifyListener.handleEvent(event); + + assertNotNull(repositoryModifyListener.repository); + assertSame(current, repositoryModifyListener.repository); + } + + /** + * Tests with non git repositories. + */ + @Test + public void testNonGitRepository(){ + Repository old = RepositoryTestData.createHeartOfGold("hg"); + old.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "master"); + Repository current = RepositoryTestData.createHeartOfGold("hg"); + current.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "develop"); + + RepositoryModificationEvent event = new RepositoryModificationEvent(HandlerEventType.MODIFY, current, old); + repositoryModifyListener.handleEvent(event); + + assertNull(repositoryModifyListener.repository); + } + + /** + * Tests without default branch. + */ + @Test + public void testWithoutDefaultBranch(){ + Repository old = RepositoryTestData.createHeartOfGold("git"); + Repository current = RepositoryTestData.createHeartOfGold("git"); + + RepositoryModificationEvent event = new RepositoryModificationEvent(HandlerEventType.MODIFY, current, old); + repositoryModifyListener.handleEvent(event); + + assertNull(repositoryModifyListener.repository); + } + + /** + * Tests with non modify event. + */ + @Test + public void testNonModifyEvent(){ + Repository old = RepositoryTestData.createHeartOfGold("git"); + old.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "master"); + Repository current = RepositoryTestData.createHeartOfGold("git"); + current.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "develop"); + + RepositoryModificationEvent event = new RepositoryModificationEvent(HandlerEventType.CREATE, current, old); + repositoryModifyListener.handleEvent(event); + + assertNull(repositoryModifyListener.repository); + } + + /** + * Tests with non git repositories. + */ + @Test + public void testNoModification(){ + Repository old = RepositoryTestData.createHeartOfGold("git"); + old.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "master"); + Repository current = RepositoryTestData.createHeartOfGold("git"); + current.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "master"); + + RepositoryModificationEvent event = new RepositoryModificationEvent(HandlerEventType.MODIFY, current, old); + repositoryModifyListener.handleEvent(event); + + assertNull(repositoryModifyListener.repository); + } + + private static class GitRepositoryModifyTestListener extends GitRepositoryModifyListener { + + private Repository repository; + + @Override + protected void sendClearRepositoryCacheEvent(Repository repository) { + this.repository = repository; + } + + } + + +} \ No newline at end of file diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitUtilTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitUtilTest.java index c34da3f738..10813098bc 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitUtilTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitUtilTest.java @@ -53,16 +53,18 @@ import static org.mockito.Mockito.*; import java.io.File; import java.io.IOException; +import static org.junit.Assert.*; + /** - * + * Unit tests for {@link GitUtil}. + * * @author Sebastian Sdorra */ public class GitUtilTest { /** - * Method description - * + * Tests {@link GitUtil#checkBranchName(org.eclipse.jgit.lib.Repository, java.lang.String)} with invalid name. * * @throws IOException */ @@ -76,8 +78,7 @@ public class GitUtilTest } /** - * Method description - * + * Tests {@link GitUtil#checkBranchName(org.eclipse.jgit.lib.Repository, java.lang.String)}. * * @throws IOException */ @@ -92,32 +93,28 @@ public class GitUtilTest } /** - * Method description - * - * - * @throws GitAPIException - * @throws IOException + * Tests {@link GitUtil#getTagName(java.lang.String)}. */ @Test - public void testOpenJava7() throws GitAPIException, IOException - { - File dir = temp.newFolder(); - - Git.init().setDirectory(dir).setBare(true).call(); - - org.eclipse.jgit.lib.Repository repo = GitUtil.open(dir); - - assertThat(repo.getFS().getClass().getName(), containsString("Java7")); + public void testGetTagName(){ + assertNull(GitUtil.getTagName("refs/head/master")); + assertEquals("1.0.0", GitUtil.getTagName("refs/tags/1.0.0")); + assertEquals("super/1.0.0", GitUtil.getTagName("refs/tags/super/1.0.0")); + } + + /** + * Tests {@link GitUtil#isBranch(java.lang.String)}. + */ + @Test + public void testIsBranchName(){ + assertTrue(GitUtil.isBranch("refs/heads/master")); + assertTrue(GitUtil.isBranch("refs/heads/feature/super")); + assertFalse(GitUtil.isBranch("")); + assertFalse(GitUtil.isBranch(null)); + assertFalse(GitUtil.isBranch("refs/tags/1.0.0")); + assertFalse(GitUtil.isBranch("refs/heads")); } - /** - * Method description - * - * - * @param directory - * - * @return - */ private org.eclipse.jgit.lib.Repository mockRepo(File directory) { org.eclipse.jgit.lib.Repository repo = diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/api/GitHookBranchProviderTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/api/GitHookBranchProviderTest.java new file mode 100644 index 0000000000..9e5e0eef36 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/api/GitHookBranchProviderTest.java @@ -0,0 +1,115 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.repository.api; + +import com.google.common.collect.Lists; +import java.util.Arrays; +import java.util.List; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.hamcrest.Matchers; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import static org.hamcrest.Matchers.*; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +/** + * Unit tests for {@link GitHookBranchProvider}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class GitHookBranchProviderTest { + + @Mock + private ReceiveCommand command; + + private List commands; + + /** + * Prepare mocks for upcoming test. + */ + @Before + public void setUpMocks(){ + commands = Lists.newArrayList(command); + } + + /** + * Tests {@link GitHookBranchProvider#getCreatedOrModified()}. + */ + @Test + public void testGetCreatedOrModified(){ + List types = Arrays.asList( + ReceiveCommand.Type.CREATE, ReceiveCommand.Type.UPDATE, ReceiveCommand.Type.UPDATE_NONFASTFORWARD + ); + for ( ReceiveCommand.Type type : types ){ + checkCreatedOrModified(type); + } + } + + private void checkCreatedOrModified(ReceiveCommand.Type type){ + GitHookBranchProvider provider = createGitHookBranchProvider(type, "refs/heads/hello"); + assertThat(provider.getCreatedOrModified(), Matchers.contains("hello")); + assertThat(provider.getDeletedOrClosed(), empty()); + } + + + /** + * Tests {@link GitHookBranchProvider#getDeletedOrClosed()}. + */ + @Test + public void testGetDeletedOrClosed(){ + GitHookBranchProvider provider = createGitHookBranchProvider(ReceiveCommand.Type.DELETE, "refs/heads/hello"); + assertThat(provider.getDeletedOrClosed(), Matchers.contains("hello")); + assertThat(provider.getCreatedOrModified(), empty()); + } + + /** + * Tests {@link GitHookBranchProvider} with a tag instead of a branch. + */ + @Test + public void testWithTag(){ + GitHookBranchProvider provider = createGitHookBranchProvider(ReceiveCommand.Type.CREATE, "refs/tags/1.0.0"); + assertThat(provider.getCreatedOrModified(), empty()); + assertThat(provider.getDeletedOrClosed(), empty()); + } + + private GitHookBranchProvider createGitHookBranchProvider(ReceiveCommand.Type type, String refName){ + when(command.getType()).thenReturn(type); + when(command.getRefName()).thenReturn(refName); + return new GitHookBranchProvider(commands); + } + +} \ No newline at end of file diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/api/GitHookTagProviderTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/api/GitHookTagProviderTest.java new file mode 100644 index 0000000000..87e277b633 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/api/GitHookTagProviderTest.java @@ -0,0 +1,130 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.repository.api; + +import com.google.common.collect.Lists; +import java.util.List; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import static org.hamcrest.Matchers.*; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.OngoingStubbing; +import sonia.scm.repository.Tag; + +/** + * Unit tests for {@link GitHookTagProvider}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class GitHookTagProviderTest { + + @Mock + private ReceiveCommand command; + + private List commands; + + /** + * Set up mocks for upcoming tests. + */ + @Before + public void setUpMocks(){ + commands = Lists.newArrayList(command); + } + + /** + * Tests {@link GitHookTagProvider#getCreatedTags()}. + */ + @Test + public void testGetCreatedTags() { + String revision = "b2002b64013e54b78eac251df0672bd5d6a83aa7"; + GitHookTagProvider provider = createProvider(ReceiveCommand.Type.CREATE, "refs/tags/1.0.0", revision); + + assertTag("1.0.0", revision, provider.getCreatedTags()); + assertThat(provider.getDeletedTags(), empty()); + } + + /** + * Tests {@link GitHookTagProvider#getDeletedTags()}. + */ + @Test + public void testGetDeletedTags() { + String revision = "b2002b64013e54b78eac251df0672bd5d6a83aa7"; + GitHookTagProvider provider = createProvider(ReceiveCommand.Type.DELETE, "refs/tags/1.0.0", revision); + + assertThat(provider.getCreatedTags(), empty()); + assertTag("1.0.0", revision, provider.getDeletedTags()); + } + + /** + * Tests {@link GitHookTagProvider} with a branch ref instead of a tag. + */ + @Test + public void testWithBranch(){ + String revision = "b2002b64013e54b78eac251df0672bd5d6a83aa7"; + GitHookTagProvider provider = createProvider(ReceiveCommand.Type.CREATE, "refs/heads/1.0.0", revision); + + assertThat(provider.getCreatedTags(), empty()); + assertThat(provider.getDeletedTags(), empty()); + } + + private void assertTag(String name, String revision, List tags){ + assertNotNull(tags); + assertFalse(tags.isEmpty()); + assertEquals(1, tags.size()); + Tag tag = tags.get(0); + assertEquals(name, tag.getName()); + assertEquals(revision, tag.getRevision()); + } + + private GitHookTagProvider createProvider(ReceiveCommand.Type type, String ref, String id){ + OngoingStubbing ongoing; + if (type == ReceiveCommand.Type.CREATE){ + ongoing = when(command.getNewId()); + } else { + ongoing = when(command.getOldId()); + } + ongoing.thenReturn(ObjectId.fromString(id)); + + when(command.getType()).thenReturn(type); + when(command.getRefName()).thenReturn(ref); + + return new GitHookTagProvider(commands); + } + +} \ No newline at end of file diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java index 953372020b..d049447d7f 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java @@ -45,14 +45,42 @@ import static org.junit.Assert.*; //~--- JDK imports ------------------------------------------------------------ import java.io.IOException; +import sonia.scm.repository.GitConstants; /** - * + * Unit tests for {@link GitBlameCommand}. + * * @author Sebastian Sdorra */ public class GitBlameCommandTest extends AbstractGitCommandTestBase { + /** + * Tests blame command with default branch. + * + * @throws IOException + * @throws RepositoryException + */ + @Test + public void testDefaultBranch() throws IOException, RepositoryException { + // without default branch, the repository head should be used + BlameCommandRequest request = new BlameCommandRequest(); + request.setPath("a.txt"); + + BlameResult result = createCommand().getBlameResult(request); + assertNotNull(result); + assertEquals(2, result.getTotal()); + assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getLine(0).getRevision()); + assertEquals("fcd0ef1831e4002ac43ea539f4094334c79ea9ec", result.getLine(1).getRevision()); + + // set default branch and test again + repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch"); + result = createCommand().getBlameResult(request); + assertNotNull(result); + assertEquals(1, result.getTotal()); + assertEquals("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4", result.getLine(0).getRevision()); + } + /** * Method description * diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java index 3999e26f85..727034b9ca 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java @@ -48,13 +48,51 @@ import static org.junit.Assert.*; import java.io.IOException; import java.util.List; +import sonia.scm.repository.GitConstants; /** - * + * Unit tests for {@link GitBrowseCommand}. + * * @author Sebastian Sdorra */ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { + + /** + * Test browse command with default branch. + * + * @throws IOException + * @throws RepositoryException + */ + @Test + public void testDefaultBranch() throws IOException, RepositoryException { + // without default branch, the repository head should be used + BrowserResult result = createCommand().getBrowserResult(new BrowseCommandRequest()); + assertNotNull(result); + + List foList = result.getFiles(); + assertNotNull(foList); + assertFalse(foList.isEmpty()); + assertEquals(4, foList.size()); + + assertEquals("a.txt", foList.get(0).getName()); + assertEquals("b.txt", foList.get(1).getName()); + assertEquals("c", foList.get(2).getName()); + assertEquals("f.txt", foList.get(3).getName()); + + // set default branch and fetch again + repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch"); + result = createCommand().getBrowserResult(new BrowseCommandRequest()); + assertNotNull(result); + + foList = result.getFiles(); + assertNotNull(foList); + assertFalse(foList.isEmpty()); + assertEquals(2, foList.size()); + + assertEquals("a.txt", foList.get(0).getName()); + assertEquals("c", foList.get(1).getName()); + } /** * Method description diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java index 1a44a0076c..ede6a53429 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java @@ -44,14 +44,36 @@ import static org.junit.Assert.*; import java.io.ByteArrayOutputStream; import java.io.IOException; +import sonia.scm.repository.GitConstants; /** + * Unit tests for {@link GitCatCommand}. + * * TODO add not found test * * @author Sebastian Sdorra */ public class GitCatCommandTest extends AbstractGitCommandTestBase { + + /** + * Tests cat command with default branch. + * + * @throws IOException + * @throws RepositoryException + */ + @Test + public void testDefaultBranch() throws IOException, RepositoryException { + // without default branch, the repository head should be used + CatCommandRequest request = new CatCommandRequest(); + request.setPath("a.txt"); + + assertEquals("a\nline for blame", execute(request)); + + // set default branch for repository and check again + repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch"); + assertEquals("a and b", execute(request)); + } /** * Method description diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java index 62c6069fba..c5af70e3a9 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java @@ -49,14 +49,48 @@ import static org.junit.Assert.*; //~--- JDK imports ------------------------------------------------------------ import java.io.IOException; +import org.eclipse.jgit.api.errors.GitAPIException; +import sonia.scm.repository.GitConstants; /** - * + * Unit tests for {@link GitLogCommand}. + * * @author Sebastian Sdorra */ public class GitLogCommandTest extends AbstractGitCommandTestBase { + /** + * Tests log command with the usage of a default branch. + * + * @throws IOException + * @throws GitAPIException + * @throws RepositoryException + */ + @Test + public void testGetDefaultBranch() throws IOException, GitAPIException, RepositoryException { + // without default branch, the repository head should be used + ChangesetPagingResult result = createCommand().getChangesets(new LogCommandRequest()); + + assertNotNull(result); + assertEquals(4, result.getTotal()); + assertEquals("fcd0ef1831e4002ac43ea539f4094334c79ea9ec", result.getChangesets().get(0).getId()); + assertEquals("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1", result.getChangesets().get(1).getId()); + assertEquals("592d797cd36432e591416e8b2b98154f4f163411", result.getChangesets().get(2).getId()); + assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getChangesets().get(3).getId()); + + // set default branch and fetch again + repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch"); + + result = createCommand().getChangesets(new LogCommandRequest()); + + assertNotNull(result); + assertEquals(3, result.getTotal()); + assertEquals("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4", result.getChangesets().get(0).getId()); + assertEquals("592d797cd36432e591416e8b2b98154f4f163411", result.getChangesets().get(1).getId()); + assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getChangesets().get(2).getId()); + } + /** * Method description * diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java index dc47c80291..28be6f7df0 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java @@ -51,7 +51,8 @@ import static org.junit.Assert.assertNotNull; import java.io.IOException; /** - * + * Unit tests for {@link OutgoingCommand}. + * * @author Sebastian Sdorra */ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitUserAgentProviderTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitUserAgentProviderTest.java new file mode 100644 index 0000000000..b16580c4a5 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitUserAgentProviderTest.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.web; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Strings; + +import org.junit.Test; + +import static org.junit.Assert.*; + +//~--- JDK imports ------------------------------------------------------------ + +import java.util.Locale; + +/** + * + * @author Sebastian Sdorra + */ +public class GitUserAgentProviderTest +{ + + /** + * Method description + * + */ + @Test + public void testParseUserAgent() + { + assertEquals(GitUserAgentProvider.GIT, parse("git/1.7.9.5")); + assertEquals(GitUserAgentProvider.MSYSGIT, parse("git/1.8.3.msysgit.0")); + assertNull(parse("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36")); + } + + /** + * Method description + * + * + * @param v + * + * @return + */ + private UserAgent parse(String v) + { + return provider.parseUserAgent( + Strings.nullToEmpty(v).toLowerCase(Locale.ENGLISH)); + } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + private final GitUserAgentProvider provider = new GitUserAgentProvider(); +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/rest/resources/HgConfigResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/rest/resources/HgConfigResource.java index 7435024e19..3ef39c163f 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/rest/resources/HgConfigResource.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/rest/resources/HgConfigResource.java @@ -43,7 +43,7 @@ import sonia.scm.installer.HgInstallerFactory; import sonia.scm.installer.HgPackage; import sonia.scm.installer.HgPackageReader; import sonia.scm.installer.HgPackages; -import sonia.scm.net.HttpClient; +import sonia.scm.net.ahc.AdvancedHttpClient; import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgRepositoryHandler; @@ -89,8 +89,8 @@ public class HgConfigResource * @param pkgReader */ @Inject - public HgConfigResource(HttpClient client, HgRepositoryHandler handler, - HgPackageReader pkgReader) + public HgConfigResource(AdvancedHttpClient client, + HgRepositoryHandler handler, HgPackageReader pkgReader) { this.client = client; this.handler = handler; @@ -158,7 +158,7 @@ public class HgConfigResource if (pkg != null) { if (HgInstallerFactory.createInstaller().installPackage(client, handler, - SCMContext.getContext().getBaseDirectory(), pkg)) + SCMContext.getContext().getBaseDirectory(), pkg)) { response = Response.noContent().build(); } @@ -262,7 +262,7 @@ public class HgConfigResource @POST @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) public Response setConfig(@Context UriInfo uriInfo, HgConfig config) - throws IOException + throws IOException { handler.setConfig(config); handler.storeConfig(); @@ -338,7 +338,7 @@ public class HgConfigResource //~--- fields --------------------------------------------------------------- /** Field description */ - private HttpClient client; + private AdvancedHttpClient client; /** Field description */ private HgRepositoryHandler handler; diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/AbstractHgInstaller.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/AbstractHgInstaller.java index 637825a407..bd5dfd7095 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/AbstractHgInstaller.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/AbstractHgInstaller.java @@ -35,7 +35,6 @@ package sonia.scm.installer; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.net.HttpClient; import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.util.IOUtil; @@ -44,6 +43,7 @@ import sonia.scm.util.IOUtil; import java.io.File; import java.io.IOException; +import sonia.scm.net.ahc.AdvancedHttpClient; /** * @@ -93,7 +93,7 @@ public abstract class AbstractHgInstaller implements HgInstaller * @return */ @Override - public boolean installPackage(HttpClient client, HgRepositoryHandler handler, + public boolean installPackage(AdvancedHttpClient client, HgRepositoryHandler handler, File baseDirectory, HgPackage pkg) { return new HgPackageInstaller(client, handler, baseDirectory, diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/HgInstaller.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/HgInstaller.java index 219ce5204e..52f18c1824 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/HgInstaller.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/HgInstaller.java @@ -35,7 +35,7 @@ package sonia.scm.installer; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.net.HttpClient; +import sonia.scm.net.ahc.AdvancedHttpClient; import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgRepositoryHandler; @@ -78,8 +78,8 @@ public interface HgInstaller * * @return */ - public boolean installPackage(HttpClient client, HgRepositoryHandler handler, - File baseDirectory, HgPackage pkg); + public boolean installPackage(AdvancedHttpClient client, + HgRepositoryHandler handler, File baseDirectory, HgPackage pkg); /** * Method description diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/HgPackageInstaller.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/HgPackageInstaller.java index 91bb6fce0c..7406505b5a 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/HgPackageInstaller.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/HgPackageInstaller.java @@ -40,10 +40,10 @@ import org.slf4j.LoggerFactory; import sonia.scm.SCMContext; import sonia.scm.io.ZipUnArchiver; -import sonia.scm.net.HttpClient; -import sonia.scm.repository.HgWindowsPackageFix; +import sonia.scm.net.ahc.AdvancedHttpClient; import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.repository.HgWindowsPackageFix; import sonia.scm.util.IOUtil; //~--- JDK imports ------------------------------------------------------------ @@ -80,8 +80,8 @@ public class HgPackageInstaller implements Runnable * @param baseDirectory * @param pkg */ - public HgPackageInstaller(HttpClient client, HgRepositoryHandler handler, - File baseDirectory, HgPackage pkg) + public HgPackageInstaller(AdvancedHttpClient client, + HgRepositoryHandler handler, File baseDirectory, HgPackage pkg) { this.client = client; this.handler = handler; @@ -155,7 +155,7 @@ public class HgPackageInstaller implements Runnable } // TODO error handling - input = client.get(pkg.getUrl()).getContent(); + input = client.get(pkg.getUrl()).request().contentAsStream(); output = new FileOutputStream(file); IOUtil.copy(input, output); } @@ -265,7 +265,7 @@ public class HgPackageInstaller implements Runnable private File baseDirectory; /** Field description */ - private HttpClient client; + private AdvancedHttpClient client; /** Field description */ private HgRepositoryHandler handler; diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/HgPackageReader.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/HgPackageReader.java index 67b988e336..d16f056889 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/HgPackageReader.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/HgPackageReader.java @@ -36,7 +36,6 @@ package sonia.scm.installer; //~--- non-JDK imports -------------------------------------------------------- import com.google.inject.Inject; -import com.google.inject.Provider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,22 +43,17 @@ import org.slf4j.LoggerFactory; import sonia.scm.PlatformType; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; -import sonia.scm.net.HttpClient; -import sonia.scm.net.HttpResponse; -import sonia.scm.util.IOUtil; +import sonia.scm.net.ahc.AdvancedHttpClient; import sonia.scm.util.SystemUtil; import sonia.scm.util.Util; //~--- JDK imports ------------------------------------------------------------ import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; import java.util.List; -import javax.xml.bind.JAXB; - /** * * @author Sebastian Sdorra @@ -85,14 +79,13 @@ public class HgPackageReader * * * @param cacheManager - * @param httpClientProvider + * @param httpClient */ @Inject - public HgPackageReader(CacheManager cacheManager, - Provider httpClientProvider) + public HgPackageReader(CacheManager cacheManager, AdvancedHttpClient httpClient) { - cache = cacheManager.getCache(CACHENAME); - this.httpClientProvider = httpClientProvider; + this.cache = cacheManager.getCache(CACHENAME); + this.httpClient = httpClient; } //~--- get methods ---------------------------------------------------------- @@ -167,7 +160,7 @@ public class HgPackageReader if (logger.isDebugEnabled()) { logger.debug("reject package {}, because of wrong platform {}", - pkg.getId(), pkg.getPlatform()); + pkg.getId(), pkg.getPlatform()); } add = false; @@ -181,7 +174,7 @@ public class HgPackageReader if (logger.isDebugEnabled()) { logger.debug("reject package {}, because of wrong arch {}", - pkg.getId(), pkg.getArch()); + pkg.getId(), pkg.getArch()); } add = false; @@ -218,23 +211,19 @@ public class HgPackageReader } HgPackages packages = null; - InputStream input = null; try { - HttpResponse response = httpClientProvider.get().get(PACKAGEURL); - - input = response.getContent(); - packages = JAXB.unmarshal(input, HgPackages.class); + //J- + packages = httpClient.get(PACKAGEURL) + .request() + .contentFromXml(HgPackages.class); + //J+ } catch (IOException ex) { logger.error("could not read HgPackages from ".concat(PACKAGEURL), ex); } - finally - { - IOUtil.close(input); - } if (packages == null) { @@ -251,5 +240,5 @@ public class HgPackageReader private final Cache cache; /** Field description */ - private final Provider httpClientProvider; + private final AdvancedHttpClient httpClient; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java index 5f3ab09734..aa0e4156ae 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java @@ -48,10 +48,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.config.ScmConfiguration; -import sonia.scm.config.ScmConfigurationChangedEvent; -import sonia.scm.net.HttpClient; -import sonia.scm.net.HttpRequest; -import sonia.scm.net.HttpResponse; + import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; @@ -62,6 +59,8 @@ import java.io.IOException; import java.util.UUID; import javax.servlet.http.HttpServletRequest; +import sonia.scm.config.ScmConfigurationChangedEvent; +import sonia.scm.net.ahc.AdvancedHttpClient; /** * @@ -88,16 +87,16 @@ public class HgHookManager * * @param configuration * @param httpServletRequestProvider - * @param httpClientProvider + * @param httpClient */ @Inject public HgHookManager(ScmConfiguration configuration, Provider httpServletRequestProvider, - Provider httpClientProvider) + AdvancedHttpClient httpClient) { this.configuration = configuration; this.httpServletRequestProvider = httpServletRequestProvider; - this.httpClientProvider = httpClientProvider; + this.httpClient = httpClient; } //~--- methods -------------------------------------------------------------- @@ -360,20 +359,16 @@ public class HgHookManager { url = url.concat("?ping=true"); - if (logger.isTraceEnabled()) - { - logger.trace("check hook url {}", url); - } - - HttpRequest request = new HttpRequest(url); - - request.setDisableCertificateValidation(true); - request.setDisableHostnameValidation(true); - request.setIgnoreProxySettings(true); - - HttpResponse response = httpClientProvider.get().get(request); - - result = response.getStatusCode() == 204; + logger.trace("check hook url {}", url); + //J- + int sc = httpClient.get(url) + .disableHostnameValidation(true) + .disableCertificateValidation(true) + .ignoreProxySettings(true) + .request() + .getStatus(); + //J+ + result = sc == 204; } catch (IOException ex) { @@ -398,7 +393,7 @@ public class HgHookManager private volatile String hookUrl; /** Field description */ - private Provider httpClientProvider; + private AdvancedHttpClient httpClient; /** Field description */ private Provider httpServletRequestProvider; diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java index 7711554d85..40987fa4da 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java @@ -89,7 +89,7 @@ public class HgRepositoryHandler /** Field description */ public static final String RESOURCE_VERSION = - "/sonia/scm/version/scm-hg-plugin"; + "sonia/scm/version/scm-hg-plugin"; /** Field description */ public static final String TYPE_DISPLAYNAME = "Mercurial"; diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/api/HgHookBranchProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/api/HgHookBranchProvider.java new file mode 100644 index 0000000000..0d134a9c8f --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/api/HgHookBranchProvider.java @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.repository.api; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; + +import sonia.scm.repository.Changeset; +import sonia.scm.repository.spi.HookChangesetProvider; +import sonia.scm.repository.spi.HookChangesetRequest; +import sonia.scm.repository.spi.javahg.AbstractChangesetCommand; +import sonia.scm.util.Util; + +//~--- JDK imports ------------------------------------------------------------ + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Mercurial hook branch provider implementation. + * + * @author Sebastian Sdorra + */ +public class HgHookBranchProvider implements HookBranchProvider +{ + + private static final Logger logger = LoggerFactory.getLogger(HgHookBranchProvider.class); + + private static final HookChangesetRequest REQUEST = + new HookChangesetRequest(); + + //~--- constructors --------------------------------------------------------- + + /** + * Constructs a new instance. + * + * + * @param changesetProvider changeset provider + */ + public HgHookBranchProvider(HookChangesetProvider changesetProvider) + { + this.changesetProvider = changesetProvider; + } + + //~--- get methods ---------------------------------------------------------- + + @Override + public List getCreatedOrModified() + { + if (createdOrModified == null) + { + collect(); + } + + return createdOrModified; + } + + @Override + public List getDeletedOrClosed() + { + if (deletedOrClosed == null) + { + collect(); + } + + return deletedOrClosed; + } + + //~--- methods -------------------------------------------------------------- + + private List appendBranches(Builder builder, Changeset c) + { + List branches = c.getBranches(); + + if (Util.isEmpty(branches)) + { + builder.add(AbstractChangesetCommand.BRANCH_DEFAULT); + } + else + { + builder.addAll(branches); + } + + return branches; + } + + private Iterable changesets() + { + return changesetProvider.handleRequest(REQUEST).getChangesets(); + } + + private void collect() + { + Builder createdOrModifiedBuilder = ImmutableList.builder(); + Builder deletedOrClosedBuilder = ImmutableList.builder(); + + logger.trace("collecting branches from hook changesets"); + + for (Changeset c : changesets()) + { + if (c.getProperty(AbstractChangesetCommand.PROPERTY_CLOSE) != null) + { + appendBranches(deletedOrClosedBuilder, c); + } + else + { + appendBranches(createdOrModifiedBuilder, c); + } + } + + createdOrModified = createdOrModifiedBuilder.build(); + deletedOrClosed = deletedOrClosedBuilder.build(); + } + + //~--- fields --------------------------------------------------------------- + + private final HookChangesetProvider changesetProvider; + + private List createdOrModified; + + private List deletedOrClosed; +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/api/HgHookTagProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/api/HgHookTagProvider.java new file mode 100644 index 0000000000..be89ebb415 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/api/HgHookTagProvider.java @@ -0,0 +1,107 @@ +/** + * * + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.repository.api; + +import com.google.common.collect.ImmutableList; +import java.util.Collections; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Tag; +import sonia.scm.repository.spi.HookChangesetProvider; +import sonia.scm.repository.spi.HookChangesetRequest; +import sonia.scm.repository.spi.HookChangesetResponse; + +/** + * Mercurial tag provider implementation. + * + * @author Sebastian Sdorra + * @since 1.50 + */ +public class HgHookTagProvider implements HookTagProvider { + + private static final Logger logger = LoggerFactory.getLogger(HgHookTagProvider.class); + + private static final HookChangesetRequest REQUEST = new HookChangesetRequest(); + + private final HookChangesetProvider changesetProvider; + + private List createdTags; + private final List deletedTags = Collections.emptyList(); + + /** + * Constructs a new instance. + * + * @param changesetProvider changeset provider + */ + public HgHookTagProvider(HookChangesetProvider changesetProvider) { + this.changesetProvider = changesetProvider; + } + + @Override + public List getCreatedTags() { + if (createdTags == null) { + collect(); + } + return createdTags; + } + + @Override + public List getDeletedTags() { + logger.warn("detecting deleted tags with mercurial is currently not supported"); + return deletedTags; + } + + private void collect() { + ImmutableList.Builder createdTagsBuilder = ImmutableList.builder(); + + logger.trace("collecting tags from hook changesets"); + HookChangesetResponse response = changesetProvider.handleRequest(REQUEST); + for ( Changeset c : response.getChangesets() ){ + appendTags(createdTagsBuilder, c); + } + + createdTags = createdTagsBuilder.build(); + } + + private void appendTags(ImmutableList.Builder tags, Changeset c){ + List tagNames = c.getTags(); + if (tagNames != null){ + for ( String tagName : tagNames ){ + logger.trace("found tag {} at changeset {}", tagName, c.getId()); + tags.add(new Tag(tagName, c.getId())); + } + } + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesCommand.java index d32560baac..64d544c5ed 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesCommand.java @@ -30,10 +30,13 @@ */ + package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- +import com.aragost.javahg.Changeset; + import com.google.common.base.Function; import com.google.common.collect.Lists; @@ -92,7 +95,15 @@ public class HgBranchesCommand extends AbstractCommand @Override public Branch apply(com.aragost.javahg.commands.Branch hgBranch) { - return new Branch(hgBranch.getName()); + String node = null; + Changeset changeset = hgBranch.getBranchTip(); + + if (changeset != null) + { + node = changeset.getNode(); + } + + return new Branch(hgBranch.getName(), node); } }); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgHookContextProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgHookContextProvider.java index 40d1ab5560..dede59796c 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgHookContextProvider.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgHookContextProvider.java @@ -36,7 +36,9 @@ package sonia.scm.repository.spi; import sonia.scm.repository.HgHookManager; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.RepositoryHookType; +import sonia.scm.repository.api.HgHookBranchProvider; import sonia.scm.repository.api.HgHookMessageProvider; +import sonia.scm.repository.api.HookBranchProvider; import sonia.scm.repository.api.HookFeature; import sonia.scm.repository.api.HookMessageProvider; @@ -44,29 +46,31 @@ import sonia.scm.repository.api.HookMessageProvider; import java.util.EnumSet; import java.util.Set; +import sonia.scm.repository.api.HgHookTagProvider; +import sonia.scm.repository.api.HookTagProvider; /** - * + * Mercurial implementation of {@link HookContextProvider}. + * * @author Sebastian Sdorra */ public class HgHookContextProvider extends HookContextProvider { - /** Field description */ private static final Set SUPPORTED_FEATURES = - EnumSet.of(HookFeature.CHANGESET_PROVIDER, HookFeature.MESSAGE_PROVIDER); + EnumSet.of(HookFeature.CHANGESET_PROVIDER, HookFeature.MESSAGE_PROVIDER, + HookFeature.BRANCH_PROVIDER, HookFeature.TAG_PROVIDER); //~--- constructors --------------------------------------------------------- /** - * Constructs ... + * Constructs a new instance. * - * - * @param handler - * @param repositoryName - * @param hookManager - * @param startRev - * @param type + * @param handler mercurial repository handler + * @param repositoryName name of changed repository + * @param hookManager mercurial hook manager + * @param startRev start revision + * @param type type of hook */ public HgHookContextProvider(HgRepositoryHandler handler, String repositoryName, HgHookManager hookManager, String startRev, @@ -78,24 +82,34 @@ public class HgHookContextProvider extends HookContextProvider //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @return - */ + @Override + public HookBranchProvider getBranchProvider() + { + if (hookBranchProvider == null) + { + hookBranchProvider = new HgHookBranchProvider(hookChangesetProvider); + } + + return hookBranchProvider; + } + + @Override + public HookTagProvider getTagProvider() + { + if (hookTagProvider == null) + { + hookTagProvider = new HgHookTagProvider(hookChangesetProvider); + } + + return hookTagProvider; + } + @Override public HookChangesetProvider getChangesetProvider() { return hookChangesetProvider; } - - /** - * Method description - * - * - * @return - */ + public HgHookMessageProvider getHgMessageProvider() { if (hgMessageProvider == null) @@ -106,12 +120,6 @@ public class HgHookContextProvider extends HookContextProvider return hgMessageProvider; } - /** - * Method description - * - * - * @return - */ @Override public Set getSupportedFeatures() { @@ -120,12 +128,6 @@ public class HgHookContextProvider extends HookContextProvider //~--- methods -------------------------------------------------------------- - /** - * Method description - * - * - * @return - */ @Override protected HookMessageProvider createMessageProvider() { @@ -134,9 +136,11 @@ public class HgHookContextProvider extends HookContextProvider //~--- fields --------------------------------------------------------------- - /** Field description */ + private final HgHookChangesetProvider hookChangesetProvider; + private HgHookMessageProvider hgMessageProvider; - /** Field description */ - private HgHookChangesetProvider hookChangesetProvider; + private HgHookBranchProvider hookBranchProvider; + + private HgHookTagProvider hookTagProvider; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/AbstractChangesetCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/AbstractChangesetCommand.java index b3e3c2f3a9..827f86ded1 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/AbstractChangesetCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/AbstractChangesetCommand.java @@ -30,6 +30,7 @@ */ + package sonia.scm.repository.spi.javahg; //~--- non-JDK imports -------------------------------------------------------- @@ -63,7 +64,7 @@ public abstract class AbstractChangesetCommand extends AbstractCommand { /** Field description */ - private static final String BRANCH_DEFAULT = "default"; + public static final String BRANCH_DEFAULT = "default"; /** * Character sequence that indicate the begin and end of the @@ -87,6 +88,9 @@ public abstract class AbstractChangesetCommand extends AbstractCommand private static final String NULL_ID = "0000000000000000000000000000000000000000"; + /** changeset property for closed branch */ + public static final String PROPERTY_CLOSE = "hg.close"; + /** changeset property for parent1 revision */ private static final String PROPERTY_PARENT1_REVISION = "hg.p1.rev"; @@ -114,12 +118,80 @@ public abstract class AbstractChangesetCommand extends AbstractCommand //~--- methods -------------------------------------------------------------- + /** + * Method description + * + * + * @param stream + * + * @return + */ + protected List loadRevisionsFromStream(HgInputStream stream) + { + List revisions = Lists.newArrayList(); - //~--- get methods ---------------------------------------------------------- + try + { + while (stream.peek() != -1) + { + int rev = stream.revisionUpTo(' '); + if (rev >= 0) + { + revisions.add(rev); + } + } + } + catch (IOException ex) + { + throw new RuntimeIOException(ex); + } - //~--- methods -------------------------------------------------------------- + return revisions; + } + + /** + * Method description + * + * + * @param in + * + * @return + */ + protected List readListFromStream(HgInputStream in) + { + List changesets = Lists.newArrayList(); + + try + { + boolean found = in.find(CHANGESET_PATTERN); + + if (found) + { + while (!in.match(CHANGESET_PATTERN)) + { + + Changeset cset = createFromInputStream(in); + + if (cset != null) + { + changesets.add(cset); + } + } + + Utils.consumeAll(in); + } + + // If the pattern is not found there is no changsets + } + catch (IOException e) + { + throw new RuntimeIOException(e); + } + + return changesets; + } /** * Method description @@ -168,7 +240,16 @@ public abstract class AbstractChangesetCommand extends AbstractCommand changeset.getParents().add(p2); } - in.mustMatch(' '); // skip space part of {parents} + // skip space part of {parents} + in.mustMatch(' '); + + // read extras + String extraLine = in.textUpTo('\n'); + + if (extraLine.contains("close=1")) + { + changeset.getProperties().put(PROPERTY_CLOSE, "true"); + } Modifications modifications = changeset.getModifications(); @@ -204,39 +285,6 @@ public abstract class AbstractChangesetCommand extends AbstractCommand return changeset; } - /** - * Method description - * - * - * @param stream - * - * @return - */ - protected List loadRevisionsFromStream(HgInputStream stream) - { - List revisions = Lists.newArrayList(); - - try - { - while (stream.peek() != -1) - { - int rev = stream.revisionUpTo(' '); - - if (rev >= 0) - { - revisions.add(rev); - } - } - - } - catch (IOException ex) - { - throw new RuntimeIOException(ex); - } - - return revisions; - } - /** * Method description * @@ -268,48 +316,6 @@ public abstract class AbstractChangesetCommand extends AbstractCommand return in.nextAsText(40); } - /** - * Method description - * - * - * @param in - * - * @return - */ - protected List readListFromStream(HgInputStream in) - { - List changesets = Lists.newArrayList(); - - try - { - boolean found = in.find(CHANGESET_PATTERN); - - if (found) - { - while (!in.match(CHANGESET_PATTERN)) - { - - Changeset cset = createFromInputStream(in); - - if (cset != null) - { - changesets.add(cset); - } - } - - Utils.consumeAll(in); - } - - // If the pattern is not found there is no changsets - } - catch (IOException e) - { - throw new RuntimeIOException(e); - } - - return changesets; - } - //~--- get methods ---------------------------------------------------------- /** diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java index 8a95e2ef97..be7118323c 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java @@ -35,6 +35,7 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.base.Stopwatch; import com.google.common.base.Strings; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -130,6 +131,7 @@ public class HgCGIServlet extends HttpServlet this.hookManager = hookManager; this.requestListenerUtil = requestListenerUtil; this.exceptionHandler = new HgCGIExceptionHandler(); + this.command = HgPythonScript.HGWEB.getFile(SCMContext.getContext()); } //~--- methods -------------------------------------------------------------- @@ -143,7 +145,7 @@ public class HgCGIServlet extends HttpServlet @Override public void init() throws ServletException { - command = HgPythonScript.HGWEB.getFile(SCMContext.getContext()); + super.init(); } @@ -184,7 +186,11 @@ public class HgCGIServlet extends HttpServlet { handleRequest(request, response, repository); } - catch (Exception ex) + catch (ServletException ex) + { + exceptionHandler.handleException(request, response, ex); + } + catch (IOException ex) { exceptionHandler.handleException(request, response, ex); } @@ -241,7 +247,9 @@ public class HgCGIServlet extends HttpServlet { if (requestListenerUtil.callListeners(request, response, repository)) { + Stopwatch sw = Stopwatch.createStarted(); process(request, response, repository); + logger.debug("mercurial request finished in {}", sw.stop()); } else if (logger.isDebugEnabled()) { @@ -358,26 +366,26 @@ public class HgCGIServlet extends HttpServlet //~--- fields --------------------------------------------------------------- /** Field description */ - private CGIExecutorFactory cgiExecutorFactory; + private final CGIExecutorFactory cgiExecutorFactory; /** Field description */ - private File command; + private final File command; /** Field description */ - private ScmConfiguration configuration; + private final ScmConfiguration configuration; /** Field description */ - private HgCGIExceptionHandler exceptionHandler; + private final HgCGIExceptionHandler exceptionHandler; /** Field description */ - private HgRepositoryHandler handler; + private final HgRepositoryHandler handler; /** Field description */ - private HgHookManager hookManager; + private final HgHookManager hookManager; /** Field description */ - private RepositoryProvider repositoryProvider; + private final RepositoryProvider repositoryProvider; /** Field description */ - private RepositoryRequestListenerUtil requestListenerUtil; + private final RepositoryRequestListenerUtil requestListenerUtil; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java index 9f753d2993..de2835dd8f 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java @@ -35,6 +35,7 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.collect.ImmutableSet; import com.google.inject.Inject; import sonia.scm.Priority; @@ -46,22 +47,27 @@ import sonia.scm.web.filter.ProviderPermissionFilter; //~--- JDK imports ------------------------------------------------------------ +import java.util.Set; + import javax.servlet.http.HttpServletRequest; /** - * + * Permission filter for mercurial repositories. + * * @author Sebastian Sdorra */ @Priority(Filters.PRIORITY_AUTHORIZATION) @WebElement(value = HgServletModule.MAPPING_HG) public class HgPermissionFilter extends ProviderPermissionFilter { + + private static final Set READ_METHODS = ImmutableSet.of("GET", "HEAD", "OPTIONS", "TRACE"); /** - * Constructs ... + * Constructs a new instance. * - * @param configuration - * @param repositoryProvider + * @param configuration scm configuration + * @param repositoryProvider repository provider */ @Inject public HgPermissionFilter(ScmConfiguration configuration, @@ -72,17 +78,9 @@ public class HgPermissionFilter extends ProviderPermissionFilter //~--- get methods ---------------------------------------------------------- - /** - * Method description - * - * - * @param request - * - * @return - */ @Override protected boolean isWriteRequest(HttpServletRequest request) { - return !request.getMethod().equalsIgnoreCase("GET"); + return !READ_METHODS.contains(request.getMethod()); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgUserAgentProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgUserAgentProvider.java new file mode 100644 index 0000000000..3229751f75 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgUserAgentProvider.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.web; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.annotations.VisibleForTesting; + +import sonia.scm.plugin.Extension; + +//~--- JDK imports ------------------------------------------------------------ + +import java.nio.charset.Charset; + +/** + * + * @author Sebastian Sdorra + * @since 1.45 + */ +@Extension +public class HgUserAgentProvider implements UserAgentProvider +{ + + /** mercurial seems to use system encoding */ + @VisibleForTesting + static UserAgent HG = UserAgent.builder("Mercurial").browser( + false).basicAuthenticationCharset( + Charset.defaultCharset()).build(); + + /** Field description */ + private static final String PREFIX = "mercurial"; + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @param userAgentString + * + * @return + */ + @Override + public UserAgent parseUserAgent(String userAgentString) + { + UserAgent ua = null; + + if (userAgentString.startsWith(PREFIX)) + { + ua = HG; + } + + return ua; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/styles/changesets-eager.style b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/styles/changesets-eager.style index 93b0b04e27..5c462fc459 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/styles/changesets-eager.style +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/styles/changesets-eager.style @@ -1,5 +1,5 @@ header = "%{pattern}" -changeset = "{rev}:{node}{author}\n{date|hgdate}\n{branch}\n{parents}{tags}{file_adds}{file_mods}{file_dels}\n{desc}\0" +changeset = "{rev}:{node}{author}\n{date|hgdate}\n{branch}\n{parents}{join(extras,',')}\n{tags}{file_adds}{file_mods}{file_dels}\n{desc}\0" tag = "t {tag}\n" file_add = "a {file_add}\n" file_mod = "m {file_mod}\n" diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/api/HgHookTagProviderTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/api/HgHookTagProviderTest.java new file mode 100644 index 0000000000..6d78d0eb3c --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/api/HgHookTagProviderTest.java @@ -0,0 +1,104 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.repository.api; + +import com.google.common.collect.Lists; +import java.util.List; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import static org.hamcrest.Matchers.*; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Tag; +import sonia.scm.repository.spi.HookChangesetProvider; +import sonia.scm.repository.spi.HookChangesetRequest; +import sonia.scm.repository.spi.HookChangesetResponse; + +/** + * Unit tests for {@link HgHookTagProvider}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class HgHookTagProviderTest { + + @Mock + private HookChangesetProvider changesetProvider; + + @InjectMocks + private HgHookTagProvider tagProvider; + + /** + * Tests {@link HgHookTagProvider#getDeletedTags()}. + */ + @Test + public void testGetDeletedTags() { + prepareChangesets(new Changeset("1", Long.MIN_VALUE, null)); + assertThat(tagProvider.getDeletedTags(), empty()); + } + + /** + * Tests {@link HgHookTagProvider#getCreatedTags()}. + */ + @Test + public void testGetCreatedTags(){ + Changeset c1 = new Changeset("1", Long.MIN_VALUE, null); + c1.getTags().add("1.0.0"); + Changeset c2 = new Changeset("2", Long.MIN_VALUE, null); + c2.getTags().add("2.0.0"); + Changeset c3 = new Changeset("3", Long.MIN_VALUE, null); + prepareChangesets(c1, c2, c3); + + List tags = tagProvider.getCreatedTags(); + assertNotNull(tags); + assertEquals(2, tags.size()); + + Tag t1 = tags.get(0); + assertEquals("1", t1.getRevision()); + assertEquals("1.0.0", t1.getName()); + + Tag t2 = tags.get(1); + assertEquals("2", t2.getRevision()); + assertEquals("2.0.0", t2.getName()); + } + + private void prepareChangesets(Changeset... changesets){ + List list = Lists.newArrayList(changesets); + HookChangesetResponse response = new HookChangesetResponse(list); + when(changesetProvider.handleRequest(Mockito.any(HookChangesetRequest.class))).thenReturn(response); + } +} \ No newline at end of file diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgAddCommand.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgAddCommand.java new file mode 100644 index 0000000000..aec21d87ad --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgAddCommand.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.repository.client.spi; + +import com.aragost.javahg.Repository; +import java.io.IOException; + +/** + * Mercurial implementation of the {@link AddCommand}. + * + * @author Sebastian Sdorra + */ +public final class HgAddCommand implements AddCommand +{ + + private final Repository repository; + + HgAddCommand(Repository repository) + { + this.repository = repository; + } + + @Override + public void add(String path) throws IOException + { + com.aragost.javahg.commands.AddCommand.on(repository).execute(path); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgBranchCommand.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgBranchCommand.java new file mode 100644 index 0000000000..74d1b1f742 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgBranchCommand.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.repository.client.spi; + +import com.aragost.javahg.Repository; +import java.io.IOException; +import sonia.scm.repository.Branch; + +/** + * Mercurial implementation of the {@link BranchCommand}. + * + * @author Sebastian Sdorra + */ +public class HgBranchCommand implements BranchCommand +{ + + private final Repository repository; + + HgBranchCommand(Repository repository) + { + this.repository = repository; + } + + @Override + public Branch branch(String name) throws IOException + { + com.aragost.javahg.commands.BranchCommand.on(repository).set(name); + return new Branch(name, repository.tip().getNode()); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgCommitCommand.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgCommitCommand.java new file mode 100644 index 0000000000..aa611a8ba4 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgCommitCommand.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.repository.client.spi; + +import com.aragost.javahg.Repository; +import com.google.common.collect.Lists; +import java.io.IOException; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Modifications; +import sonia.scm.repository.Person; + +/** + * Mercurial implementation of the {@link CommitCommand}. + * + * @author Sebastian Sdorra + */ +public class HgCommitCommand implements CommitCommand +{ + + private final Repository repository; + + HgCommitCommand(Repository repository) + { + this.repository = repository; + } + + @Override + public Changeset commit(CommitRequest request) throws IOException + { + com.aragost.javahg.Changeset c = com.aragost.javahg.commands.CommitCommand + .on(repository) + .user(request.getAuthor().toString()) + .message(request.getMessage()) + .execute(); + + Changeset changeset = new Changeset( + c.getNode(), + c.getTimestamp().getDate().getTime(), + Person.toPerson(c.getUser()), + c.getMessage() + ); + + changeset.setBranches(Lists.newArrayList(c.getBranch())); + changeset.setTags(c.tags()); + changeset.setModifications( + new Modifications(c.getAddedFiles(), c.getModifiedFiles(), c.getDeletedFiles()) + ); + return changeset; + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgPushCommand.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgPushCommand.java new file mode 100644 index 0000000000..47144dad35 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgPushCommand.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.repository.client.spi; + +import com.aragost.javahg.Repository; +import java.io.IOException; + +/** + * Mercurial implementation of the {@link PushCommand}. + * + * @author Sebastian Sdorra + */ +public class HgPushCommand implements PushCommand +{ + + private final Repository repository; + private final String url; + + HgPushCommand(Repository repository, String url) + { + this.repository = repository; + this.url = url; + } + + @Override + public void push() throws IOException + { + com.aragost.javahg.commands.PushCommand cmd = com.aragost.javahg.commands.PushCommand.on(repository); + cmd.cmdAppend("--new-branch"); + cmd.execute(url); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgRemoveCommand.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgRemoveCommand.java new file mode 100644 index 0000000000..88d6ef04a8 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgRemoveCommand.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.repository.client.spi; + +import com.aragost.javahg.Repository; +import java.io.IOException; + +/** + * Mercurial implementation of the {@link RemoveCommand}. + * + * @author Sebastian Sdorra + */ +public class HgRemoveCommand implements RemoveCommand +{ + + private final Repository repository; + + HgRemoveCommand(Repository repository) + { + this.repository = repository; + } + + @Override + public void remove(String path) throws IOException + { + com.aragost.javahg.commands.RemoveCommand.on(repository).execute(path); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgRepositoryClientFactoryProvider.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgRepositoryClientFactoryProvider.java new file mode 100644 index 0000000000..ad83abc97b --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgRepositoryClientFactoryProvider.java @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.repository.client.spi; + +import com.aragost.javahg.Repository; +import com.aragost.javahg.RepositoryConfiguration; +import com.aragost.javahg.commands.PullCommand; +import com.google.common.base.Strings; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import sonia.scm.io.INIConfiguration; +import sonia.scm.io.INIConfigurationWriter; +import sonia.scm.io.INISection; +import sonia.scm.util.IOUtil; + +/** + * Mercurial implementation of the {@link RepositoryClientFactoryProvider}. + * + * @author Sebastian Sdorra + */ +public class HgRepositoryClientFactoryProvider implements RepositoryClientFactoryProvider +{ + + private static final String TYPE = "hg"; + + @Override + public RepositoryClientProvider create(File main, File workingCopy) throws IOException + { + return create(main.toURI().toString(), null, null, workingCopy); + } + + @Override + public RepositoryClientProvider create(String url, String username, String password, File workingCopy) + throws IOException + { + RepositoryConfiguration configuration = new RepositoryConfiguration(); + String binary = IOUtil.search("hg"); + if (Strings.isNullOrEmpty(binary)){ + throw new IOException("could not find mercurial binary (hg)"); + } + configuration.setHgBin(binary); + + File hgrc = null; + if (!Strings.isNullOrEmpty(username)) + { + hgrc = createHgrc(url, username, password); + configuration.setHgrcPath(hgrc.getAbsolutePath()); + } + + Repository repository = Repository.create(configuration, workingCopy); + PullCommand.on(repository).execute(url); + + return new HgRepositoryClientProvider(repository, hgrc, url); + } + + private File createHgrc(String url, String username, String password) throws IOException + { + URL repositoryUrl = new URL(url); + + INIConfiguration hgConfig = new INIConfiguration(); + + INISection pathSection = new INISection("paths"); + pathSection.setParameter(repositoryUrl.getHost(), url); + hgConfig.addSection(pathSection); + + String prefix = repositoryUrl.getHost() + "."; + + INISection authSection = new INISection("auth"); + authSection.setParameter( + prefix + "prefix", + repositoryUrl.getHost() + ":" + repositoryUrl.getPort() + repositoryUrl.getPath() + ); + authSection.setParameter(prefix + "schemes", repositoryUrl.getProtocol()); + authSection.setParameter(prefix + "username", username); + if (!Strings.isNullOrEmpty(password)) + { + authSection.setParameter(prefix + "password", password); + } + hgConfig.addSection(authSection); + + File hgrc = File.createTempFile("hgrc", ".temp"); + INIConfigurationWriter writer = new INIConfigurationWriter(); + writer.write(hgConfig, hgrc); + return hgrc; + } + + @Override + public String getType() + { + return TYPE; + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgRepositoryClientProvider.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgRepositoryClientProvider.java new file mode 100644 index 0000000000..353e6a5b46 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgRepositoryClientProvider.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.repository.client.spi; + +import com.aragost.javahg.Repository; +import com.google.common.collect.ImmutableSet; +import java.io.File; +import java.io.IOException; +import java.util.Set; +import sonia.scm.repository.client.api.ClientCommand; + +/** + * Mercurial implementation of the {@link RepositoryClientProvider}. + * + * @author Sebastian Sdorra + */ +public class HgRepositoryClientProvider extends RepositoryClientProvider +{ + + private static final Set SUPPORTED_COMMANDS = ImmutableSet.of( + ClientCommand.ADD, ClientCommand.REMOVE, ClientCommand.COMMIT, + ClientCommand.TAG, ClientCommand.BANCH, ClientCommand.PUSH + ); + + private final Repository repository; + private final File hgrc; + private final String url; + + HgRepositoryClientProvider(Repository repository, File hgrc, String url) + { + this.repository = repository; + this.hgrc = hgrc; + this.url = url; + } + + @Override + public Set getSupportedClientCommands() + { + return SUPPORTED_COMMANDS; + } + + @Override + public AddCommand getAddCommand() + { + return new HgAddCommand(repository); + } + + @Override + public RemoveCommand getRemoveCommand() + { + return new HgRemoveCommand(repository); + } + + @Override + public CommitCommand getCommitCommand() + { + return new HgCommitCommand(repository); + } + + @Override + public TagCommand getTagCommand() + { + return new HgTagCommand(repository); + } + + @Override + public BranchCommand getBranchCommand() + { + return new HgBranchCommand(repository); + } + + @Override + public PushCommand getPushCommand() + { + return new HgPushCommand(repository, url); + } + + @Override + public void close() throws IOException + { + if ( hgrc != null && hgrc.exists() && ! hgrc.delete() ){ + throw new IOException("failed to remove hgrc file ".concat(hgrc.getPath())); + } + repository.close(); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgTagCommand.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgTagCommand.java new file mode 100644 index 0000000000..3aa448bfca --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgTagCommand.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.repository.client.spi; + +import com.aragost.javahg.Repository; +import com.google.common.base.Strings; +import java.io.IOException; +import sonia.scm.repository.Tag; + +/** + * Mercurial implementation of the {@link TagCommand}. + * + * @author Sebastian Sdorra + */ +public class HgTagCommand implements TagCommand +{ + + private final Repository repository; + + HgTagCommand(Repository repository) + { + this.repository = repository; + } + + @Override + public Tag tag(TagRequest request) throws IOException + { + String rev = request.getRevision(); + if ( Strings.isNullOrEmpty(rev) ){ + rev = repository.tip().getNode(); + } + com.aragost.javahg.commands.TagCommand.on(repository).rev(rev).execute(request.getName()); + return new Tag(request.getName(), rev); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java new file mode 100644 index 0000000000..01e01cf302 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.web; + +import javax.servlet.http.HttpServletRequest; +import org.junit.Test; +import static org.junit.Assert.*; + +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import static org.mockito.Mockito.*; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.repository.RepositoryProvider; + +/** + * Unit tests for {@link HgPermissionFilter}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class HgPermissionFilterTest { + + @Mock + private HttpServletRequest request; + + @Mock + private ScmConfiguration configuration; + + @Mock + private RepositoryProvider repositoryProvider; + + @InjectMocks + private HgPermissionFilter filter; + + /** + * Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)}. + */ + @Test + public void testIsWriteRequest() { + // read methods + assertFalse(isWriteRequest("GET")); + assertFalse(isWriteRequest("HEAD")); + assertFalse(isWriteRequest("TRACE")); + assertFalse(isWriteRequest("OPTIONS")); + + // write methods + assertTrue(isWriteRequest("POST")); + assertTrue(isWriteRequest("PUT")); + assertTrue(isWriteRequest("DELETE")); + assertTrue(isWriteRequest("KA")); + } + + private boolean isWriteRequest(String method) { + when(request.getMethod()).thenReturn(method); + return filter.isWriteRequest(request); + } +} \ No newline at end of file diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgUserAgentProviderTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgUserAgentProviderTest.java new file mode 100644 index 0000000000..0a3e1cd03c --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgUserAgentProviderTest.java @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.web; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Strings; + +import org.junit.Test; + +import static org.junit.Assert.*; + +//~--- JDK imports ------------------------------------------------------------ + +import java.util.Locale; + +/** + * + * @author Sebastian Sdorra + */ +public class HgUserAgentProviderTest +{ + + /** Field description */ + private static final String UA_1 = "mercurial/proto-1.0"; + + /** Field description */ + private static final String UA_2 = + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36"; + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + */ + @Test + public void testParseUserAgent() + { + assertEquals(HgUserAgentProvider.HG, parse(UA_1)); + assertNull(parse(UA_2)); + } + + /** + * Method description + * + * + * @param v + * + * @return + */ + private UserAgent parse(String v) + { + return provider.parseUserAgent( + Strings.nullToEmpty(v).toLowerCase(Locale.ENGLISH)); + } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + private final HgUserAgentProvider provider = new HgUserAgentProvider(); +} diff --git a/scm-plugins/scm-hg-plugin/src/test/resources/META-INF/services/sonia.scm.repository.client.spi.RepositoryClientFactoryProvider b/scm-plugins/scm-hg-plugin/src/test/resources/META-INF/services/sonia.scm.repository.client.spi.RepositoryClientFactoryProvider new file mode 100644 index 0000000000..ebb411a4fe --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/resources/META-INF/services/sonia.scm.repository.client.spi.RepositoryClientFactoryProvider @@ -0,0 +1 @@ +sonia.scm.repository.client.spi.HgRepositoryClientFactoryProvider \ No newline at end of file diff --git a/scm-plugins/scm-svn-plugin/pom.xml b/scm-plugins/scm-svn-plugin/pom.xml index 5f2a4a6d8d..35dd4fcda4 100644 --- a/scm-plugins/scm-svn-plugin/pom.xml +++ b/scm-plugins/scm-svn-plugin/pom.xml @@ -83,12 +83,6 @@ scm-manager release repository http://maven.scm-manager.org/nexus/content/groups/public - - - maven.tmatesoft.com - tmatesoft release repository - http://maven.tmatesoft.com/content/repositories/releases/ - diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java index b3bb8a2721..dcadd5c1c6 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java @@ -78,7 +78,7 @@ public class SvnRepositoryHandler /** Field description */ public static final String RESOURCE_VERSION = - "/sonia/scm/version/scm-svn-plugin"; + "sonia/scm/version/scm-svn-plugin"; /** Field description */ public static final String TYPE_DISPLAYNAME = "Subversion"; diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnBasicAuthenticationFilter.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnBasicAuthenticationFilter.java index 8ed93f05ae..1d9581f7c1 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnBasicAuthenticationFilter.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnBasicAuthenticationFilter.java @@ -69,9 +69,7 @@ public class SvnBasicAuthenticationFilter extends AuthenticationFilter * @param webTokenGenerators */ @Inject - public SvnBasicAuthenticationFilter(ScmConfiguration configuration, - Set webTokenGenerators) - { + public SvnBasicAuthenticationFilter(ScmConfiguration configuration, Set webTokenGenerators) { super(configuration, webTokenGenerators); } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServlet.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServlet.java index 701de1d44b..db22857595 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServlet.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServlet.java @@ -284,21 +284,21 @@ public class SvnDAVServlet extends DAVServlet //~--- fields ------------------------------------------------------------- /** Field description */ - private RepositoryProvider repositoryProvider; + private final RepositoryProvider repositoryProvider; } //~--- fields --------------------------------------------------------------- /** Field description */ - private SvnCollectionRenderer collectionRenderer; + private final SvnCollectionRenderer collectionRenderer; /** Field description */ - private SvnRepositoryHandler handler; + private final SvnRepositoryHandler handler; /** Field description */ - private RepositoryProvider repositoryProvider; + private final RepositoryProvider repositoryProvider; /** Field description */ - private RepositoryRequestListenerUtil repositoryRequestListenerUtil; + private final RepositoryRequestListenerUtil repositoryRequestListenerUtil; } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnUserAgentProvider.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnUserAgentProvider.java new file mode 100644 index 0000000000..e853673e30 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnUserAgentProvider.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.web; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Charsets; + +import sonia.scm.plugin.Extension; + +/** + * + * @author Sebastian Sdorra + * @since 1.45 + */ +@Extension +public final class SvnUserAgentProvider implements UserAgentProvider +{ + + /** ua prefix */ + private static final String PREFIX = "svn"; + + /** ua suffix */ + private static final String SUFFIX = "tortoisesvn"; + + /** TortoiseSVN */ + @VisibleForTesting + static final UserAgent TORTOISE_SVN = + UserAgent.builder("TortoiseSVN").browser(false) + .basicAuthenticationCharset(Charsets.UTF_8).build(); + + /** Subversion cli client */ + @VisibleForTesting + static final UserAgent SVN = + UserAgent.builder("Subversion").browser(false) + .basicAuthenticationCharset(Charsets.UTF_8).build(); + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @param userAgentString + * + * @return + */ + @Override + public UserAgent parseUserAgent(String userAgentString) + { + UserAgent ua = null; + + if (userAgentString.startsWith(PREFIX)) + { + if (userAgentString.contains(SUFFIX)) + { + ua = TORTOISE_SVN; + } + else + { + ua = SVN; + } + } + + return ua; + } +} diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/web/SvnUserAgentProviderTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/web/SvnUserAgentProviderTest.java new file mode 100644 index 0000000000..2604079e64 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/web/SvnUserAgentProviderTest.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.web; + +//~--- non-JDK imports -------------------------------------------------------- + +import org.junit.Test; + + +import static org.junit.Assert.*; + +//~--- JDK imports ------------------------------------------------------------ + +import java.util.Locale; + +/** + * + * @author Sebastian Sdorra + */ +public class SvnUserAgentProviderTest +{ + + /** Field description */ + private static final String UA_1 = + "SVN/1.8.8 (x64-microsoft-windows) serf/1.3.4 TortoiseSVN-1.8.6.25419"; + + /** Field description */ + private static final String UA_2 = "SVN/1.5.4 (r33841) neon/0.28.3"; + + /** Field description */ + private static final String UA_3 = "SVN/1.6.3 (r38063) neon/0.28.4"; + + /** Field description */ + private static final String UA_4 = + "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0;Google Wireless Transcoder;)"; + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + */ + @Test + public void testParseUserAgent() + { + assertEquals(SvnUserAgentProvider.TORTOISE_SVN, parse(UA_1)); + assertEquals(SvnUserAgentProvider.SVN, parse(UA_2)); + assertEquals(SvnUserAgentProvider.SVN, parse(UA_3)); + assertNull(parse(UA_4)); + } + + /** + * Method description + * + * + * @param ua + * + * @return + */ + private UserAgent parse(String ua) + { + return suap.parseUserAgent(ua.toLowerCase(Locale.ENGLISH)); + } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + private final SvnUserAgentProvider suap = new SvnUserAgentProvider(); +} diff --git a/scm-server/pom.xml b/scm-server/pom.xml index 272127cda7..687658bff5 100644 --- a/scm-server/pom.xml +++ b/scm-server/pom.xml @@ -69,7 +69,7 @@ scm-server - ${commons.daemon.version} + ${commons.daemon.native.version} sonia.scm.server.ScmServerDaemon commons-daemon @@ -172,7 +172,7 @@ com.github.sdorra nativepkg-maven-plugin - 1.1.0 + 1.1.3 @@ -243,13 +243,13 @@ /etc/default/scm-server ${project.basedir}/src/main/nativepkg/default - 0744 + 0644 true /etc/init.d/scm-server ${project.basedir}/src/main/nativepkg/init-script - 0744 + 0755 @@ -307,6 +307,7 @@ 1.0.15 + 1.0.15.1 ${project.build.directory}/appassembler/commons-daemon/scm-server diff --git a/scm-server/src/main/nativepkg/create-user b/scm-server/src/main/nativepkg/create-user index 82398c513c..3ffe4972ce 100644 --- a/scm-server/src/main/nativepkg/create-user +++ b/scm-server/src/main/nativepkg/create-user @@ -1,5 +1,6 @@ +#!/bin/sh getent group scm >/dev/null || groupadd -r scm getent passwd scm >/dev/null || \ useradd -r -g scm -M -s /sbin/nologin \ -c "user for the scm-server process" scm -exit 0 \ No newline at end of file +exit 0 diff --git a/scm-server/src/main/nativepkg/default b/scm-server/src/main/nativepkg/default index f2675e1673..5e04061e0c 100644 --- a/scm-server/src/main/nativepkg/default +++ b/scm-server/src/main/nativepkg/default @@ -29,6 +29,9 @@ # http://bitbucket.org/sdorra/scm-manager # +# scm-server host interface +HOST=0.0.0.0 + # scm-server port PORT=8080 @@ -51,4 +54,4 @@ LOGDIR=/var/log/scm # EXTRA_JVM_ARGUMENTS="$EXTRA_JVM_ARGUMENTS -Xms1g -Xmx1g" # pass extra jvm arguments -EXTRA_JVM_ARGUMENTS="$EXTRA_JVM_ARGUMENTS -Djetty.port=$PORT" +EXTRA_JVM_ARGUMENTS="$EXTRA_JVM_ARGUMENTS -Djetty.host=$HOST -Djetty.port=$PORT" diff --git a/scm-server/src/main/nativepkg/init-script b/scm-server/src/main/nativepkg/init-script index 5e6cac53f3..db2d995147 100644 --- a/scm-server/src/main/nativepkg/init-script +++ b/scm-server/src/main/nativepkg/init-script @@ -62,20 +62,33 @@ appname=ScmServerDaemon # See how we were called. start() { - /opt/scm-server/bin/scm-server start + if [ $(ps aux | grep java | grep ${appname} | wc -l) = 0 ] + then + echo "SCM-Server will now be started" + /opt/scm-server/bin/scm-server start + else + echo "SCM-Server already running" + status + fi } stop() { - if [ ! status = 0 ] + if [ ! $(ps aux | grep java | grep ${appname} | wc -l) = 0 ] then + echo "SCM-Server will now be stopped" /opt/scm-server/bin/scm-server stop else - echo "SCM-Server is not running" + echo "SCM-Server is not running" fi } status() { - ps auxwww | grep java | grep ${appname} || echo "SCM-Server is not running" + if [ "$(ps auxwww | grep java | grep ${appname} | awk '{ print $1 " PID:" $2 }')" ]; then + echo "SCM-Server is running" + ps auxwww | grep java | grep ${appname} | awk '{ print " PID: " $2 }' + else + echo "SCM-Server is not running" + fi } restart() { @@ -84,6 +97,7 @@ restart() { STAT=$( ps auxwww | grep java | grep ${appname} | wc -l ) while [ $STAT -ne 0 ] do + echo -n . sleep 3 if [ $SECONDS -gt 300 ] then @@ -92,7 +106,9 @@ restart() { fi STAT=$( ps auxwww | grep java | grep ${appname} | wc -l ) done + status start + status } # See how we were called. @@ -115,4 +131,4 @@ case "$1" in exit 1 esac -exit $RETVAL \ No newline at end of file +exit $RETVAL diff --git a/scm-test/pom.xml b/scm-test/pom.xml index 6c9f5f598e..5286fb1419 100644 --- a/scm-test/pom.xml +++ b/scm-test/pom.xml @@ -79,12 +79,6 @@ jgit release repository http://download.eclipse.org/jgit/maven
- - - maven.tmatesoft.com - tmatesoft release repository - http://maven.tmatesoft.com/content/repositories/releases/ - diff --git a/scm-test/src/main/java/sonia/scm/repository/client/GitRepositoryClient.java b/scm-test/src/main/java/sonia/scm/repository/client/GitRepositoryClient.java index 9f73fcb21a..9eba91d0e0 100644 --- a/scm-test/src/main/java/sonia/scm/repository/client/GitRepositoryClient.java +++ b/scm-test/src/main/java/sonia/scm/repository/client/GitRepositoryClient.java @@ -476,19 +476,10 @@ public class GitRepositoryClient extends AbstractRepositoryClient private RevCommit parseCommit(Ref branch) throws MissingObjectException, IncorrectObjectTypeException, IOException { - final RevWalk rw = new RevWalk(repository); - final RevCommit commit; - - try + try (RevWalk rw = new RevWalk(repository)) { - commit = rw.parseCommit(branch.getObjectId()); + return rw.parseCommit(branch.getObjectId()); } - finally - { - rw.release(); - } - - return commit; } /** diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index 7fcbfba541..a43548368f 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -180,19 +180,25 @@ commons-codec 1.9 - - - asm - asm - 3.3.1 - - + com.google.guava guava ${guava.version} + + org.quartz-scheduler + quartz + ${quartz.version} + + + c3p0 + c3p0 + + + + @@ -243,6 +249,44 @@ + + org.seleniumhq.selenium + selenium-java + ${selenium.version} + test + + + + org.seleniumhq.selenium + selenium-firefox-driver + ${selenium.version} + test + + + + org.seleniumhq.selenium + htmlunit-driver + 2.21 + test + + + + com.sun.jersey + jersey-client + ${jersey.version} + test + + + + com.sun.jersey.contribs + jersey-apache-client + ${jersey.version} + test + + + + + com.github.sdorra shiro-unit @@ -258,6 +302,13 @@ test + + sonia.scm.plugins + scm-git-plugin + 2.0.0-SNAPSHOT + test + + sonia.scm.plugins scm-hg-plugin @@ -266,6 +317,13 @@ test + + sonia.scm.plugins + scm-hg-plugin + 2.0.0-SNAPSHOT + test + + sonia.scm.plugins scm-svn-plugin @@ -275,30 +333,9 @@ - org.seleniumhq.selenium - selenium-java - ${selenium.version} - test - - - - org.seleniumhq.selenium - selenium-firefox-driver - ${selenium.version} - test - - - - com.sun.jersey - jersey-client - ${jersey.version} - test - - - - com.sun.jersey.contribs - jersey-apache-client - ${jersey.version} + sonia.scm.plugins + scm-svn-plugin + 2.0.0-SNAPSHOT test @@ -472,7 +509,7 @@ org.eclipse.jetty jetty-maven-plugin - ${jetty.version} + ${jetty.maven.version} 8005 STOP @@ -507,14 +544,15 @@ DEVELOPMENT target/scm-it default - 2.28.0 - 1.28 + 2.53.1 + 1.31 1.0 0.8.17 Tomcat + release @@ -597,7 +635,7 @@ org.eclipse.jetty jetty-maven-plugin - ${jetty.version} + ${jetty.maven.version} 8085 STOP @@ -606,6 +644,10 @@ scm.home target/scm-it + + scm.stage + ${scm.stage} + ${project.basedir}/src/main/conf/jetty.xml 0 @@ -637,6 +679,17 @@ selenium + + + + org.apache.httpcomponents + httpclient + 4.3.2 + test + + + + @@ -668,7 +721,7 @@ org.eclipse.jetty jetty-maven-plugin - ${jetty.version} + ${jetty.maven.version} 8082 diff --git a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java index f8bc248199..5824c2f0d6 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java @@ -65,7 +65,9 @@ import java.util.Set; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; +import sonia.scm.debug.DebugModule; import sonia.scm.filter.WebElementModule; +import sonia.scm.schedule.Scheduler; /** * @@ -108,6 +110,8 @@ public class ScmContextListener extends GuiceServletContextListener { if ((globalInjector != null) &&!startupError) { + // close Scheduler + IOUtil.close(globalInjector.getInstance(Scheduler.class)); // close RepositoryManager IOUtil.close(globalInjector.getInstance(RepositoryManager.class)); @@ -259,6 +263,10 @@ public class ScmContextListener extends GuiceServletContextListener ); appendModules(pluginLoader.getExtensionProcessor(), moduleList); moduleList.addAll(overrides.getModules()); + + if (SCMContext.getContext().getStage() == Stage.DEVELOPMENT){ + moduleList.add(new DebugModule()); + } SCMContextProvider ctx = SCMContext.getContext(); diff --git a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java index edf2eabdd6..57cd1c62c5 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java @@ -128,6 +128,18 @@ import java.util.Map; import javax.servlet.ServletContext; import sonia.scm.store.ConfigurationStoreFactory; +import javax.net.ssl.SSLContext; +import sonia.scm.net.SSLContextProvider; +import sonia.scm.net.ahc.AdvancedHttpClient; +import sonia.scm.net.ahc.ContentTransformer; +import sonia.scm.net.ahc.DefaultAdvancedHttpClient; +import sonia.scm.net.ahc.JsonContentTransformer; +import sonia.scm.net.ahc.XmlContentTransformer; +import sonia.scm.schedule.QuartzScheduler; +import sonia.scm.schedule.Scheduler; +import sonia.scm.security.XsrfProtectionFilter; +import sonia.scm.web.UserAgentParser; + /** * * @author Sebastian Sdorra @@ -249,6 +261,9 @@ public class ScmServletModule extends JerseyServletModule bind(PluginLoader.class).toInstance(pluginLoader); bind(PluginManager.class, DefaultPluginManager.class); + // bind scheduler + bind(Scheduler.class).to(QuartzScheduler.class); + // note CipherUtil uses an other generator bind(KeyGenerator.class).to(DefaultKeyGenerator.class); bind(CipherHandler.class).toInstance(cu.getCipherHandler()); @@ -280,8 +295,18 @@ public class ScmServletModule extends JerseyServletModule GroupManagerProvider.class); bind(CGIExecutorFactory.class, DefaultCGIExecutorFactory.class); + // bind sslcontext provider + bind(SSLContext.class).toProvider(SSLContextProvider.class); + // bind httpclient bind(HttpClient.class, URLHttpClient.class); + + // bind ahc + Multibinder transformers = + Multibinder.newSetBinder(binder(), ContentTransformer.class); + transformers.addBinding().to(XmlContentTransformer.class); + transformers.addBinding().to(JsonContentTransformer.class); + bind(AdvancedHttpClient.class).to(DefaultAdvancedHttpClient.class); // bind resourcemanager if (context.getStage() == Stage.DEVELOPMENT) @@ -310,13 +335,16 @@ public class ScmServletModule extends JerseyServletModule // bind new hook api bind(HookContextFactory.class); bind(HookEventFacade.class); + + // bind user-agent parser + bind(UserAgentParser.class); // bind debug logging filter if ("true".equalsIgnoreCase(System.getProperty(SYSTEM_PROPERTY_DEBUG_HTTP))) { filter(PATTERN_ALL).through(LoggingFilter.class); } - + // debug servlet serve(PATTERN_DEBUG).with(DebugServlet.class); diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AuthenticationResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AuthenticationResource.java index 9143488ccd..5d559afe62 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AuthenticationResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AuthenticationResource.java @@ -83,6 +83,7 @@ import javax.ws.rs.core.Response; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; +import sonia.scm.security.XsrfCookies; /** * @@ -266,6 +267,9 @@ public class AuthenticationResource public Response logout(@Context HttpServletRequest request, @Context HttpServletResponse response) { + // remove xsrf token + XsrfCookies.remove(request, response); + Subject subject = SecurityUtils.getSubject(); subject.logout(); diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java index 674b44c158..74e8310e05 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java @@ -553,7 +553,8 @@ public class RepositoryImportResource } catch (RepositoryAlreadyExistsException ex) { - logger.warn("a {} repository with the name {} already exists", ex); + logger.warn("a {} repository with the name {} already exists", type, + name); throw new WebApplicationException(Response.Status.CONFLICT); } @@ -692,7 +693,7 @@ public class RepositoryImportResource private void handleGenericCreationFailure(Exception ex, String type, String name) { - logger.error(String.format("could not create repository {} with type {}", + logger.error(String.format("could not create repository %s with type %s", type, name), ex); throw new WebApplicationException(ex); diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java index 42de0f00bd..640721bc64 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java @@ -1111,7 +1111,7 @@ public class RepositoryResource */ private String getContentDispositionName(String name) { - return "attachment; filename=\"".concat(name).concat("\""); + return HttpUtil.createContentDispositionAttachmentHeader(name); } /** diff --git a/scm-webapp/src/main/java/sonia/scm/debug/DebugHook.java b/scm-webapp/src/main/java/sonia/scm/debug/DebugHook.java new file mode 100644 index 0000000000..d75b455a95 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/debug/DebugHook.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.debug; + +import com.github.legman.ReferenceType; +import com.github.legman.Subscribe; +import com.google.common.base.Function; +import com.google.common.collect.Collections2; +import javax.inject.Inject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.EagerSingleton; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.PostReceiveRepositoryHookEvent; + +/** + * {@link PostReceiveRepositoryHookEvent} which stores receives data and passes it to the {@link DebugService}. + * + * @author Sebastian Sdorra + */ +@EagerSingleton +public final class DebugHook +{ + /** + * the logger for DebugHook + */ + private static final Logger LOG = LoggerFactory.getLogger(DebugHook.class); + + private final DebugService debugService; + + /** + * Constructs a new instance. + * + * @param debugService debug service + */ + @Inject + public DebugHook(DebugService debugService) + { + this.debugService = debugService; + } + + /** + * Processes the received {@link PostReceiveRepositoryHookEvent} and transforms it to a {@link DebugHookData} and + * passes it to the {@link DebugService}. + * + * @param event received event + */ + @Subscribe(referenceType = ReferenceType.STRONG) + public void processEvent(PostReceiveRepositoryHookEvent event){ + LOG.trace("store changeset ids from repository", event.getRepository().getId()); + + debugService.put( + event.getRepository().getId(), + new DebugHookData(Collections2.transform( + event.getContext().getChangesetProvider().getChangesetList(), IDEXTRACTOR) + )); + } + + private static final Function IDEXTRACTOR = (Changeset changeset) -> changeset.getId(); +} diff --git a/scm-webapp/src/main/java/sonia/scm/debug/DebugHookData.java b/scm-webapp/src/main/java/sonia/scm/debug/DebugHookData.java new file mode 100644 index 0000000000..bb8cfa42e7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/debug/DebugHookData.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.debug; + +import java.util.Collection; +import java.util.Date; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Received data from repository hook event. + * + * @author Sebastian Sdorra + */ +@XmlRootElement(name = "hook") +@XmlAccessorType(XmlAccessType.FIELD) +public class DebugHookData +{ + private Date date; + private Collection changesets; + + /** + * Constructs a new instance. This constructor should only be used by JAXB. + */ + public DebugHookData() + { + } + + /** + * Constructs a new instance. + * + * @param changesets collection of changeset ids + */ + public DebugHookData(Collection changesets) + { + this.date = new Date(); + this.changesets = changesets; + } + + /** + * Returns the receiving date. + * + * @return receiving date + */ + public Date getDate() + { + return date; + } + + /** + * Return collection of changeset ids. + * + * @return collection of changeset ids + */ + public Collection getChangesets() + { + return changesets; + } + + +} diff --git a/scm-webapp/src/main/java/sonia/scm/debug/DebugModule.java b/scm-webapp/src/main/java/sonia/scm/debug/DebugModule.java new file mode 100644 index 0000000000..3c767a1ddb --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/debug/DebugModule.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.debug; + +import com.google.inject.AbstractModule; + +/** + * DebugModule binds all required classes around the {@link DebugService}. The module will only be activated, if the + * application was started in development stage. + * + * @author Sebastian Sdorra + */ +public final class DebugModule extends AbstractModule +{ + + @Override + protected void configure() + { + bind(DebugService.class); + bind(DebugHook.class); + bind(DebugResource.class); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/debug/DebugResource.java b/scm-webapp/src/main/java/sonia/scm/debug/DebugResource.java new file mode 100644 index 0000000000..f65e0c7708 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/debug/DebugResource.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.debug; + +import java.util.Collection; +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +/** + * Rest api resource for the {@link DebugService}. + * + * @author Sebastian Sdorra + */ +@Path("debug/{repository}/post-receive") +public final class DebugResource +{ + private final DebugService debugService; + + /** + * Constructs a new instance. + * + * @param debugService debug service + */ + @Inject + public DebugResource(DebugService debugService) + { + this.debugService = debugService; + } + + /** + * Returns all received hook data for the given repository. + * + * @param repository repository id + * + * @return all received hook data for the given repository + */ + @GET + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) + public Collection getAll(@PathParam("repository") String repository){ + return debugService.getAll(repository); + } + + /** + * Returns the last received hook data for the given repository. + * + * @param repository repository id + * + * @return the last received hook data for the given repository + */ + @GET + @Path("last") + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) + public DebugHookData getLast(@PathParam("repository") String repository){ + return debugService.getLast(repository); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/debug/DebugService.java b/scm-webapp/src/main/java/sonia/scm/debug/DebugService.java new file mode 100644 index 0000000000..31282b6b08 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/debug/DebugService.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.debug; + +import com.google.common.collect.Iterables; +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Multimap; +import com.google.inject.Singleton; +import java.util.Collection; +import org.apache.shiro.SecurityUtils; +import sonia.scm.security.Role; + +/** + * The DebugService stores and returns received data from repository hook events. + * + * @author Sebastian Sdorra + */ +@Singleton +public final class DebugService +{ + + private final Multimap receivedHooks = LinkedListMultimap.create(); + + /** + * Stores {@link DebugHookData} for the given repository. + * + * @param repository repository id + * @param hookData received hook data + */ + void put(String repository, DebugHookData hookData) + { + receivedHooks.put(repository, hookData); + } + + /** + * Returns the last received hook data for the given repository. + * + * @param repository repository id + * + * @return the last received hook data for the given repository + */ + public DebugHookData getLast(String repository){ + SecurityUtils.getSubject().checkRole(Role.ADMIN); + DebugHookData hookData = null; + Collection receivedHookData = receivedHooks.get(repository); + if (receivedHookData != null && ! receivedHookData.isEmpty()){ + hookData = Iterables.getLast(receivedHookData); + } + return hookData; + } + + /** + * Returns all received hook data for the given repository. + * + * @param repository repository id + * + * @return all received hook data for the given repository + */ + public Collection getAll(String repository){ + SecurityUtils.getSubject().checkRole(Role.ADMIN); + return receivedHooks.get(repository); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/event/LegmanScmEventBus.java b/scm-webapp/src/main/java/sonia/scm/event/LegmanScmEventBus.java index ba41275324..b491c820ea 100644 --- a/scm-webapp/src/main/java/sonia/scm/event/LegmanScmEventBus.java +++ b/scm-webapp/src/main/java/sonia/scm/event/LegmanScmEventBus.java @@ -92,8 +92,9 @@ public class LegmanScmEventBus extends ScmEventBus @Override public void register(Object object) { - logger.debug("register {} to event bus", object); + logger.trace("register {} to event bus", object); eventBus.register(object); + } /** @@ -105,8 +106,8 @@ public class LegmanScmEventBus extends ScmEventBus @Override public void unregister(Object object) { - logger.debug("unregister {} from event bus", object); - + logger.trace("unregister {} from event bus", object); + try { eventBus.unregister(object); diff --git a/scm-webapp/src/main/java/sonia/scm/filter/AdminSecurityFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/AdminSecurityFilter.java index 75bcc55ad6..328fb3a2ae 100644 --- a/scm-webapp/src/main/java/sonia/scm/filter/AdminSecurityFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/filter/AdminSecurityFilter.java @@ -44,7 +44,9 @@ import sonia.scm.config.ScmConfiguration; import sonia.scm.security.Role; /** - * + * Security filter which allow only administrators to access the underlying + * resources. + * * @author Sebastian Sdorra */ @WebElement( @@ -60,10 +62,9 @@ public class AdminSecurityFilter extends SecurityFilter { /** - * Constructs ... + * Constructs a new instance. * - * - * @param configuration + * @param configuration scm-manager main configuration */ @Inject public AdminSecurityFilter(ScmConfiguration configuration) @@ -74,12 +75,17 @@ public class AdminSecurityFilter extends SecurityFilter //~--- get methods ---------------------------------------------------------- /** - * Method description - * + * Returns {@code true} if the subject has the admin role. * +<<<<<<< working copy * @param subject * * @return +======= + * @param subject subject + * + * @return {@code true} if the subject has the admin role +>>>>>>> merge rev */ @Override protected boolean hasPermission(Subject subject) diff --git a/scm-webapp/src/main/java/sonia/scm/filter/MDCFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/MDCFilter.java index 8445a29f54..9198c19ef7 100644 --- a/scm-webapp/src/main/java/sonia/scm/filter/MDCFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/filter/MDCFilter.java @@ -33,6 +33,7 @@ package sonia.scm.filter; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.annotations.VisibleForTesting; import com.google.inject.Singleton; import org.apache.shiro.SecurityUtils; @@ -63,13 +64,24 @@ public class MDCFilter extends HttpFilter { /** Field description */ - private static final String MDC_CLIEN_HOST = "client_host"; + @VisibleForTesting + static final String MDC_CLIEN_HOST = "client_host"; /** Field description */ - private static final String MDC_CLIEN_IP = "client_ip"; + @VisibleForTesting + static final String MDC_CLIEN_IP = "client_ip"; + + /** url of the current request */ + @VisibleForTesting + static final String MDC_REQUEST_URI = "request_uri"; + + /** request method */ + @VisibleForTesting + static final String MDC_REQUEST_METHOD = "request_method"; /** Field description */ - private static final String MDC_USERNAME = "username"; + @VisibleForTesting + static final String MDC_USERNAME = "username"; //~--- methods -------------------------------------------------------------- @@ -92,6 +104,8 @@ public class MDCFilter extends HttpFilter MDC.put(MDC_USERNAME, getUsername()); MDC.put(MDC_CLIEN_IP, request.getRemoteAddr()); MDC.put(MDC_CLIEN_HOST, request.getRemoteHost()); + MDC.put(MDC_REQUEST_METHOD, request.getMethod()); + MDC.put(MDC_REQUEST_URI, request.getRequestURI()); try { @@ -102,6 +116,8 @@ public class MDCFilter extends HttpFilter MDC.remove(MDC_USERNAME); MDC.remove(MDC_CLIEN_IP); MDC.remove(MDC_CLIEN_HOST); + MDC.remove(MDC_REQUEST_METHOD); + MDC.remove(MDC_REQUEST_URI); } } diff --git a/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java index be031f5ed1..7f5366ae1c 100644 --- a/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java @@ -35,8 +35,8 @@ package sonia.scm.filter; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; -import com.google.inject.Singleton; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; @@ -44,7 +44,6 @@ import org.apache.shiro.subject.Subject; import sonia.scm.Priority; import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; -import sonia.scm.user.User; import sonia.scm.web.filter.HttpFilter; import sonia.scm.web.filter.SecurityHttpServletRequestWrapper; @@ -62,11 +61,14 @@ import javax.servlet.http.HttpServletResponse; * @author Sebastian Sdorra */ @Priority(Filters.PRIORITY_AUTHORIZATION) -@WebElement(value = Filters.PATTERN_RESTAPI, - morePatterns = { Filters.PATTERN_DEBUG }) +@WebElement(value = Filters.PATTERN_RESTAPI, morePatterns = { Filters.PATTERN_DEBUG }) public class SecurityFilter extends HttpFilter { + /** name of request attribute for the primary principal */ + @VisibleForTesting + static final String ATTRIBUTE_REMOTE_USER = "principal"; + /** Field description */ public static final String URL_AUTHENTICATION = "/api/rest/authentication"; @@ -102,17 +104,21 @@ public class SecurityFilter extends HttpFilter HttpServletResponse response, FilterChain chain) throws IOException, ServletException { - Subject subject = SecurityUtils.getSubject(); - String uri = request.getRequestURI().substring(request.getContextPath().length()); if (!uri.startsWith(URL_AUTHENTICATION)) { + Subject subject = SecurityUtils.getSubject(); if (hasPermission(subject)) { - chain.doFilter(new SecurityHttpServletRequestWrapper(request, - getUsername(subject)), response); + // add primary principal as request attribute + // see https://goo.gl/JRjNmf + String username = getUsername(subject); + request.setAttribute(ATTRIBUTE_REMOTE_USER, username); + + // wrap servlet request to provide authentication informations + chain.doFilter(new SecurityHttpServletRequestWrapper(request, username), response); } else if (subject.isAuthenticated() || subject.isRemembered()) { @@ -150,14 +156,6 @@ public class SecurityFilter extends HttpFilter || subject.isRemembered(); } - /** - * Method description - * - * - * @param subject - * - * @return - */ private String getUsername(Subject subject) { String username = SCMContext.USER_ANONYMOUS; diff --git a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java index 94a40a09f3..ef69e5c07d 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java +++ b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java @@ -208,15 +208,14 @@ public class DefaultGroupManager extends AbstractGroupManager String name = group.getName(); GroupPermissions.modify().check(name); - Group oldGroup = groupDAO.get(name); - - if (oldGroup != null) + Group notModified = groupDAO.get(name); + if (notModified != null) { removeDuplicateMembers(group); + fireEvent(HandlerEventType.BEFORE_MODIFY, group, notModified); group.setLastModified(System.currentTimeMillis()); - fireEvent(HandlerEventType.BEFORE_MODIFY, group, oldGroup); groupDAO.modify(group); - fireEvent(HandlerEventType.MODIFY, group, oldGroup); + fireEvent(HandlerEventType.MODIFY, group, notModified); } else { diff --git a/scm-webapp/src/main/java/sonia/scm/net/SSLContextProvider.java b/scm-webapp/src/main/java/sonia/scm/net/SSLContextProvider.java new file mode 100644 index 0000000000..aadf8a2b13 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/net/SSLContextProvider.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.net; + +import com.google.common.base.Throwables; +import com.google.inject.Inject; +import java.security.NoSuchAlgorithmException; +import javax.inject.Named; +import javax.inject.Provider; +import javax.net.ssl.SSLContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provider for {@link SSLContext}. The provider will first try to retrieve the {@link SSLContext} from an "default" + * named optional provider, if this fails the provider will return the jvm default context. + * + * @author Sebastian Sdorra + * @version 1.47 + */ +public final class SSLContextProvider implements Provider +{ + + /** + * the logger for SSLContextProvider + */ + private static final Logger logger = LoggerFactory.getLogger(SSLContextProvider.class); + + @Named("default") + @Inject(optional = true) + private Provider sslContextProvider; + + @Override + public SSLContext get() + { + SSLContext context = null; + if (sslContextProvider != null) + { + context = sslContextProvider.get(); + } + + if (context == null) + { + try + { + logger.trace("could not find ssl context provider, use jvm default"); + context = SSLContext.getDefault(); + } + catch (NoSuchAlgorithmException ex) + { + throw Throwables.propagate(ex); + } + } + else + { + logger.trace("use custom ssl context from provider"); + } + return context; + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/net/URLHttpClient.java b/scm-webapp/src/main/java/sonia/scm/net/URLHttpClient.java index 66c1fb9914..83b0fd23d5 100644 --- a/scm-webapp/src/main/java/sonia/scm/net/URLHttpClient.java +++ b/scm-webapp/src/main/java/sonia/scm/net/URLHttpClient.java @@ -48,8 +48,10 @@ import sonia.scm.util.Util; //~--- JDK imports ------------------------------------------------------------ import com.sun.jersey.core.util.Base64; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.HttpURLConnection; @@ -67,11 +69,14 @@ import java.util.Map; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; +import sonia.scm.util.HttpUtil; /** * * @author Sebastian Sdorra + * @deprecated use {@link sonia.scm.net.ahc.AdvancedHttpClient} */ +@Deprecated public class URLHttpClient implements HttpClient { @@ -152,6 +157,9 @@ public class URLHttpClient implements HttpClient url); connection.setRequestMethod(METHOD_POST); + // send empty content-length + // see issue #701 http://goo.gl/oyTdrA + setContentLength(connection, 0); return new URLHttpResponse(connection); } @@ -347,41 +355,85 @@ public class URLHttpClient implements HttpClient { if (Util.isNotEmpty(parameters)) { + // use a ByteArrayOutputStream in order to get the final content-length + // see issue #701 http://goo.gl/oyTdrA connection.setDoOutput(true); OutputStreamWriter writer = null; + ByteArrayOutputStream baos = null; try { - writer = new OutputStreamWriter(connection.getOutputStream()); + baos = new ByteArrayOutputStream(); + writer = new OutputStreamWriter(baos); - Iterator>> it = - parameters.entrySet().iterator(); - - while (it.hasNext()) - { - Map.Entry> p = it.next(); - List values = p.getValue(); - - if (Util.isNotEmpty(values)) - { - String key = encode(p.getKey()); - - for (String value : values) - { - writer.append(key).append("=").append(encode(value)); - } - - if (it.hasNext()) - { - writer.write("&"); - } - } - } + appendPostParameters(writer, parameters); } finally { IOUtil.close(writer); + IOUtil.close(baos); + } + + if ( baos != null ){ + byte[] data = baos.toByteArray(); + appendBody(connection, data); + } + } + else + { + setContentLength(connection, 0); + } + } + + private void appendBody(HttpURLConnection connection, byte[] body) throws IOException + { + int length = body.length; + logger.trace("write {} bytes to output", length); + setContentLength(connection, length); + connection.setFixedLengthStreamingMode(length); + OutputStream os = null; + try { + os = connection.getOutputStream(); + os.write(body); + } finally { + IOUtil.close(os); + } + } + + private void setContentLength(HttpURLConnection connection, int length) + { + connection.setRequestProperty(HttpUtil.HEADER_CONTENT_LENGTH, String.valueOf(length)); + } + + private void appendPostParameters(OutputStreamWriter writer, Map> parameters) throws IOException + { + Iterator>> it = parameters.entrySet().iterator(); + + while (it.hasNext()) + { + Map.Entry> p = it.next(); + List values = p.getValue(); + + if (Util.isNotEmpty(values)) + { + String key = encode(p.getKey()); + + Iterator valueIt = values.iterator(); + + while(valueIt.hasNext()) + { + String value = valueIt.next(); + writer.append(key).append("=").append(encode(value)); + if ( valueIt.hasNext() ){ + writer.write("&"); + } + } + + if (it.hasNext()) + { + writer.write("&"); + } } } } diff --git a/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpClient.java b/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpClient.java new file mode 100644 index 0000000000..df6c93515b --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpClient.java @@ -0,0 +1,409 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.collect.Multimap; +import com.google.common.io.Closeables; +import com.google.inject.Inject; + +import org.apache.shiro.codec.Base64; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import sonia.scm.config.ScmConfiguration; +import sonia.scm.net.Proxies; +import sonia.scm.net.TrustAllHostnameVerifier; +import sonia.scm.net.TrustAllTrustManager; +import sonia.scm.util.HttpUtil; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.IOException; +import java.io.OutputStream; + +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.ProtocolException; +import java.net.Proxy; +import java.net.SocketAddress; +import java.net.URL; + +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +import java.util.Set; +import javax.inject.Provider; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; + +/** + * Default implementation of the {@link AdvancedHttpClient}. The default + * implementation uses {@link HttpURLConnection}. + * + * @author Sebastian Sdorra + * @since 1.46 + */ +public class DefaultAdvancedHttpClient extends AdvancedHttpClient +{ + + /** proxy authorization header */ + @VisibleForTesting + static final String HEADER_PROXY_AUTHORIZATION = "Proxy-Authorization"; + + /** connection timeout */ + @VisibleForTesting + static final int TIMEOUT_CONNECTION = 30000; + + /** read timeout */ + @VisibleForTesting + static final int TIMEOUT_RAED = 1200000; + + /** credential separator */ + private static final String CREDENTIAL_SEPARATOR = ":"; + + /** basic authentication prefix */ + private static final String PREFIX_BASIC_AUTHENTICATION = "Basic "; + + /** + * the logger for DefaultAdvancedHttpClient + */ + private static final Logger logger = + LoggerFactory.getLogger(DefaultAdvancedHttpClient.class); + + //~--- constructors --------------------------------------------------------- + + /** + * Constructs a new {@link DefaultAdvancedHttpClient}. + * + * + * @param configuration scm-manager main configuration + * @param contentTransformers content transformer + * @param sslContextProvider ssl context provider + */ + @Inject + public DefaultAdvancedHttpClient(ScmConfiguration configuration, + Set contentTransformers, Provider sslContextProvider) + { + this.configuration = configuration; + this.contentTransformers = contentTransformers; + this.sslContextProvider = sslContextProvider; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Creates a new {@link HttpURLConnection} from the given {@link URL}. The + * method is visible for testing. + * + * + * @param url url + * + * @return new {@link HttpURLConnection} + * + * @throws IOException + */ + @VisibleForTesting + protected HttpURLConnection createConnection(URL url) throws IOException + { + return (HttpURLConnection) url.openConnection(); + } + + /** + * Creates a new proxy {@link HttpURLConnection} from the given {@link URL} + * and {@link SocketAddress}. The method is visible for testing. + * + * + * @param url url + * @param address proxy socket address + * + * @return new proxy {@link HttpURLConnection} + * + * @throws IOException + */ + @VisibleForTesting + protected HttpURLConnection createProxyConnecton(URL url, + SocketAddress address) + throws IOException + { + return (HttpURLConnection) url.openConnection(new Proxy(Proxy.Type.HTTP, + address)); + } + + /** + * {@inheritDoc} + */ + @Override + protected ContentTransformer createTransformer(Class type, String contentType) + { + ContentTransformer responsible = null; + + for (ContentTransformer transformer : contentTransformers) + { + if (transformer.isResponsible(type, contentType)) + { + responsible = transformer; + + break; + } + } + + if (responsible == null) + { + throw new ContentTransformerNotFoundException( + "could not find content transformer for content type ".concat( + contentType)); + } + + return responsible; + } + + /** + * Executes the given request and returns the server response. + * + * + * @param request http request + * + * @return server response + * + * @throws IOException + */ + @Override + protected AdvancedHttpResponse request(BaseHttpRequest request) + throws IOException + { + HttpURLConnection connection = openConnection(request, + new URL(request.getUrl())); + + applyBaseSettings(request, connection); + + if (connection instanceof HttpsURLConnection) + { + applySSLSettings(request, (HttpsURLConnection) connection); + } + + Content content = null; + + if (request instanceof AdvancedHttpRequestWithBody) + { + AdvancedHttpRequestWithBody ahrwb = (AdvancedHttpRequestWithBody) request; + + content = ahrwb.getContent(); + + if (content != null) + { + content.prepare(ahrwb); + } + else + { + request.header(HttpUtil.HEADER_CONTENT_LENGTH, "0"); + } + } + else + { + request.header(HttpUtil.HEADER_CONTENT_LENGTH, "0"); + } + + applyHeaders(request, connection); + + if (content != null) + { + applyContent(connection, content); + } + + return new DefaultAdvancedHttpResponse(this, connection, + connection.getResponseCode(), connection.getResponseMessage()); + } + + private void appendProxyAuthentication(HttpURLConnection connection) + { + String username = configuration.getProxyUser(); + String password = configuration.getProxyPassword(); + + if (!Strings.isNullOrEmpty(username) ||!Strings.isNullOrEmpty(password)) + { + logger.debug("append proxy authentication header for user {}", username); + + String auth = username.concat(CREDENTIAL_SEPARATOR).concat(password); + + auth = Base64.encodeToString(auth.getBytes()); + connection.addRequestProperty(HEADER_PROXY_AUTHORIZATION, + PREFIX_BASIC_AUTHENTICATION.concat(auth)); + } + } + + private void applyBaseSettings(BaseHttpRequest request, + HttpURLConnection connection) + throws ProtocolException + { + connection.setRequestMethod(request.getMethod()); + connection.setReadTimeout(TIMEOUT_RAED); + connection.setConnectTimeout(TIMEOUT_CONNECTION); + } + + private void applyContent(HttpURLConnection connection, Content content) + throws IOException + { + connection.setDoOutput(true); + + OutputStream output = null; + + try + { + output = connection.getOutputStream(); + content.process(output); + } + finally + { + Closeables.close(output, true); + } + } + + private void applyHeaders(BaseHttpRequest request, + HttpURLConnection connection) + { + Multimap headers = request.getHeaders(); + + for (String key : headers.keySet()) + { + for (String value : headers.get(key)) + { + connection.addRequestProperty(key, value); + } + } + } + + private void applySSLSettings(BaseHttpRequest request, + HttpsURLConnection connection) + { + if (request.isDisableCertificateValidation()) + { + logger.trace("disable certificate validation"); + + try + { + TrustManager[] trustAllCerts = new TrustManager[] { + new TrustAllTrustManager() }; + SSLContext sc = SSLContext.getInstance("SSL"); + + sc.init(null, trustAllCerts, new java.security.SecureRandom()); + connection.setSSLSocketFactory(sc.getSocketFactory()); + } + catch (KeyManagementException ex) + { + logger.error("could not disable certificate validation", ex); + } + catch (NoSuchAlgorithmException ex) + { + logger.error("could not disable certificate validation", ex); + } + } + else + { + logger.trace("set ssl socker factory from provider"); + connection.setSSLSocketFactory(sslContextProvider.get().getSocketFactory()); + } + + if (request.isDisableHostnameValidation()) + { + logger.trace("disable hostname validation"); + connection.setHostnameVerifier(new TrustAllHostnameVerifier()); + } + } + + private HttpURLConnection openConnection(BaseHttpRequest request, URL url) + throws IOException + { + HttpURLConnection connection; + + if (isProxyEnabled(request)) + { + connection = openProxyConnection(request, url); + appendProxyAuthentication(connection); + } + else + { + if (request.isIgnoreProxySettings()) + { + logger.trace("ignore proxy settings"); + } + + logger.debug("fetch {}", url.toExternalForm()); + + connection = createConnection(url); + } + + return connection; + } + + private HttpURLConnection openProxyConnection(BaseHttpRequest request, + URL url) + throws IOException + { + if (logger.isDebugEnabled()) + { + logger.debug("fetch '{}' using proxy {}:{}", url.toExternalForm(), + configuration.getProxyServer(), configuration.getProxyPort()); + } + + SocketAddress address = + new InetSocketAddress(configuration.getProxyServer(), + configuration.getProxyPort()); + + return createProxyConnecton(url, address); + } + + //~--- get methods ---------------------------------------------------------- + + private boolean isProxyEnabled(BaseHttpRequest request) + { + return !request.isIgnoreProxySettings() + && Proxies.isEnabled(configuration, request.getUrl()); + } + + //~--- fields --------------------------------------------------------------- + + /** scm-manager main configuration */ + private final ScmConfiguration configuration; + + /** set of content transformers */ + private final Set contentTransformers; + + /** ssl context provider */ + private final Provider sslContextProvider; +} diff --git a/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponse.java b/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponse.java new file mode 100644 index 0000000000..21179ce1b6 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponse.java @@ -0,0 +1,226 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import com.google.common.io.ByteSource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.IOException; +import java.io.InputStream; + +import java.net.HttpURLConnection; + +import java.util.List; +import java.util.Map.Entry; + +/** + * Http server response object of {@link DefaultAdvancedHttpClient}. + * + * @author Sebastian Sdorra + * @since 1.46 + */ +public class DefaultAdvancedHttpResponse extends AdvancedHttpResponse +{ + + /** + * Constructs a new {@link DefaultAdvancedHttpResponse}. + * + * @param client ahc + * @param connection http connection + * @param status response status code + * @param statusText response status text + */ + DefaultAdvancedHttpResponse(DefaultAdvancedHttpClient client, + HttpURLConnection connection, int status, String statusText) + { + this.client = client; + this.connection = connection; + this.status = status; + this.statusText = statusText; + } + + //~--- methods -------------------------------------------------------------- + + /** + * {@inheritDoc} + */ + @Override + public ByteSource contentAsByteSource() throws IOException + { + return new URLConnectionByteSource(connection); + } + + //~--- get methods ---------------------------------------------------------- + + /** + * {@inheritDoc} + */ + @Override + public Multimap getHeaders() + { + if (headers == null) + { + headers = LinkedHashMultimap.create(); + + for (Entry> e : + connection.getHeaderFields().entrySet()) + { + headers.putAll(e.getKey(), e.getValue()); + } + } + + return headers; + } + + /** + * {@inheritDoc} + */ + @Override + public int getStatus() + { + return status; + } + + /** + * {@inheritDoc} + */ + @Override + public String getStatusText() + { + return statusText; + } + + //~--- methods -------------------------------------------------------------- + + /** + * {@inheritDoc} + */ + @Override + protected ContentTransformer createTransformer(Class type, + String contentType) + { + return client.createTransformer(type, contentType); + } + + //~--- inner classes -------------------------------------------------------- + + /** + * {@link ByteSource} implementation of a http connection. + */ + private static class URLConnectionByteSource extends ByteSource + { + + /** + * the logger for URLConnectionByteSource + */ + private static final Logger logger = + LoggerFactory.getLogger(URLConnectionByteSource.class); + + //~--- constructors ------------------------------------------------------- + + /** + * Constructs a new {@link URLConnectionByteSource}. + * + * + * @param connection http connection + */ + private URLConnectionByteSource(HttpURLConnection connection) + { + this.connection = connection; + } + + //~--- methods ------------------------------------------------------------ + + /** + * Opens the input stream of http connection, if an error occurs during + * opening the method will return the error stream instead. + * + * + * @return input or error stream of http connection + * + * @throws IOException + */ + @Override + public InputStream openStream() throws IOException + { + InputStream stream; + + try + { + stream = connection.getInputStream(); + } + catch (IOException ex) + { + if (logger.isDebugEnabled()) + { + logger.debug( + "could not open input stream, open error stream instead", ex); + } + + stream = connection.getErrorStream(); + } + + return stream; + } + + //~--- fields ------------------------------------------------------------- + + /** http connection */ + private final HttpURLConnection connection; + } + + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + private final DefaultAdvancedHttpClient client; + + /** http connection */ + private final HttpURLConnection connection; + + /** server response status */ + private final int status; + + /** server response text */ + private final String statusText; + + /** http headers */ + private Multimap headers; +} diff --git a/scm-webapp/src/main/java/sonia/scm/net/ahc/JsonContentTransformer.java b/scm-webapp/src/main/java/sonia/scm/net/ahc/JsonContentTransformer.java new file mode 100644 index 0000000000..fce81ba386 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/net/ahc/JsonContentTransformer.java @@ -0,0 +1,157 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.io.ByteSource; + +import org.codehaus.jackson.map.AnnotationIntrospector; +import org.codehaus.jackson.map.DeserializationConfig; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.introspect.JacksonAnnotationIntrospector; +import org.codehaus.jackson.xc.JaxbAnnotationIntrospector; + +import sonia.scm.plugin.Extension; +import sonia.scm.util.IOUtil; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.ws.rs.core.MediaType; + +/** + * {@link ContentTransformer} for json. The {@link JsonContentTransformer} uses + * jacksons {@link ObjectMapper} to marshalling/unmarshalling. + * + * @author Sebastian Sdorra + * @since 1.46 + */ +@Extension +public class JsonContentTransformer implements ContentTransformer +{ + + /** + * Constructs a new {@link JsonContentTransformer}. + * + */ + public JsonContentTransformer() + { + this.mapper = new ObjectMapper(); + + // allow jackson and jaxb annotations + AnnotationIntrospector jackson = new JacksonAnnotationIntrospector(); + AnnotationIntrospector jaxb = new JaxbAnnotationIntrospector(); + + this.mapper.setAnnotationIntrospector(new AnnotationIntrospector.Pair(jackson, jaxb)); + + // do not fail on unknown json properties + this.mapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + //~--- methods -------------------------------------------------------------- + + /** + * {@inheritDoc} + */ + @Override + public ByteSource marshall(Object object) + { + ByteSource source = null; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + try + { + mapper.writeValue(baos, object); + source = ByteSource.wrap(baos.toByteArray()); + } + catch (IOException ex) + { + throw new ContentTransformerException("could not marshall object", ex); + } + + return source; + } + + /** + * {@inheritDoc} + */ + @Override + public T unmarshall(Class type, ByteSource content) + { + T object = null; + InputStream stream = null; + + try + { + stream = content.openBufferedStream(); + object = mapper.readValue(stream, type); + } + catch (IOException ex) + { + throw new ContentTransformerException("could not unmarshall content", ex); + } + finally + { + IOUtil.close(stream); + } + + return object; + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Returns {@code true}, if the content type is compatible with + * application/json. + * + * + * @param type object type + * @param contentType content type + * + * @return {@code true}, if the content type is compatible with + * application/json + */ + @Override + public boolean isResponsible(Class type, String contentType) + { + return MediaType.valueOf(contentType).isCompatible(MediaType.APPLICATION_JSON_TYPE); + } + + //~--- fields --------------------------------------------------------------- + + /** object mapper */ + private final ObjectMapper mapper; +} diff --git a/scm-webapp/src/main/java/sonia/scm/net/ahc/XmlContentTransformer.java b/scm-webapp/src/main/java/sonia/scm/net/ahc/XmlContentTransformer.java new file mode 100644 index 0000000000..84dcda3593 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/net/ahc/XmlContentTransformer.java @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.io.ByteSource; + +import sonia.scm.plugin.Extension; +import sonia.scm.util.IOUtil; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.ws.rs.core.MediaType; + +import javax.xml.bind.DataBindingException; +import javax.xml.bind.JAXB; + +/** + * {@link ContentTransformer} for xml. The {@link XmlContentTransformer} uses + * jaxb to marshalling/unmarshalling. + * + * @author Sebastian Sdorra + * @since 1.46 + */ +@Extension +public class XmlContentTransformer implements ContentTransformer +{ + + /** + * {@inheritDoc} + */ + @Override + public ByteSource marshall(Object object) + { + ByteSource source = null; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + try + { + JAXB.marshal(object, baos); + source = ByteSource.wrap(baos.toByteArray()); + } + catch (DataBindingException ex) + { + throw new ContentTransformerException("could not marshall object", ex); + } + + return source; + } + + /** + * {@inheritDoc} + */ + @Override + public T unmarshall(Class type, ByteSource content) + { + T object = null; + InputStream stream = null; + + try + { + stream = content.openBufferedStream(); + object = JAXB.unmarshal(stream, type); + } + catch (IOException ex) + { + throw new ContentTransformerException("could not unmarshall content", ex); + } + catch (DataBindingException ex) + { + throw new ContentTransformerException("could not unmarshall content", ex); + } + finally + { + IOUtil.close(stream); + } + + return object; + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Returns {@code true}, if the content type is compatible with + * application/xml. + * + * + * @param type object type + * @param contentType content type + * + * @return {@code true}, if the content type is compatible with + * application/xml + */ + @Override + public boolean isResponsible(Class type, String contentType) + { + return MediaType.valueOf(contentType).isCompatible( + MediaType.APPLICATION_XML_TYPE); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginLoader.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginLoader.java index 3f793e3655..cc3ef01c56 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginLoader.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginLoader.java @@ -39,7 +39,6 @@ import com.google.common.base.Throwables; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet.Builder; import com.google.common.collect.Iterables; -import com.google.inject.Module; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java index 3771005ca7..ed1f691988 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -40,20 +40,17 @@ import com.github.legman.Subscribe; import com.google.common.base.Predicate; import com.google.common.io.Files; import com.google.inject.Inject; -import com.google.inject.Provider; import com.google.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.ConfigurationException; import sonia.scm.SCMContextProvider; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfigurationChangedEvent; import sonia.scm.io.ZipUnArchiver; -import sonia.scm.net.HttpClient; import sonia.scm.util.AssertUtil; import sonia.scm.util.IOUtil; import sonia.scm.util.SystemUtil; @@ -78,12 +75,12 @@ import java.util.Map; import java.util.Set; import javax.xml.bind.JAXB; -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBException; -import javax.xml.bind.Unmarshaller; + +import sonia.scm.net.ahc.AdvancedHttpClient; /** - * TODO replace aether stuff + * TODO replace aether stuff. + * TODO check AdvancedPluginConfiguration from 1.x * * @author Sebastian Sdorra */ @@ -117,17 +114,17 @@ public class DefaultPluginManager implements PluginManager * @param configuration * @param pluginLoader * @param cacheManager - * @param clientProvider + * @param httpClient */ @Inject public DefaultPluginManager(SCMContextProvider context, ScmConfiguration configuration, PluginLoader pluginLoader, - CacheManager cacheManager, Provider clientProvider) + CacheManager cacheManager, AdvancedHttpClient httpClient) { this.context = context; this.configuration = configuration; this.cache = cacheManager.getCache(CACHE_NAME); - this.clientProvider = clientProvider; + this.httpClient = httpClient; installedPlugins = new HashMap<>(); for (PluginWrapper wrapper : pluginLoader.getInstalledPlugins()) @@ -140,16 +137,6 @@ public class DefaultPluginManager implements PluginManager installedPlugins.put(info.getId(), plugin); } } - - try - { - unmarshaller = - JAXBContext.newInstance(PluginCenter.class).createUnmarshaller(); - } - catch (JAXBException ex) - { - throw new ConfigurationException(ex); - } } //~--- methods -------------------------------------------------------------- @@ -624,21 +611,9 @@ public class DefaultPluginManager implements PluginManager */ if (REMOTE_PLUGINS_ENABLED && Util.isNotEmpty(pluginUrl)) { - InputStream input = null; - try { - input = clientProvider.get().get(pluginUrl).getContent(); - - /* - * TODO: add gzip support - * - * if (gzip) - * { - * input = new GZIPInputStream(input); - * } - */ - center = (PluginCenter) unmarshaller.unmarshal(input); + center = httpClient.get(pluginUrl).request().contentFromXml(PluginCenter.class); preparePlugins(center); cache.put(PluginCenter.class.getName(), center); @@ -653,14 +628,10 @@ public class DefaultPluginManager implements PluginManager * pluginHandler.setPluginRepositories(center.getRepositories()); */ } - catch (IOException | JAXBException ex) + catch (IOException ex) { logger.error("could not load plugins from plugin center", ex); } - finally - { - IOUtil.close(input); - } } if (center == null) @@ -758,7 +729,7 @@ public class DefaultPluginManager implements PluginManager private final Cache cache; /** Field description */ - private final Provider clientProvider; + private final AdvancedHttpClient httpClient; /** Field description */ private final ScmConfiguration configuration; @@ -768,7 +739,4 @@ public class DefaultPluginManager implements PluginManager /** Field description */ private final Map installedPlugins; - - /** Field description */ - private Unmarshaller unmarshaller; } diff --git a/scm-webapp/src/main/java/sonia/scm/resources/AbstractResourceManager.java b/scm-webapp/src/main/java/sonia/scm/resources/AbstractResourceManager.java index 1316d46dd1..95d7b39b69 100644 --- a/scm-webapp/src/main/java/sonia/scm/resources/AbstractResourceManager.java +++ b/scm-webapp/src/main/java/sonia/scm/resources/AbstractResourceManager.java @@ -146,6 +146,9 @@ public abstract class AbstractResourceManager implements ResourceManager processPlugin(resources, plugin.getPlugin()); } } + + // fix order of script resources, see https://goo.gl/ok03l4 + Collections.sort(resources); return resources; } diff --git a/scm-webapp/src/main/java/sonia/scm/schedule/InjectionEnabledJob.java b/scm-webapp/src/main/java/sonia/scm/schedule/InjectionEnabledJob.java new file mode 100644 index 0000000000..44ac5ae3ab --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/schedule/InjectionEnabledJob.java @@ -0,0 +1,90 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.schedule; + +import com.google.common.base.Preconditions; +import com.google.inject.Injector; +import com.google.inject.Provider; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.web.security.AdministrationContext; +import sonia.scm.web.security.PrivilegedAction; + +/** + * InjectionEnabledJob allows the execution of quartz jobs and enable injection on them. + * + * @author Sebastian Sdorra + * @since 1.47 + */ +public class InjectionEnabledJob implements Job { + + private static final Logger logger = LoggerFactory.getLogger(InjectionEnabledJob.class); + + @Override + public void execute(JobExecutionContext jec) throws JobExecutionException { + Preconditions.checkNotNull(jec, "execution context is null"); + + JobDetail detail = jec.getJobDetail(); + Preconditions.checkNotNull(detail, "job detail not provided"); + + JobDataMap dataMap = detail.getJobDataMap(); + Preconditions.checkNotNull(dataMap, "job detail does not contain data map"); + + Injector injector = (Injector) dataMap.get(Injector.class.getName()); + Preconditions.checkNotNull(injector, "data map does not contain injector"); + + final Provider runnableProvider = (Provider) dataMap.get(Runnable.class.getName()); + if (runnableProvider == null) { + throw new JobExecutionException("could not find runnable provider"); + } + + AdministrationContext ctx = injector.getInstance(AdministrationContext.class); + ctx.runAsAdmin(new PrivilegedAction() + { + @Override + public void run() + { + logger.trace("create runnable from provider"); + Runnable runnable = runnableProvider.get(); + logger.debug("execute injection enabled job {}", runnable.getClass()); + runnable.run(); + } + }); + } + + +} diff --git a/scm-webapp/src/main/java/sonia/scm/schedule/QuartzScheduler.java b/scm-webapp/src/main/java/sonia/scm/schedule/QuartzScheduler.java new file mode 100644 index 0000000000..5b46d438d8 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/schedule/QuartzScheduler.java @@ -0,0 +1,175 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.schedule; + +import com.google.common.base.Throwables; +import com.google.inject.Injector; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import java.io.IOException; +import javax.inject.Inject; +import org.quartz.CronScheduleBuilder; +import org.quartz.JobBuilder; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.SchedulerException; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; +import org.quartz.impl.StdSchedulerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.Initable; +import sonia.scm.SCMContextProvider; + +/** + * {@link Scheduler} which uses the quartz scheduler. + * + * @author Sebastian Sdorra + * @since 1.47 + * + * @see Quartz Job Scheduler + */ +@Singleton +public class QuartzScheduler implements Scheduler, Initable { + + private static final Logger logger = LoggerFactory.getLogger(QuartzScheduler.class); + + private final Injector injector; + private final org.quartz.Scheduler scheduler; + + /** + * Creates a new quartz scheduler. + * + * @param injector injector + */ + @Inject + public QuartzScheduler(Injector injector) + { + this.injector = injector; + + // get default scheduler + try { + scheduler = StdSchedulerFactory.getDefaultScheduler(); + } catch (SchedulerException ex) { + throw Throwables.propagate(ex); + } + } + + /** + * Creates a new quartz scheduler. This constructor is only for testing. + * + * @param injector injector + * @param scheduler quartz scheduler + */ + QuartzScheduler(Injector injector, org.quartz.Scheduler scheduler) + { + this.injector = injector; + this.scheduler = scheduler; + } + + @Override + public void init(SCMContextProvider context) + { + try + { + if (!scheduler.isStarted()) + { + scheduler.start(); + } + } + catch (SchedulerException ex) + { + logger.error("can not start scheduler", ex); + } + } + + @Override + public void close() throws IOException + { + try + { + if (scheduler.isStarted()){ + scheduler.shutdown(); + } + } + catch (SchedulerException ex) + { + logger.error("can not stop scheduler", ex); + } + } + + @Override + public Task schedule(String expression, final Runnable runnable) + { + return schedule(expression, new Provider(){ + @Override + public Runnable get() + { + return runnable; + } + }); + } + + @Override + public Task schedule(String expression, Class runnable) + { + return schedule(expression, injector.getProvider(runnable)); + } + + private Task schedule(String expression, Provider provider){ + // create data map with injection provider for InjectionEnabledJob + JobDataMap map = new JobDataMap(); + map.put(Runnable.class.getName(), provider); + map.put(Injector.class.getName(), injector); + + // create job detail for InjectionEnabledJob with the provider for the annotated class + JobDetail detail = JobBuilder.newJob(InjectionEnabledJob.class) + .usingJobData(map) + .build(); + + // create a trigger with the cron expression from the annotation + Trigger trigger = TriggerBuilder.newTrigger() + .forJob(detail) + .withSchedule(CronScheduleBuilder.cronSchedule(expression)) + .build(); + + try { + scheduler.scheduleJob(detail, trigger); + } catch (SchedulerException ex) { + throw Throwables.propagate(ex); + } + + return new QuartzTask(scheduler, trigger.getJobKey()); + } + + +} diff --git a/scm-webapp/src/main/java/sonia/scm/schedule/QuartzTask.java b/scm-webapp/src/main/java/sonia/scm/schedule/QuartzTask.java new file mode 100644 index 0000000000..a45790a2e9 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/schedule/QuartzTask.java @@ -0,0 +1,68 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.schedule; + +import com.google.common.base.Throwables; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; + +/** + * Task implementation for the {@link QuartzScheduler}. + * + * @author Sebastian Sdorra + */ +public class QuartzTask implements Task { + + private final org.quartz.Scheduler scheduler; + private final JobKey jobKey; + + QuartzTask(Scheduler scheduler, JobKey jobKey) + { + this.scheduler = scheduler; + this.jobKey = jobKey; + } + + @Override + public void cancel() + { + try + { + scheduler.deleteJob(jobKey); + } + catch (SchedulerException ex) + { + throw Throwables.propagate(ex); + } + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index 3ce787e2b4..e3826ad853 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -70,6 +70,10 @@ import sonia.scm.util.Util; import java.util.List; import java.util.Set; +import sonia.scm.group.Group; +import sonia.scm.group.GroupModificationEvent; +import sonia.scm.repository.RepositoryModificationEvent; +import sonia.scm.user.UserModificationEvent; /** * @@ -80,6 +84,9 @@ import java.util.Set; public class DefaultAuthorizationCollector implements AuthorizationCollector { + // TODO move to util class + private static final String SEPARATOR = System.getProperty("line.separator", "\n"); + /** Field description */ private static final String ADMIN_PERMISSION = "*"; @@ -139,10 +146,15 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector } /** - * Method description + * Invalidates the cache of a user which was modified. The cache entries for the user will be invalidated for the + * following reasons: + *
    + *
  • Admin or Active flag was modified.
  • + *
  • New user created, for the case of old cache values
  • + *
  • User deleted
  • + *
* - * - * @param event + * @param event user event */ @Subscribe public void onEvent(UserEvent event) @@ -150,82 +162,160 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector if (event.getEventType().isPost()) { User user = event.getItem(); - - if (logger.isDebugEnabled()) + String username = user.getId(); + if (event instanceof UserModificationEvent) { - logger.debug( - "clear cache of user {}, because user properties have changed", - user.getName()); + User beforeModification = ((UserModificationEvent) event).getItemBeforeModification(); + if (shouldCacheBeCleared(user, beforeModification)) + { + logger.debug("invalidate cache of user {}, because of a permission relevant field has changed", username); + invalidateUserCache(username); + } + else + { + logger.debug("cache of user {} is not invalidated, because no permission relevant field has changed", username); + } + } + else + { + logger.debug("invalidate cache of user {}, because of user {} event", username, event.getEventType()); + invalidateUserCache(username); } - - // check if this is neccessary - cache.clear(); } } + private boolean shouldCacheBeCleared(User user, User beforeModification) + { + return user.isAdmin() != beforeModification.isAdmin() || user.isActive() != beforeModification.isActive(); + } + + private void invalidateUserCache(final String username) + { + cache.removeAll((CacheKey item) -> username.equalsIgnoreCase(item.username)); + } + /** - * Method description + * Invalidates the whole cache, if a repository has changed. The cache get cleared for one of the following reasons: + *
    + *
  • New repository created
  • + *
  • Repository was removed
  • + *
  • Archived, Public readable or permission field of the repository was modified
  • + *
* - * - * @param event + * @param event repository event */ @Subscribe public void onEvent(RepositoryEvent event) { if (event.getEventType().isPost()) { - if (logger.isDebugEnabled()) + Repository repository = event.getItem(); + + if (event instanceof RepositoryModificationEvent) { - logger.debug("clear cache, because repository {} has changed", - event.getItem().getName()); + Repository beforeModification = ((RepositoryModificationEvent) event).getItemBeforeModification(); + if (shouldCacheBeCleared(repository, beforeModification)) + { + logger.debug("clear cache, because a relevant field of repository {} has changed", repository.getName()); + cache.clear(); + } + else + { + logger.debug( + "cache is not invalidated, because non relevant field of repository {} has changed", + repository.getName() + ); + } + } + else + { + logger.debug("clear cache, received {} event of repository {}", event.getEventType(), repository.getName()); + cache.clear(); } - - cache.clear(); } } + private boolean shouldCacheBeCleared(Repository repository, Repository beforeModification) + { + return repository.isArchived() != beforeModification.isArchived() + || repository.isPublicReadable() != beforeModification.isPublicReadable() + || ! repository.getPermissions().equals(beforeModification.getPermissions()); + } + /** - * Method description + * Invalidates the whole cache if a group permission has changed and invalidates the cached entries of a user, if a + * user permission has changed. * * - * @param event + * @param event permission event */ @Subscribe public void onEvent(StoredAssignedPermissionEvent event) { if (event.getEventType().isPost()) { - if (logger.isDebugEnabled()) + StoredAssignedPermission permission = event.getPermission(); + if (permission.isGroupPermission()) { - logger.debug("clear cache, because permission {} has changed", - event.getPermission().getId()); + logger.debug("clear cache, because global group permission {} has changed", permission.getId()); + cache.clear(); + } + else + { + logger.debug( + "clear cache of user {}, because permission {} has changed", + permission.getName(), event.getPermission().getId() + ); + invalidateUserCache(permission.getName()); } - - cache.clear(); } } /** - * Method description + * Invalidates the whole cache, if a group has changed. The cache get cleared for one of the following reasons: + *
    + *
  • New group created
  • + *
  • Group was removed
  • + *
  • Group members was modified
  • + *
* - * - * @param event + * @param event group event */ @Subscribe public void onEvent(GroupEvent event) { if (event.getEventType().isPost()) { - if (logger.isDebugEnabled()) + Group group = event.getItem(); + if (event instanceof GroupModificationEvent) { - logger.debug("clear cache, because group {} has changed", - event.getItem().getId()); + Group beforeModification = ((GroupModificationEvent) event).getItemBeforeModification(); + if (shouldCacheBeCleared(group, beforeModification)) + { + logger.debug("clear cache, because group {} has changed", group.getId()); + cache.clear(); + } + else + { + logger.debug( + "cache is not invalidated, because non relevant field of group {} has changed", + group.getId() + ); + } + } + else + { + logger.debug("clear cache, received group event {} for group {}", event.getEventType(), group.getId()); + cache.clear(); } - - cache.clear(); } } + private boolean shouldCacheBeCleared(Group group, Group beforeModification) + { + return !group.getMembers().equals(beforeModification.getMembers()); + } + /** * Method description * @@ -251,18 +341,13 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector if (info == null) { - if (logger.isTraceEnabled()) - { - logger.trace("collect AuthorizationInfo for user {}", user.getName()); - } - + logger.trace("collect AuthorizationInfo for user {}", user.getName()); info = createAuthorizationInfo(user, groupNames); cache.put(cacheKey, info); } else if (logger.isTraceEnabled()) { - logger.trace("retrieve AuthorizationInfo for user {} from cache", - user.getName()); + logger.trace("retrieve AuthorizationInfo for user {} from cache", user.getName()); } return info; @@ -271,21 +356,8 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector private void collectGlobalPermissions(Builder builder, final User user, final GroupNames groups) { - if (logger.isTraceEnabled()) - { - logger.trace("collect global permissions for user {}", user.getName()); - } - List globalPermissions = - securitySystem.getPermissions(new Predicate() - { - - @Override - public boolean apply(AssignedPermission input) - { - return isUserPermission(user, groups, input); - } - }); + securitySystem.getPermissions((AssignedPermission input) -> isUserPermitted(user, groups, input)); for (StoredAssignedPermission gp : globalPermissions) { @@ -301,12 +373,6 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector { for (Repository repository : repositoryDAO.getAll()) { - if (logger.isTraceEnabled()) - { - logger.trace("collect permissions for repository {} and user {}", - repository.getName(), user.getName()); - } - collectRepositoryPermissions(builder, repository, user, groups); } } @@ -314,30 +380,36 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector private void collectRepositoryPermissions(Builder builder, Repository repository, User user, GroupNames groups) { - List repositoryPermissions = - repository.getPermissions(); + List repositoryPermissions + = repository.getPermissions(); if (Util.isNotEmpty(repositoryPermissions)) { - + boolean hasPermission = false; for (sonia.scm.repository.Permission permission : repositoryPermissions) { - if (isUserPermission(user, groups, permission)) + if (isUserPermitted(user, groups, permission)) { - String perm = permission.getType().getPermissionPrefix().concat( - repository.getId()); - - logger.trace("add repository permission {} for user {}", perm, - user.getName()); + String perm = permission.getType().getPermissionPrefix().concat(repository.getId()); + if (logger.isTraceEnabled()) + { + logger.trace("add repository permission {} for user {} at repository {}", + perm, user.getName(), repository.getName()); + } builder.add(perm); } } + + if (!hasPermission && logger.isTraceEnabled()) + { + logger.trace("no permission for user {} defined at repository {}", user.getName(), repository.getName()); + } } else if (logger.isTraceEnabled()) { - logger.trace("repository {} has not permission entries", + logger.trace("repository {} has no permission entries", repository.getName()); } } @@ -371,19 +443,47 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector } SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles); - info.addStringPermissions(permissions); - + + if (logger.isTraceEnabled()){ + logger.trace(createAuthorizationSummary(user, groups, info)); + } + return info; } + + private String createAuthorizationSummary(User user, GroupNames groups, AuthorizationInfo authzInfo) + { + StringBuilder buffer = new StringBuilder("authorization summary: "); + + buffer.append(SEPARATOR).append("username : ").append(user.getName()); + buffer.append(SEPARATOR).append("groups : "); + append(buffer, groups); + buffer.append(SEPARATOR).append("roles : "); + append(buffer, authzInfo.getRoles()); + buffer.append(SEPARATOR).append("permissions:"); + append(buffer, authzInfo.getStringPermissions()); + append(buffer, authzInfo.getObjectPermissions()); + + return buffer.toString(); + } + + private void append(StringBuilder buffer, Iterable iterable){ + if (iterable != null){ + for ( Object item : iterable ) + { + buffer.append(SEPARATOR).append(" - ").append(item); + } + } + } //~--- get methods ---------------------------------------------------------- - private boolean isUserPermission(User user, GroupNames groups, + private boolean isUserPermitted(User user, GroupNames groups, PermissionObject perm) { //J- - return (perm.isGroupPermission() && groups.contains(perm.getName())) + return (perm.isGroupPermission() && groups.contains(perm.getName())) || ((!perm.isGroupPermission()) && user.getName().equals(perm.getName())); //J+ } @@ -443,7 +543,6 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector private final String username; } - //~--- fields --------------------------------------------------------------- /** authorization cache */ diff --git a/scm-webapp/src/main/java/sonia/scm/security/XsrfCookies.java b/scm-webapp/src/main/java/sonia/scm/security/XsrfCookies.java new file mode 100644 index 0000000000..de3c26995a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/XsrfCookies.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.security; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Util methods to handle XsrfCookies. + * + * @author Sebastian Sdorra + * @version 1.47 + */ +public final class XsrfCookies +{ + + private XsrfCookies() + { + } + + /** + * Creates a new xsrf protection cookie and add it to the response. + * + * @param request http servlet request + * @param response http servlet response + * @param token xsrf token + */ + public static void create(HttpServletRequest request, HttpServletResponse response, String token){ + applyCookie(request, response, new Cookie(XsrfProtectionFilter.KEY, token)); + + } + + /** + * Removes the current xsrf protection cookie from response. + * + * @param request http servlet request + * @param response http servlet response + */ + public static void remove(HttpServletRequest request, HttpServletResponse response) + { + Cookie[] cookies = request.getCookies(); + if ( cookies != null ){ + for ( Cookie c : cookies ){ + if ( XsrfProtectionFilter.KEY.equals(c.getName()) ){ + c.setMaxAge(0); + c.setValue(null); + applyCookie(request, response, c); + } + } + } + } + + private static void applyCookie(HttpServletRequest request, HttpServletResponse response, Cookie cookie){ + cookie.setPath(request.getContextPath()); + response.addCookie(cookie); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/XsrfProtectionFilter.java b/scm-webapp/src/main/java/sonia/scm/security/XsrfProtectionFilter.java new file mode 100644 index 0000000000..7d1a12f9f1 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/XsrfProtectionFilter.java @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.security; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.inject.Inject; +import java.io.IOException; +import java.util.UUID; +import javax.inject.Singleton; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.Priority; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.filter.Filters; +import sonia.scm.filter.WebElement; +import sonia.scm.util.HttpUtil; +import sonia.scm.web.filter.HttpFilter; + +/** + * Xsrf protection http filter. The filter will issue an cookie with an xsrf protection token on the first ajax request + * of the scm web interface and marks the http session as xsrf protected. On every other request within a protected + * session, the web interface has to send the token from the cookie as http header on every request. If the filter + * receives an request to a protected session, without proper xsrf header the filter will abort the request and send an + * http error code back to the client. If the filter receives an request to a non protected session, from a non web + * interface client the filter will call the chain. The {@link XsrfProtectionFilter} is disabled by default and can be + * enabled with {@link ScmConfiguration#setEnabledXsrfProtection(boolean)}. + * + * TODO for scm-manager 2 we have to store the csrf token as part of the jwt token instead of session. + * + * @see https://bitbucket.org/sdorra/scm-manager/issues/793/json-hijacking-vulnerability-cwe-116-cwe + * @author Sebastian Sdorra + * @version 1.47 + */ +@WebElement(Filters.PATTERN_RESTAPI) +@Priority(Filters.PRIORITY_PRE_BASEURL) +public final class XsrfProtectionFilter extends HttpFilter +{ + + /** + * the logger for XsrfProtectionFilter + */ + private static final Logger logger = LoggerFactory.getLogger(XsrfProtectionFilter.class); + + /** + * Key used for session, header and cookie. + */ + static final String KEY = "X-XSRF-Token"; + + @Inject + public XsrfProtectionFilter(ScmConfiguration configuration) + { + this.configuration = configuration; + } + + @Override + protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws + IOException, ServletException + { + if (configuration.isEnabledXsrfProtection()) + { + doXsrfProtection(request, response, chain); + } + else + { + logger.trace("xsrf protection is disabled, skipping check"); + chain.doFilter(request, response); + } + } + + private void doXsrfProtection(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws + IOException, ServletException + { + HttpSession session = request.getSession(true); + String storedToken = (String) session.getAttribute(KEY); + if ( ! Strings.isNullOrEmpty(storedToken) ){ + String headerToken = request.getHeader(KEY); + if ( storedToken.equals(headerToken) ){ + logger.trace("received valid xsrf protected request"); + chain.doFilter(request, response); + } else { + // is forbidden the correct status code? + logger.warn("received request to a xsrf protected session without proper xsrf token"); + response.sendError(HttpServletResponse.SC_FORBIDDEN); + } + } else if (HttpUtil.isWUIRequest(request)) { + logger.debug("received wui request, mark session as xsrf protected and issue a new token"); + String token = createToken(); + session.setAttribute(KEY, token); + XsrfCookies.create(request, response, token); + chain.doFilter(request, response); + } else { + // handle non webinterface clients, which does not need xsrf protection + logger.trace("received request to a non xsrf protected session"); + chain.doFilter(request, response); + } + } + + private String createToken() + { + // TODO create interface and use a better method + return UUID.randomUUID().toString(); + } + + private ScmConfiguration configuration; +} diff --git a/scm-webapp/src/main/java/sonia/scm/template/ErrorServlet.java b/scm-webapp/src/main/java/sonia/scm/template/ErrorServlet.java index 8604324769..4d62b823a1 100644 --- a/scm-webapp/src/main/java/sonia/scm/template/ErrorServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/template/ErrorServlet.java @@ -184,8 +184,8 @@ public class ErrorServlet extends HttpServlet //~--- fields --------------------------------------------------------------- /** Field description */ - private SCMContextProvider context; + private final SCMContextProvider context; /** Field description */ - private TemplateEngineFactory templateEngineFactory; + private final TemplateEngineFactory templateEngineFactory; } diff --git a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java index 89048efeaa..99e3abbe76 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java +++ b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java @@ -237,15 +237,14 @@ public class DefaultUserManager extends AbstractUserManager } UserPermissions.modify(user).check(); - User oldUser = userDAO.get(name); - - if (oldUser != null) + User notModified = userDAO.get(name); + if (notModified != null) { AssertUtil.assertIsValid(user); + fireEvent(HandlerEventType.BEFORE_MODIFY, user, notModified); user.setLastModified(System.currentTimeMillis()); - fireEvent(HandlerEventType.BEFORE_MODIFY, user, oldUser); userDAO.modify(user); - fireEvent(HandlerEventType.MODIFY, user, oldUser); + fireEvent(HandlerEventType.MODIFY, user, notModified); } else { diff --git a/scm-webapp/src/main/java/sonia/scm/web/BasicWebTokenGenerator.java b/scm-webapp/src/main/java/sonia/scm/web/BasicWebTokenGenerator.java index 80aee62b36..5f47ba371f 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/BasicWebTokenGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/web/BasicWebTokenGenerator.java @@ -33,6 +33,12 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.base.Charsets; +import com.google.inject.Inject; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; + import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.codec.Base64; @@ -59,15 +65,28 @@ public class BasicWebTokenGenerator extends SchemeBasedWebTokenGenerator { /** credential separator for basic authentication */ - public static final String CREDENTIAL_SEPARATOR = ":"; + private static final String CREDENTIAL_SEPARATOR = ":"; + /** default encoding to decode basic authentication header */ + private static final Charset DEFAULT_ENCODING = Charsets.ISO_8859_1; + /** * the logger for BasicWebTokenGenerator */ private static final Logger logger = LoggerFactory.getLogger(BasicWebTokenGenerator.class); - //~--- methods -------------------------------------------------------------- + private final UserAgentParser userAgentParser; + + /** + * Constructs a new BasicWebTokenGenerator. + * + * @param userAgentParser parser for user-agent header + */ + @Inject + public BasicWebTokenGenerator(UserAgentParser userAgentParser) { + this.userAgentParser = userAgentParser; + } /** * Creates a {@link UsernamePasswordToken} from an authorization header with @@ -88,7 +107,7 @@ public class BasicWebTokenGenerator extends SchemeBasedWebTokenGenerator if (HttpUtil.AUTHORIZATION_SCHEME_BASIC.equalsIgnoreCase(scheme)) { - String token = new String(Base64.decode(authorization.getBytes())); + String token = decodeAuthenticationHeader(request, authorization); int index = token.indexOf(CREDENTIAL_SEPARATOR); @@ -115,4 +134,32 @@ public class BasicWebTokenGenerator extends SchemeBasedWebTokenGenerator return authToken; } + +/** + * Decode base64 of the basic authentication header. The method will use + * the charset provided by the {@link UserAgent}, if the + * {@link UserAgentParser} is not available the method will be fall back to + * ISO-8859-1. + * + * @param request http request + * @param authentication base64 encoded basic authentication string + * + * @return decoded basic authentication header + * + * @see issue 627 + * @see Stackoverflow Basic Authentication + * + * @throws UnsupportedEncodingException + */ + private String decodeAuthenticationHeader(HttpServletRequest request, String authentication) + { + Charset encoding = DEFAULT_ENCODING; + + if (userAgentParser != null) + { + encoding = userAgentParser.parse(request).getBasicAuthenticationCharset(); + } + + return new String(Base64.decode(authentication), encoding); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/web/BrowserUserAgentProvider.java b/scm-webapp/src/main/java/sonia/scm/web/BrowserUserAgentProvider.java new file mode 100644 index 0000000000..edf1f14ee3 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/web/BrowserUserAgentProvider.java @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.web; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Charsets; + +import sonia.scm.plugin.Extension; + +/** + * + * @author Sebastian Sdorra + */ +@Extension +public class BrowserUserAgentProvider implements UserAgentProvider +{ + + /** Field description */ + @VisibleForTesting + static final UserAgent CHROME = UserAgent.builder( + "Chrome").basicAuthenticationCharset( + Charsets.UTF_8).build(); + + /** Field description */ + private static final String CHROME_PATTERN = "chrome"; + + /** Field description */ + @VisibleForTesting + static final UserAgent FIREFOX = UserAgent.builder("Firefox").build(); + + /** Field description */ + private static final String FIREFOX_PATTERN = "firefox"; + + /** Field description */ + @VisibleForTesting + static final UserAgent MSIE = UserAgent.builder("Internet Explorer").build(); + + /** Field description */ + private static final String MSIE_PATTERN = "msie"; + + /** Field description */ + @VisibleForTesting // todo check charset + static final UserAgent SAFARI = UserAgent.builder("Safari").build(); + + /** Field description */ + private static final String OPERA_PATTERN = "opera"; + + /** Field description */ + private static final String SAFARI_PATTERN = "safari"; + + /** Field description */ + @VisibleForTesting // todo check charset + static final UserAgent OPERA = UserAgent.builder( + "Opera").basicAuthenticationCharset( + Charsets.UTF_8).build(); + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @param userAgentString + * + * @return + */ + @Override + public UserAgent parseUserAgent(String userAgentString) + { + UserAgent ua = null; + + if (userAgentString.contains(CHROME_PATTERN)) + { + ua = CHROME; + } + else if (userAgentString.contains(FIREFOX_PATTERN)) + { + ua = FIREFOX; + } + else if (userAgentString.contains(OPERA_PATTERN)) + { + ua = OPERA; + } + else if (userAgentString.contains(MSIE_PATTERN)) + { + ua = MSIE; + } + else if (userAgentString.contains(SAFARI_PATTERN)) + { + ua = SAFARI; + } + + return ua; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/web/cgi/DefaultCGIExecutor.java b/scm-webapp/src/main/java/sonia/scm/web/cgi/DefaultCGIExecutor.java index 52eead7956..3eaa684080 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/cgi/DefaultCGIExecutor.java +++ b/scm-webapp/src/main/java/sonia/scm/web/cgi/DefaultCGIExecutor.java @@ -58,6 +58,7 @@ import java.io.OutputStream; import java.util.Enumeration; import java.util.Map; +import java.util.concurrent.ExecutorService; import javax.servlet.ServletContext; import javax.servlet.ServletInputStream; @@ -94,15 +95,17 @@ public class DefaultCGIExecutor extends AbstractCGIExecutor * Constructs ... * * + * @param executor to handle error stream processing * @param configuration * @param context * @param request * @param response */ - public DefaultCGIExecutor(ScmConfiguration configuration, - ServletContext context, HttpServletRequest request, - HttpServletResponse response) + public DefaultCGIExecutor(ExecutorService executor, + ScmConfiguration configuration, ServletContext context, + HttpServletRequest request, HttpServletResponse response) { + this.executor = executor; this.configuration = configuration; this.context = context; this.request = request; @@ -190,7 +193,7 @@ public class DefaultCGIExecutor extends AbstractCGIExecutor p = Runtime.getRuntime().exec(execCmd, env.getEnvArray(), workDirectory); execute(p); } - catch (Throwable ex) + catch (IOException ex) { getExceptionHandler().handleException(request, response, ex); } @@ -507,7 +510,7 @@ public class DefaultCGIExecutor extends AbstractCGIExecutor */ private void processErrorStreamAsync(final Process process) { - new Thread(new Runnable() + executor.execute(new Runnable() { @Override public void run() @@ -528,7 +531,7 @@ public class DefaultCGIExecutor extends AbstractCGIExecutor IOUtil.close(errorStream); } } - }).start(); + }); } /** @@ -539,6 +542,8 @@ public class DefaultCGIExecutor extends AbstractCGIExecutor */ private void processServletInput(Process process) { + logger.trace("process servlet input"); + OutputStream processOS = null; ServletInputStream servletIS = null; @@ -637,6 +642,9 @@ public class DefaultCGIExecutor extends AbstractCGIExecutor //~--- fields --------------------------------------------------------------- + /** executor to handle error stream processing */ + private final ExecutorService executor; + /** Field description */ private ScmConfiguration configuration; diff --git a/scm-webapp/src/main/java/sonia/scm/web/cgi/DefaultCGIExecutorFactory.java b/scm-webapp/src/main/java/sonia/scm/web/cgi/DefaultCGIExecutorFactory.java index eaa89224e2..8da159f3c0 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/cgi/DefaultCGIExecutorFactory.java +++ b/scm-webapp/src/main/java/sonia/scm/web/cgi/DefaultCGIExecutorFactory.java @@ -35,10 +35,15 @@ package sonia.scm.web.cgi; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.util.concurrent.ThreadFactoryBuilder; + import sonia.scm.config.ScmConfiguration; //~--- JDK imports ------------------------------------------------------------ +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -50,6 +55,21 @@ import javax.servlet.http.HttpServletResponse; public class DefaultCGIExecutorFactory implements CGIExecutorFactory { + /** + * Constructs ... + * + */ + public DefaultCGIExecutorFactory() + { + //J- + this.executor = Executors.newCachedThreadPool( + new ThreadFactoryBuilder().setNameFormat("cgi-pool-%d").build() + ); + //J+ + } + + //~--- methods -------------------------------------------------------------- + /** * Method description * @@ -63,10 +83,15 @@ public class DefaultCGIExecutorFactory implements CGIExecutorFactory */ @Override public CGIExecutor createExecutor(ScmConfiguration configuration, - ServletContext context, - HttpServletRequest request, - HttpServletResponse response) + ServletContext context, HttpServletRequest request, + HttpServletResponse response) { - return new DefaultCGIExecutor(configuration, context, request, response); + return new DefaultCGIExecutor(executor, configuration, context, request, + response); } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + private final ExecutorService executor; } diff --git a/scm-webapp/src/main/resources/logback.default.xml b/scm-webapp/src/main/resources/logback.default.xml index 6ddedcdfad..939ff92b37 100644 --- a/scm-webapp/src/main/resources/logback.default.xml +++ b/scm-webapp/src/main/resources/logback.default.xml @@ -52,12 +52,19 @@ + + + + + + + diff --git a/scm-webapp/src/main/webapp/index.mustache b/scm-webapp/src/main/webapp/index.mustache index 4c2c3c913c..fec01e96ca 100644 --- a/scm-webapp/src/main/webapp/index.mustache +++ b/scm-webapp/src/main/webapp/index.mustache @@ -115,6 +115,8 @@ + + diff --git a/scm-webapp/src/main/webapp/resources/css/style.css b/scm-webapp/src/main/webapp/resources/css/style.css index 355e8afcbe..deb968f35e 100644 --- a/scm-webapp/src/main/webapp/resources/css/style.css +++ b/scm-webapp/src/main/webapp/resources/css/style.css @@ -153,7 +153,7 @@ a.scm-link:hover { } .scm-commit { - margin-bottom: 50px; + margin-bottom: 65px; } .scm-commit h1 { diff --git a/scm-webapp/src/main/webapp/resources/js/config/sonia.config.scmconfigpanel.js b/scm-webapp/src/main/webapp/resources/js/config/sonia.config.scmconfigpanel.js index 29efa3fa00..a985688a3e 100644 --- a/scm-webapp/src/main/webapp/resources/js/config/sonia.config.scmconfigpanel.js +++ b/scm-webapp/src/main/webapp/resources/js/config/sonia.config.scmconfigpanel.js @@ -102,6 +102,10 @@ Sonia.config.ScmConfigPanel = Ext.extend(Sonia.config.ConfigPanel,{ forceBaseUrlHelpText: 'Redirects to the base url if the request comes from a other url', disableGroupingGridHelpText: 'Disable repository Groups. A complete page reload is required after a change of this value.', enableRepositoryArchiveHelpText: 'Enable repository archives. A complete page reload is required after a change of this value.', + + // TODO i18n + enableXsrfProtectionText: 'Enable Xsrf Protection', + enableXsrfProtectionHelpText: 'Enable Xsrf Cookie Protection. Note: This feature is still experimental.', initComponent: function(){ @@ -166,6 +170,12 @@ Sonia.config.ScmConfigPanel = Ext.extend(Sonia.config.ConfigPanel,{ name: 'skip-failed-authenticators', inputValue: 'true', helpText: this.skipFailedAuthenticatorsHelpText + },{ + xtype: 'checkbox', + fieldLabel: this.enableXsrfProtectionText, + name: 'xsrf-protection', + inputValue: 'true', + helpText: this.enableXsrfProtectionHelpText },{ xtype: 'numberfield', fieldLabel: this.loginAttemptLimitText, diff --git a/scm-webapp/src/main/webapp/resources/js/i18n/de.js b/scm-webapp/src/main/webapp/resources/js/i18n/de.js index 91a76c7685..60597a2b89 100644 --- a/scm-webapp/src/main/webapp/resources/js/i18n/de.js +++ b/scm-webapp/src/main/webapp/resources/js/i18n/de.js @@ -221,7 +221,8 @@ if (Sonia.repository.Panel){ emptyText: 'Es wurde kein Repository selektiert', removeTitleText: 'Repository entfernen', - removeMsgText: 'Repository entfernen "{0}"?', + removeMsgText: 'Diese Aktion wird das Repository "{0}" permanent von der \n\ + Festplatte löschen?
Repository entfernen?', errorTitleText: 'Fehler', errorMsgText: 'Repository entfernen fehlgeschlagen' }); diff --git a/scm-webapp/src/main/webapp/resources/js/override/ext.form.field.js b/scm-webapp/src/main/webapp/resources/js/override/ext.form.field.js index 61abb54b88..2a66bfe8b0 100644 --- a/scm-webapp/src/main/webapp/resources/js/override/ext.form.field.js +++ b/scm-webapp/src/main/webapp/resources/js/override/ext.form.field.js @@ -71,6 +71,8 @@ Ext.override(Ext.form.Field, { switch ( this.getXType() ){ case 'combo': + case 'repositoryBranchComboBox': + case 'repositoryTagComboBox': if ( this.readOnly ){ cls = 'scm-form-help-button'; } else { diff --git a/scm-webapp/src/main/webapp/resources/js/panel/sonia.panel.syntaxhighlighterpanel.js b/scm-webapp/src/main/webapp/resources/js/panel/sonia.panel.syntaxhighlighterpanel.js index 8dcc40b176..fab21ac806 100644 --- a/scm-webapp/src/main/webapp/resources/js/panel/sonia.panel.syntaxhighlighterpanel.js +++ b/scm-webapp/src/main/webapp/resources/js/panel/sonia.panel.syntaxhighlighterpanel.js @@ -41,6 +41,7 @@ Sonia.panel.SyntaxHighlighterPanel = Ext.extend(Ext.Panel, { fileName: 'shBrushAppleScript.js' },{ name: 'Bash/shell', + brush: 'shell', aliases: ['sh', 'bash', 'shell'], fileName: 'shBrushBash.js' },{ @@ -49,6 +50,7 @@ Sonia.panel.SyntaxHighlighterPanel = Ext.extend(Ext.Panel, { fileName: 'shBrushColdFusion.js' },{ name: 'C#', + brush: 'csharp', aliases: ['cs', 'c-sharp', 'csharp'], fileName: 'shBrushCSharp.js' },{ @@ -97,6 +99,7 @@ Sonia.panel.SyntaxHighlighterPanel = Ext.extend(Ext.Panel, { fileName: 'shBrushPhp.js' },{ name: 'Plain Text', + brush: 'plain', aliases: ['plain', 'text', 'txt', 'log'], fileName: 'shBrushPlain.js' },{ @@ -160,6 +163,9 @@ Sonia.panel.SyntaxHighlighterPanel = Ext.extend(Ext.Panel, { for ( var j=0;j', 'Branches:', ' ',{ - xtype: 'combo', - valueField: 'name', - displayField: 'name', - typeAhead: false, - editable: false, - triggerAction: 'all', - store: branchStore, + xtype: 'repositoryBranchComboBox', + repositoryId: this.repository.id, listeners: { select: { fn: this.selectBranch, diff --git a/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.commitpanel.js b/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.commitpanel.js index b711a477d3..7b72c4fca3 100644 --- a/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.commitpanel.js +++ b/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.commitpanel.js @@ -73,10 +73,16 @@ Sonia.repository.CommitPanel = Ext.extend(Ext.Panel, { ', templateModifications: '
    \n\ -
  • {.}
  • \n\ -
  • {.}
  • \n\ -
  • {.}
  • \n\ -
', + \n\ +
  • {.}
  • \n\ +
    \n\ + \n\ +
  • {.}
  • \n\ +
    \n\ + \n\ +
  • {.}
  • \n\ +
    \n\ + ', // header panel commitPanel: null, @@ -84,7 +90,15 @@ Sonia.repository.CommitPanel = Ext.extend(Ext.Panel, { initComponent: function(){ this.commitPanel = new Ext.Panel({ - tpl: new Ext.XTemplate(this.templateCommit + this.templateModifications) + tpl: new Ext.XTemplate(this.templateCommit + this.templateModifications), + listeners: { + render: { + fn: function(panel){ + panel.body.on('click', this.onClick, this); + }, + scope: this + } + } }); this.diffPanel = new Sonia.panel.SyntaxHighlighterPanel({ @@ -132,6 +146,20 @@ Sonia.repository.CommitPanel = Ext.extend(Ext.Panel, { ); } }); + }, + + onClick: function(e){ + var el = e.getTarget('a.scm-link'); + if (el){ + var path = el.rel; + if (path){ + this.openFile(path); + } + } + }, + + openFile: function(path){ + Sonia.repository.openFile(this.repository, path, this.revision); } }); diff --git a/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.formpanel.js b/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.formpanel.js index d717a77daf..66482399ac 100644 --- a/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.formpanel.js +++ b/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.formpanel.js @@ -61,8 +61,15 @@ Sonia.repository.FormPanel = Ext.extend(Sonia.rest.FormPanel,{ Sonia.repository.FormPanel.superclass.initComponent.apply(this, arguments); }, + prepareUpdate: function(item) { + + }, + update: function(item){ item = Ext.apply( this.item, item ); + // allow plugins to modify item + this.prepareUpdate(item); + if ( debug ){ console.debug( 'update repository: ' + item.name ); } diff --git a/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.grid.js b/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.grid.js index 0003de58b0..fb66b91a5a 100644 --- a/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.grid.js +++ b/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.grid.js @@ -82,7 +82,8 @@ Sonia.repository.Grid = Ext.extend(Sonia.rest.Grid, { name: 'group', convert: this.convertToGroup },{ - name: 'name' + name: 'name', + sortType:'asUCString' },{ name: 'type' },{ @@ -431,21 +432,22 @@ Sonia.repository.Grid = Ext.extend(Sonia.rest.Grid, { var infoPanel = main.getInfoPanel(item.type); infoPanel.item = item; + var settingsForm = main.getSettingsForm(item.type); + settingsForm.item = item; + settingsForm.onUpdate = { + fn: this.reload, + scope: this + }; + settingsForm.onCreate = { + fn: this.reload, + scope: this + }; + var panels = [infoPanel]; if ( owner ){ - panels.push({ - item: item, - xtype: 'repositorySettingsForm', - onUpdate: { - fn: this.reload, - scope: this - }, - onCreate: { - fn: this.reload, - scope: this - } - },{ + panels.push( + settingsForm, { item: item, xtype: 'repositoryPermissionsForm', listeners: { @@ -481,8 +483,34 @@ Sonia.repository.Grid = Ext.extend(Sonia.rest.Grid, { listener.call(listener.scope, item, panels); } }); + + // get the xtype of the currently opened tab + // search for the tab with the same xtype and activate it + // see issue https://goo.gl/3RGnA3 + var activeTab = 0; + var activeXtype = this.getActiveTabXtype(); + if (activeXtype){ + for (var i=0; iDo you want to proceed?', errorTitleText: 'Error', errorMsgText: 'Repository deletion failed', @@ -255,12 +256,12 @@ Sonia.repository.Panel = Ext.extend(Sonia.rest.Panel, { return repository; }, - executeRemoteCall: function(title, message, method, url, data, failureCallback){ + executeRemoteCall: function(title, message, method, url, data, failureCallback, icon){ Ext.MessageBox.show({ title: title, msg: message, buttons: Ext.MessageBox.OKCANCEL, - icon: Ext.MessageBox.QUESTION, + icon: icon ? icon : Ext.MessageBox.QUESTION, fn: function(result){ if ( result === 'ok' ){ @@ -339,7 +340,8 @@ Sonia.repository.Panel = Ext.extend(Sonia.rest.Panel, { this.errorTitleText, this.errorMsgText ); - } + }, + Ext.MessageBox.WARNING ); } }, diff --git a/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.repositorybrowser.js b/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.repositorybrowser.js index 25d40bbc9e..489dd3c06c 100644 --- a/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.repositorybrowser.js +++ b/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.repositorybrowser.js @@ -154,27 +154,37 @@ Sonia.repository.RepositoryBrowser = Ext.extend(Ext.grid.GridPanel, { var items = [this.repository.name]; var type = Sonia.repository.getTypeByName( this.repository.type ); + var branches = false; + if (type && type.supportedCommands && type.supportedCommands.indexOf('BRANCHES') >= 0){ + + branches = true; + + items.push('->','Branches:', ' ',{ + id: 'branchComboBox', + xtype: 'repositoryBranchComboBox', + repositoryId: this.repository.id, + listeners: { + select: { + fn: this.selectBranch, + scope: this + } + } + }); + + } + if ( type && type.supportedCommands && type.supportedCommands.indexOf('TAGS') >= 0){ - var tagStore = new Sonia.rest.JsonStore({ - proxy: new Ext.data.HttpProxy({ - url: restUrl + 'repositories/' + this.repository.id + '/tags.json', - method: 'GET', - disableCaching: false - }), - root: 'tag', - idProperty: 'name', - fields: [ 'name', 'revision' ] - }); + if (branches){ + items.push(' '); + } else { + items.push('->'); + } - items.push('->','Tags:', ' ',{ - xtype: 'combo', - valueField: 'revision', - displayField: 'name', - typeAhead: false, - editable: false, - triggerAction: 'all', - store: tagStore, + items.push('Tags:', ' ',{ + id: 'tagComboBox', + xtype: 'repositoryTagComboBox', + repositoryId: this.repository.id, listeners: { select: { fn: this.selectTag, @@ -193,10 +203,18 @@ Sonia.repository.RepositoryBrowser = Ext.extend(Ext.grid.GridPanel, { return tbar; }, + selectBranch: function(combo, rec){ + this.selectRev(rec, Ext.getCmp('tagComboBox')); + }, + selectTag: function(combo, rec){ + this.selectRev(rec, Ext.getCmp('branchComboBox')); + }, + + selectRev: function(rec, comboToClear){ var tag = rec.get('name'); if (debug){ - console.debug('select tag ' + tag); + console.debug('select rev ' + tag); } this.revision = rec.get('revision'); this.getStore().load({ @@ -208,6 +226,11 @@ Sonia.repository.RepositoryBrowser = Ext.extend(Ext.grid.GridPanel, { this.reRenderBottomBar(this.path); this.updateHistory(); + + // clear other combobox + if (comboToClear){ + comboToClear.clearValue(); + } }, loadStore: function(store, records, extra){ @@ -299,25 +322,7 @@ Sonia.repository.RepositoryBrowser = Ext.extend(Ext.grid.GridPanel, { }, openFile: function(path){ - if ( debug ){ - console.debug( 'open file: ' + path ); - } - - var id = Sonia.repository.createContentId( - this.repository, - path, - this.revision - ); - - main.addTab({ - id: id, - path: path, - revision: this.revision, - repository: this.repository, - xtype: 'contentPanel', - closable: true, - autoScroll: true - }); + Sonia.repository.openFile(this.repository, path, this.revision); }, changeDirectory: function(path){ @@ -374,7 +379,12 @@ Sonia.repository.RepositoryBrowser = Ext.extend(Ext.grid.GridPanel, { var bbar = this.getBottomToolbar(); bbar.removeAll(); - var parts = path.split('/'); + var parts; + if (path){ + parts = path.split('/'); + } else { + parts = []; + } var currentPath = ''; var items = [this.createFolderButton(currentPath, '')]; diff --git a/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.settingsformpanel.js b/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.settingsformpanel.js index 5c6b902756..2b182a5de2 100644 --- a/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.settingsformpanel.js +++ b/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.settingsformpanel.js @@ -78,9 +78,15 @@ Sonia.repository.SettingsFormPanel = Ext.extend(Sonia.repository.FormPanel, { helpText: this.publicHelpText }] }; + + this.modifyDefaultConfig(config); Ext.apply(this, Ext.apply(this.initialConfig, config)); Sonia.repository.SettingsFormPanel.superclass.initComponent.apply(this, arguments); + }, + + modifyDefaultConfig: function(config){ + } }); diff --git a/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.tagcombobox.js b/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.tagcombobox.js new file mode 100644 index 0000000000..3f045a8cf6 --- /dev/null +++ b/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.tagcombobox.js @@ -0,0 +1,65 @@ +/* * + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + +Sonia.repository.TagComboBox = Ext.extend(Ext.form.ComboBox, { + + repositoryId: null, + + initComponent: function(){ + var tagStore = new Sonia.rest.JsonStore({ + proxy: new Ext.data.HttpProxy({ + url: restUrl + 'repositories/' + this.repositoryId + '/tags.json', + method: 'GET', + disableCaching: false + }), + root: 'tag', + idProperty: 'name', + fields: [ 'name', 'revision' ] + }); + + var config = { + valueField: 'revision', + displayField: 'name', + typeAhead: false, + editable: false, + triggerAction: 'all', + store: tagStore + }; + + Ext.apply(this, Ext.apply(this.initialConfig, config)); + Sonia.repository.TagComboBox.superclass.initComponent.apply(this, arguments); + } + +}); + +// register xtype +Ext.reg('repositoryTagComboBox', Sonia.repository.TagComboBox); \ No newline at end of file diff --git a/scm-webapp/src/main/webapp/resources/js/sonia.global.js b/scm-webapp/src/main/webapp/resources/js/sonia.global.js index 32dec370a9..a43feb824b 100644 --- a/scm-webapp/src/main/webapp/resources/js/sonia.global.js +++ b/scm-webapp/src/main/webapp/resources/js/sonia.global.js @@ -37,6 +37,17 @@ Ext.Ajax.defaultHeaders = { 'X-SCM-Client': 'WUI' }; +// XSRF protection +Ext.Ajax.on('beforerequest', function(conn, options){ + var token = Ext.util.Cookies.get('X-XSRF-Token'); + if (token){ + if (!options.headers){ + options.headers = {}; + } + options.headers['X-XSRF-Token'] = token; + } +}); + var state = null; var admin = false; diff --git a/scm-webapp/src/main/webapp/resources/js/sonia.scm.js b/scm-webapp/src/main/webapp/resources/js/sonia.scm.js index 4e54c3af09..6d7e6a3257 100644 --- a/scm-webapp/src/main/webapp/resources/js/sonia.scm.js +++ b/scm-webapp/src/main/webapp/resources/js/sonia.scm.js @@ -78,6 +78,7 @@ Sonia.scm.Main = Ext.extend(Ext.util.Observable, { mainTabPanel: null, infoPanels: [], + settingsForm: [], scripts: [], stylesheets: [], @@ -96,6 +97,23 @@ Sonia.scm.Main = Ext.extend(Ext.util.Observable, { this.infoPanels[type] = panel; }, + registerSettingsForm: function(type, form){ + this.settingsForm[type] = form; + }, + + getSettingsForm: function(type){ + var rp = null; + var panel = this.settingsForm[type]; + if ( ! panel ){ + rp = { + xtype: 'repositorySettingsForm' + }; + } else { + rp = Sonia.util.clone( panel ); + } + return rp; + }, + getInfoPanel: function(type){ var rp = null; var panel = this.infoPanels[type]; diff --git a/scm-webapp/src/test/java/sonia/scm/filter/AdminSecurityFilterTest.java b/scm-webapp/src/test/java/sonia/scm/filter/AdminSecurityFilterTest.java new file mode 100644 index 0000000000..b6a9c02a8d --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/filter/AdminSecurityFilterTest.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.filter; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.apache.shiro.SecurityUtils; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Rule; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.config.ScmConfiguration; + +/** + * Unit tests for {@link AdminSecurityFilter}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini") +public class AdminSecurityFilterTest { + + private AdminSecurityFilter securityFilter; + + @Rule + public ShiroRule shiro = new ShiroRule(); + + /** + * Prepare object under test and mocks. + */ + @Before + public void setUp(){ + this.securityFilter = new AdminSecurityFilter(new ScmConfiguration()); + } + + /** + * Tests {@link AdminSecurityFilter#hasPermission(org.apache.shiro.subject.Subject)} as administrator. + */ + @Test + @SubjectAware(username = "dent", password = "secret") + public void testHasPermissionAsAdministrator() { + assertTrue(securityFilter.hasPermission(SecurityUtils.getSubject())); + } + + /** + * Tests {@link AdminSecurityFilter#hasPermission(org.apache.shiro.subject.Subject)} as user. + */ + @Test + @SubjectAware(username = "trillian", password = "secret") + public void testHasPermissionAsUser() { + assertFalse(securityFilter.hasPermission(SecurityUtils.getSubject())); + } + +} \ No newline at end of file diff --git a/scm-webapp/src/test/java/sonia/scm/filter/MDCFilterTest.java b/scm-webapp/src/test/java/sonia/scm/filter/MDCFilterTest.java new file mode 100644 index 0000000000..010c9bc1cf --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/filter/MDCFilterTest.java @@ -0,0 +1,130 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.filter; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import java.io.IOException; +import java.util.Map; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import org.junit.Rule; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.slf4j.MDC; +import sonia.scm.AbstractTestBase; + +/** + * Unit tests for {@link MDCFilter}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class MDCFilterTest extends AbstractTestBase { + + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + private final MDCFilter filter = new MDCFilter(); + + /** + * Tests {@link MDCFilter#doFilter(HttpServletRequest, HttpServletResponse, FilterChain)}. + * + * @throws IOException + * @throws ServletException + */ + @Test + @SubjectAware( + username = "trillian", + password = "secret", + configuration = "classpath:sonia/scm/shiro-001.ini" + ) + public void testDoFilter() throws IOException, ServletException + { + when(request.getRequestURI()).thenReturn("api/v1/repositories"); + when(request.getRemoteAddr()).thenReturn("127.0.0.1"); + when(request.getRemoteHost()).thenReturn("localhost"); + when(request.getMethod()).thenReturn("GET"); + + MDCCapturingFilterChain chain = new MDCCapturingFilterChain(); + filter.doFilter(request, response, chain); + + assertNotNull(chain.ctx); + assertEquals("trillian", chain.ctx.get(MDCFilter.MDC_USERNAME)); + assertEquals("api/v1/repositories", chain.ctx.get(MDCFilter.MDC_REQUEST_URI)); + assertEquals("127.0.0.1", chain.ctx.get(MDCFilter.MDC_CLIEN_IP)); + assertEquals("localhost", chain.ctx.get(MDCFilter.MDC_CLIEN_HOST)); + assertEquals("GET", chain.ctx.get(MDCFilter.MDC_REQUEST_METHOD)); + } + + /** + * Tests {@link MDCFilter#doFilter(HttpServletRequest, HttpServletResponse, FilterChain)} as anonymous user. + * + * @throws IOException + * @throws ServletException + */ + @Test + @SubjectAware + public void testDoFilterAsAnonymous() throws IOException, ServletException { + MDCCapturingFilterChain chain = new MDCCapturingFilterChain(); + filter.doFilter(request, response, chain); + + assertNotNull(chain.ctx); + assertEquals("anonymous", chain.ctx.get(MDCFilter.MDC_USERNAME)); + } + + private static class MDCCapturingFilterChain implements FilterChain { + + private Map ctx; + + @Override + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + this.ctx = MDC.getCopyOfContextMap(); + } + + } + +} \ No newline at end of file diff --git a/scm-webapp/src/test/java/sonia/scm/filter/SecurityFilterTest.java b/scm-webapp/src/test/java/sonia/scm/filter/SecurityFilterTest.java new file mode 100644 index 0000000000..13333cc223 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/filter/SecurityFilterTest.java @@ -0,0 +1,238 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.filter; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.shiro.subject.SimplePrincipalCollection; +import org.apache.shiro.subject.Subject; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.hamcrest.Matchers.*; +import org.junit.Before; +import org.junit.Rule; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Mockito; +import static org.mockito.Mockito.*; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.SCMContext; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.user.User; +import sonia.scm.user.UserTestData; + +/** + * Unit tests for {@link SecurityFilter}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini") +public class SecurityFilterTest { + + @Mock + private HttpServletRequest request; + + @Captor + private ArgumentCaptor requestCaptor; + + @Mock + private HttpServletResponse response; + + @Captor + private ArgumentCaptor responseCaptor; + + @Mock + private FilterChain chain; + + private ScmConfiguration configuration; + + private SecurityFilter securityFilter; + + @Rule + public ShiroRule shiro = new ShiroRule(); + + /** + * Prepare object under test and mocks. + */ + @Before + public void setUp(){ + this.configuration = new ScmConfiguration(); + this.securityFilter = new SecurityFilter(configuration); + + when(request.getContextPath()).thenReturn("/scm"); + } + + /** + * Tests filter on authentication endpoint. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoOnAuthenticationUrl() throws IOException, ServletException { + when(request.getRequestURI()).thenReturn("/scm/api/rest/authentication"); + securityFilter.doFilter(request, response, chain); + verify(request, never()).setAttribute(Mockito.anyString(), Mockito.any()); + verify(chain).doFilter(request, response); + } + + /** + * Tests filter without prior authentication. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testAnonymous() throws IOException, ServletException { + when(request.getRequestURI()).thenReturn("/scm/api"); + securityFilter.doFilter(request, response, chain); + response.sendError(HttpServletResponse.SC_FORBIDDEN); + } + + /** + * Tests filter without prior authentication and enabled anonymous access. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testAnonymousWithAccessEnabled() throws IOException, ServletException { + when(request.getRequestURI()).thenReturn("/scm/api"); + configuration.setAnonymousAccessEnabled(true); + + // execute + securityFilter.doFilter(request, response, chain); + + // verify and capture + verify(request).setAttribute(SecurityFilter.ATTRIBUTE_REMOTE_USER, SCMContext.USER_ANONYMOUS); + verify(chain).doFilter(requestCaptor.capture(), responseCaptor.capture()); + + // assert + HttpServletRequest captured = requestCaptor.getValue(); + assertEquals(SCMContext.USER_ANONYMOUS, captured.getRemoteUser()); + } + + /** + * Tests filter with prior authentication. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testAuthenticated() throws IOException, ServletException { + authenticateUser(UserTestData.createTrillian()); + when(request.getRequestURI()).thenReturn("/scm/api"); + + // execute + securityFilter.doFilter(request, response, chain); + + // verify and capture + verify(request).setAttribute(SecurityFilter.ATTRIBUTE_REMOTE_USER, "trillian"); + verify(chain).doFilter(requestCaptor.capture(), responseCaptor.capture()); + + // assert + HttpServletRequest captured = requestCaptor.getValue(); + assertEquals("trillian", captured.getRemoteUser()); + } + + /** + * Tests filter without permissions. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testForbidden() throws IOException, ServletException { + authenticateUser(UserTestData.createTrillian()); + when(request.getRequestURI()).thenReturn("/scm/api"); + + // execute + securityFilter = new AccessForbiddenSecurityFilter(configuration); + securityFilter.doFilter(request, response, chain); + + // assert + verify(response).sendError(HttpServletResponse.SC_FORBIDDEN); + } + + /** + * Tests filter unauthenticated and without permissions. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testUnauthorized() throws IOException, ServletException { + when(request.getRequestURI()).thenReturn("/scm/api"); + + // execute + securityFilter = new AccessForbiddenSecurityFilter(configuration); + securityFilter.doFilter(request, response, chain); + + // assert + verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED); + } + + private void authenticateUser(User user) { + SimplePrincipalCollection spc = new SimplePrincipalCollection(); + spc.add(user.getName(), "unit-test"); + spc.add(user, "unit-test"); + + Subject subject = new Subject.Builder() + .authenticated(true) + .principals(spc) + .buildSubject(); + + shiro.setSubject(subject); + } + + private static class AccessForbiddenSecurityFilter extends SecurityFilter { + + private AccessForbiddenSecurityFilter(ScmConfiguration configuration) { + super(configuration); + } + + @Override + protected boolean hasPermission(Subject subject) { + return false; + } + + } + +} \ No newline at end of file diff --git a/scm-webapp/src/test/java/sonia/scm/it/AnonymousAccessITCase.java b/scm-webapp/src/test/java/sonia/scm/it/AnonymousAccessITCase.java index 8122a9b26d..cf27804d2e 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/AnonymousAccessITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/it/AnonymousAccessITCase.java @@ -286,7 +286,7 @@ public class AnonymousAccessITCase File directory = temporaryFolder.newFolder(); RepositoryClient client = null; - String url = repository.createUrl(URL); + String url = repository.createUrl(BASE_URL); if ((username != null) && (password != null)) { diff --git a/scm-webapp/src/test/java/sonia/scm/it/ChangesetViewerITCase.java b/scm-webapp/src/test/java/sonia/scm/it/ChangesetViewerITCase.java index 99f775eaa0..adb055d46a 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/ChangesetViewerITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/it/ChangesetViewerITCase.java @@ -37,7 +37,6 @@ package sonia.scm.it; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -264,7 +263,7 @@ public class ChangesetViewerITCase extends AbstractAdminITCaseBase throws RepositoryClientException { return RepositoryClientFactory.createClient(repositoryType, localDirectory, - repository.createUrl(URL), IntegrationTestUtil.ADMIN_USERNAME, + repository.createUrl(BASE_URL), IntegrationTestUtil.ADMIN_USERNAME, IntegrationTestUtil.ADMIN_PASSWORD); } diff --git a/scm-webapp/src/test/java/sonia/scm/it/IntegrationTestUtil.java b/scm-webapp/src/test/java/sonia/scm/it/IntegrationTestUtil.java index 7d5fa754cd..078b12acc2 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/IntegrationTestUtil.java +++ b/scm-webapp/src/test/java/sonia/scm/it/IntegrationTestUtil.java @@ -77,11 +77,12 @@ public final class IntegrationTestUtil /** Field description */ public static final String ADMIN_USERNAME = "scmadmin"; - - public static final String URL = "http://localhost:8081/scm"; - /** Field description */ - public static final String BASE_URL = "http://localhost:8081/scm/api/rest/"; + /** scm-manager base url */ + public static final String BASE_URL = "http://localhost:8081/scm/"; + + /** scm-manager base url for the rest api */ + public static final String REST_BASE_URL = BASE_URL.concat("api/rest/"); /** Field description */ public static final String EXTENSION = ".xml"; @@ -256,7 +257,7 @@ public final class IntegrationTestUtil */ public static String createResourceUrl(String url) { - return BASE_URL.concat(url).concat(EXTENSION); + return REST_BASE_URL.concat(url).concat(EXTENSION); } /** diff --git a/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java b/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java new file mode 100644 index 0000000000..ad8fddb994 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java @@ -0,0 +1,211 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.it; + +import com.google.common.base.Charsets; +import com.google.common.collect.Lists; +import com.google.common.io.Files; +import com.sun.jersey.api.client.WebResource; +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import org.junit.After; +import static org.junit.Assert.*; +import static org.hamcrest.Matchers.*; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import sonia.scm.debug.DebugHookData; +import static sonia.scm.it.IntegrationTestUtil.createResource; +import static sonia.scm.it.RepositoryITUtil.createRepository; +import static sonia.scm.it.RepositoryITUtil.deleteRepository; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Person; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryTestData; +import sonia.scm.repository.client.api.RepositoryClient; +import sonia.scm.repository.client.api.RepositoryClientFactory; +import sonia.scm.util.IOUtil; + +/** + * Integration tests for repository hooks. + * + * @author Sebastian Sdorra + */ +@RunWith(Parameterized.class) +public class RepositoryHookITCase extends AbstractAdminITCaseBase +{ + + private static final long WAIT_TIME = 125; + + private static final RepositoryClientFactory REPOSITORY_CLIENT_FACTORY = new RepositoryClientFactory(); + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + private final String repositoryType; + private Repository repository; + private File workingCopy; + private RepositoryClient repositoryClient; + + /** + * Constructs a new instance with a repository type. + * + * @param repositoryType repository type + */ + public RepositoryHookITCase(String repositoryType) + { + this.repositoryType = repositoryType; + } + + /** + * Creates a test repository. + * + * @throws IOException + */ + @Before + public void setUpTestRepository() throws IOException + { + repository = RepositoryTestData.createHeartOfGold(repositoryType); + repository = createRepository(client, repository); + workingCopy = tempFolder.newFolder(); + repositoryClient = createRepositoryClient(); + } + + /** + * Removes the tests repository. + */ + @After + public void removeTestRepository() + { + deleteRepository(client, repository.getId()); + } + + /** + * Tests that the debug service has received the commit. + * + * @throws IOException + * @throws InterruptedException + */ + @Test + public void testSimpleHook() throws IOException, InterruptedException + { + // push commit + Files.write("a", new File(workingCopy, "a.txt"), Charsets.UTF_8); + repositoryClient.getAddCommand().add("a.txt"); + Changeset changeset = repositoryClient.getCommitCommand().commit( + new Person("scmadmin", "scmadmin@scm-manager.org"), "added a" + ); + repositoryClient.getPushCommand().push(); + + // wait some time, because the debug hook is asnychron + Thread.sleep(WAIT_TIME); + + // check debug servlet for pushed commit + WebResource wr = createResource(client, "debug/" + repository.getId() + "/post-receive/last"); + DebugHookData data = wr.get(DebugHookData.class); + assertNotNull(data); + assertThat(data.getChangesets(), contains(changeset.getId())); + } + + /** + * Tests that the debug service receives only new commits. + * + * @throws IOException + * @throws InterruptedException + */ + @Test + public void testOnlyNewCommit() throws IOException, InterruptedException + { + // push commit + Files.write("a", new File(workingCopy, "a.txt"), Charsets.UTF_8); + repositoryClient.getAddCommand().add("a.txt"); + Changeset a = repositoryClient.getCommitCommand().commit( + new Person("scmadmin", "scmadmin@scm-manager.org"), "added a" + ); + repositoryClient.getPushCommand().push(); + + // create branch, commit and push again + repositoryClient.getBranchCommand().branch("feature/added-b"); + Files.write("b", new File(workingCopy, "b.txt"), Charsets.UTF_8); + repositoryClient.getAddCommand().add("a.txt"); + Changeset b = repositoryClient.getCommitCommand().commit( + new Person("scmadmin", "scmadmin@scm-manager.org"), "added b" + ); + repositoryClient.getPushCommand().push(); + + // wait some time, because the debug hook is asnychron + Thread.sleep(WAIT_TIME); + + // check debug servlet that only one commit is present + WebResource wr = createResource(client, "debug/" + repository.getId() + "/post-receive/last"); + DebugHookData data = wr.get(DebugHookData.class); + assertNotNull(data); + assertThat(data.getChangesets(), allOf( + contains(b.getId()), + not( + contains(a.getId()) + ) + )); + } + + private RepositoryClient createRepositoryClient() throws IOException + { + return REPOSITORY_CLIENT_FACTORY.create(repositoryType, + IntegrationTestUtil.BASE_URL + repositoryType + "/" + repository.getName(), + IntegrationTestUtil.ADMIN_USERNAME, IntegrationTestUtil.ADMIN_PASSWORD, workingCopy + ); + } + + + /** + * Returns repository types a test parameter. + * + * @return repository types test parameter + */ + @Parameters + public static Collection createParameters() + { + Collection params = Lists.newArrayList(); + params.add(new String[] { "git" }); + // params.add(new String[] { "svn" }); + if (IOUtil.search("hg") != null) + { + params.add(new String[] { "hg" }); + } + return params; + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/it/RepositoryITCase.java b/scm-webapp/src/test/java/sonia/scm/it/RepositoryITCase.java index b3d35a6c17..9ee0cadf58 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/RepositoryITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/it/RepositoryITCase.java @@ -35,7 +35,8 @@ package sonia.scm.it; //~--- non-JDK imports -------------------------------------------------------- -import org.junit.After; +import com.google.common.collect.Lists; + import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -45,23 +46,23 @@ import sonia.scm.repository.Permission; import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryTestData; -import sonia.scm.util.IOUtil; import static org.junit.Assert.*; +import static org.hamcrest.Matchers.*; import static sonia.scm.it.IntegrationTestUtil.*; import static sonia.scm.it.RepositoryITUtil.*; //~--- JDK imports ------------------------------------------------------------ -import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.GenericType; import com.sun.jersey.api.client.WebResource; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import org.junit.After; +import sonia.scm.util.IOUtil; /** * @@ -90,34 +91,9 @@ public class RepositoryITCase extends AbstractAdminITCaseBase * * @return */ - @Parameters - public static Collection createParameters() - { - Collection params = new ArrayList(); - - params.add(new String[] { "git" }); - - params.add(new String[] { "svn" }); - - if (IOUtil.search("hg") != null) - { - params.add(new String[] { "hg" }); - } - - return params; - } - - /** - * Method description - * - */ @After public void cleanup() { - Client client = createClient(); - - authenticateAdmin(client); - Collection repositories = createResource(client, "repositories").get(new GenericType>() {} @@ -137,6 +113,29 @@ public class RepositoryITCase extends AbstractAdminITCaseBase /** * Method description * + * + * @return + */ + @Parameters + public static Collection createParameters() + { + Collection params = Lists.newArrayList(); + + params.add(new String[] { "git" }); + params.add(new String[] { "svn" }); + + if (IOUtil.search("hg") != null) + { + params.add(new String[] { "hg" }); + } + + return params; + } + + /** + * Method description + * +>>>>>>> merge rev */ @Test public void create() @@ -165,21 +164,18 @@ public class RepositoryITCase extends AbstractAdminITCaseBase * Method description * */ - @Test public void doubleCreate() { Repository repository = RepositoryTestData.create42Puzzle(repositoryType); repository = createRepository(client, repository); - + WebResource wr = createResource(client, "repositories"); ClientResponse response = wr.post(ClientResponse.class, repository); - + assertNotNull(response); - - // TODO should be 409? - assertEquals(500, response.getStatus()); + assertThat(response.getStatus(), not(lessThanOrEqualTo(400))); } /** diff --git a/scm-webapp/src/test/java/sonia/scm/it/RepositoryITCaseBase.java b/scm-webapp/src/test/java/sonia/scm/it/RepositoryITCaseBase.java index edfeffe3d3..5df0b56f9b 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/RepositoryITCaseBase.java +++ b/scm-webapp/src/test/java/sonia/scm/it/RepositoryITCaseBase.java @@ -142,7 +142,7 @@ public class RepositoryITCaseBase { RepositoryClient rc = RepositoryClientFactory.createClient(repository.getType(), directory, - repository.createUrl(URL), username, password); + repository.createUrl(BASE_URL), username, password); rc.init(); addTestFiles(rc); @@ -343,7 +343,7 @@ public class RepositoryITCaseBase throws RepositoryClientException { return RepositoryClientFactory.createClient(repository.getType(), - directory, repository.createUrl(URL), user.getName(), password); + directory, repository.createUrl(BASE_URL), user.getName(), password); } //~--- fields --------------------------------------------------------------- diff --git a/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpClientTest.java b/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpClientTest.java new file mode 100644 index 0000000000..faac9a99e5 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpClientTest.java @@ -0,0 +1,376 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +//~--- non-JDK imports -------------------------------------------------------- + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import sonia.scm.config.ScmConfiguration; +import sonia.scm.net.TrustAllHostnameVerifier; +import sonia.scm.util.HttpUtil; + +import static org.junit.Assert.*; + +import static org.mockito.Mockito.*; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import java.net.HttpURLConnection; +import java.net.SocketAddress; +import java.net.URL; + +import java.util.HashSet; +import java.util.Set; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; +import sonia.scm.net.SSLContextProvider; + +/** + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class DefaultAdvancedHttpClientTest +{ + + /** + * Method description + * + * + * @throws IOException + */ + @Test + public void testApplyBaseSettings() throws IOException + { + new AdvancedHttpRequest(client, HttpMethod.GET, + "https://www.scm-manager.org").request(); + verify(connection).setRequestMethod(HttpMethod.GET); + verify(connection).setReadTimeout(DefaultAdvancedHttpClient.TIMEOUT_RAED); + verify(connection).setConnectTimeout( + DefaultAdvancedHttpClient.TIMEOUT_CONNECTION); + verify(connection).addRequestProperty(HttpUtil.HEADER_CONTENT_LENGTH, "0"); + } + + @Test(expected = ContentTransformerNotFoundException.class) + public void testContentTransformerNotFound(){ + client.createTransformer(String.class, "text/plain"); + } + + @Test + public void testContentTransformer(){ + ContentTransformer transformer = mock(ContentTransformer.class); + when(transformer.isResponsible(String.class, "text/plain")).thenReturn(Boolean.TRUE); + transformers.add(transformer); + ContentTransformer t = client.createTransformer(String.class, "text/plain"); + assertSame(transformer, t); + } + + /** + * Method description + * + * + * @throws IOException + */ + @Test + public void testApplyContent() throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + when(connection.getOutputStream()).thenReturn(baos); + + AdvancedHttpRequestWithBody request = + new AdvancedHttpRequestWithBody(client, HttpMethod.PUT, + "https://www.scm-manager.org"); + + request.stringContent("test").request(); + verify(connection).setDoOutput(true); + verify(connection).addRequestProperty(HttpUtil.HEADER_CONTENT_LENGTH, "4"); + assertEquals("test", baos.toString("UTF-8")); + } + + /** + * Method description + * + * + * @throws IOException + */ + @Test + public void testApplyHeaders() throws IOException + { + AdvancedHttpRequest request = new AdvancedHttpRequest(client, + HttpMethod.POST, + "http://www.scm-manager.org"); + + request.header("Header-One", "One").header("Header-Two", "Two").request(); + verify(connection).setRequestMethod(HttpMethod.POST); + verify(connection).addRequestProperty("Header-One", "One"); + verify(connection).addRequestProperty("Header-Two", "Two"); + } + + /** + * Method description + * + * + * @throws IOException + */ + @Test + public void testApplyMultipleHeaders() throws IOException + { + AdvancedHttpRequest request = new AdvancedHttpRequest(client, + HttpMethod.POST, + "http://www.scm-manager.org"); + + request.header("Header-One", "One").header("Header-One", "Two").request(); + verify(connection).setRequestMethod(HttpMethod.POST); + verify(connection).addRequestProperty("Header-One", "One"); + verify(connection).addRequestProperty("Header-One", "Two"); + } + + /** + * Method description + * + * + * @throws IOException + */ + @Test + public void testBodyRequestWithoutContent() throws IOException + { + AdvancedHttpRequestWithBody request = + new AdvancedHttpRequestWithBody(client, HttpMethod.PUT, + "https://www.scm-manager.org"); + + request.request(); + verify(connection).addRequestProperty(HttpUtil.HEADER_CONTENT_LENGTH, "0"); + } + + /** + * Method description + * + * + * @throws IOException + */ + @Test + public void testDisableCertificateValidation() throws IOException + { + AdvancedHttpRequest request = new AdvancedHttpRequest(client, + HttpMethod.GET, + "https://www.scm-manager.org"); + + request.disableCertificateValidation(true).request(); + verify(connection).setSSLSocketFactory(any(SSLSocketFactory.class)); + } + + /** + * Method description + * + * + * @throws IOException + */ + @Test + public void testDisableHostnameValidation() throws IOException + { + AdvancedHttpRequest request = new AdvancedHttpRequest(client, + HttpMethod.GET, + "https://www.scm-manager.org"); + + request.disableHostnameValidation(true).request(); + verify(connection).setHostnameVerifier(any(TrustAllHostnameVerifier.class)); + } + + /** + * Method description + * + * + * @throws IOException + */ + @Test + public void testIgnoreProxy() throws IOException + { + configuration.setProxyServer("proxy.scm-manager.org"); + configuration.setProxyPort(8090); + configuration.setEnableProxy(true); + new AdvancedHttpRequest(client, HttpMethod.GET, + "https://www.scm-manager.org").ignoreProxySettings(true).request(); + assertFalse(client.proxyConnection); + } + + /** + * Method description + * + * + * @throws IOException + */ + @Test + public void testProxyConnection() throws IOException + { + configuration.setProxyServer("proxy.scm-manager.org"); + configuration.setProxyPort(8090); + configuration.setEnableProxy(true); + new AdvancedHttpRequest(client, HttpMethod.GET, + "https://www.scm-manager.org").request(); + assertTrue(client.proxyConnection); + } + + /** + * Method description + * + * + * @throws IOException + */ + @Test + public void testProxyWithAuthentication() throws IOException + { + configuration.setProxyServer("proxy.scm-manager.org"); + configuration.setProxyPort(8090); + configuration.setProxyUser("tricia"); + configuration.setProxyPassword("tricias secret"); + configuration.setEnableProxy(true); + new AdvancedHttpRequest(client, HttpMethod.GET, + "https://www.scm-manager.org").request(); + assertTrue(client.proxyConnection); + verify(connection).addRequestProperty( + DefaultAdvancedHttpClient.HEADER_PROXY_AUTHORIZATION, + "Basic dHJpY2lhOnRyaWNpYXMgc2VjcmV0"); + } + + //~--- set methods ---------------------------------------------------------- + + /** + * Method description + * + */ + @Before + public void setUp() + { + configuration = new ScmConfiguration(); + transformers = new HashSet(); + client = new TestingAdvacedHttpClient(configuration, transformers); + } + + //~--- inner classes -------------------------------------------------------- + + /** + * Class description + * + * + * @version Enter version here..., 15/05/01 + * @author Enter your name here... + */ + public class TestingAdvacedHttpClient extends DefaultAdvancedHttpClient + { + + /** + * Constructs ... + * + * + * @param configuration + * @param transformers + */ + public TestingAdvacedHttpClient(ScmConfiguration configuration, + Set transformers) + { + super(configuration, transformers, new SSLContextProvider()); + } + + //~--- methods ------------------------------------------------------------ + + /** + * Method description + * + * + * @param url + * + * @return + * + * @throws IOException + */ + @Override + protected HttpURLConnection createConnection(URL url) throws IOException + { + return connection; + } + + /** + * Method description + * + * + * @param url + * @param address + * + * @return + * + * @throws IOException + */ + @Override + protected HttpURLConnection createProxyConnecton(URL url, + SocketAddress address) + throws IOException + { + proxyConnection = true; + + return connection; + } + + //~--- fields ------------------------------------------------------------- + + /** Field description */ + private boolean proxyConnection = false; + } + + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + private TestingAdvacedHttpClient client; + + /** Field description */ + private ScmConfiguration configuration; + + /** Field description */ + @Mock + private HttpsURLConnection connection; + + /** Field description */ + private Set transformers; +} diff --git a/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponseTest.java b/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponseTest.java new file mode 100644 index 0000000000..0a2b4cbdb3 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponseTest.java @@ -0,0 +1,152 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Charsets; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.common.io.ByteSource; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import sonia.scm.config.ScmConfiguration; + +import static org.hamcrest.Matchers.*; + +import static org.junit.Assert.*; + +import static org.mockito.Mockito.*; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import java.net.HttpURLConnection; + +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import sonia.scm.net.SSLContextProvider; + +/** + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class DefaultAdvancedHttpResponseTest +{ + + /** + * Method description + * + * + * @throws IOException + */ + @Test + public void testContentAsByteSource() throws IOException + { + ByteArrayInputStream bais = + new ByteArrayInputStream("test".getBytes(Charsets.UTF_8)); + + when(connection.getInputStream()).thenReturn(bais); + + AdvancedHttpResponse response = new DefaultAdvancedHttpResponse(client, + connection, 200, "OK"); + ByteSource content = response.contentAsByteSource(); + + assertEquals("test", content.asCharSource(Charsets.UTF_8).read()); + } + + /** + * Method description + * + * + * @throws IOException + */ + @Test + public void testContentAsByteSourceWithFailedRequest() throws IOException + { + ByteArrayInputStream bais = + new ByteArrayInputStream("test".getBytes(Charsets.UTF_8)); + + when(connection.getInputStream()).thenThrow(IOException.class); + when(connection.getErrorStream()).thenReturn(bais); + + AdvancedHttpResponse response = new DefaultAdvancedHttpResponse(client, + connection, 404, "NOT FOUND"); + ByteSource content = response.contentAsByteSource(); + + assertEquals("test", content.asCharSource(Charsets.UTF_8).read()); + } + + /** + * Method description + * + */ + @Test + public void testGetHeaders() + { + LinkedHashMap> map = Maps.newLinkedHashMap(); + List test = Lists.newArrayList("One", "Two"); + + map.put("Test", test); + when(connection.getHeaderFields()).thenReturn(map); + + AdvancedHttpResponse response = new DefaultAdvancedHttpResponse(client, + connection, 200, "OK"); + Multimap headers = response.getHeaders(); + + assertThat(headers.get("Test"), contains("One", "Two")); + assertTrue(headers.get("Test-2").isEmpty()); + } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + private final DefaultAdvancedHttpClient client = + new DefaultAdvancedHttpClient(new ScmConfiguration(), + new HashSet(), new SSLContextProvider()); + + /** Field description */ + @Mock + private HttpURLConnection connection; +} diff --git a/scm-webapp/src/test/java/sonia/scm/net/ahc/JsonContentTransformerTest.java b/scm-webapp/src/test/java/sonia/scm/net/ahc/JsonContentTransformerTest.java new file mode 100644 index 0000000000..8094d1c06f --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/net/ahc/JsonContentTransformerTest.java @@ -0,0 +1,167 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.net.ahc; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Charsets; +import com.google.common.io.ByteSource; + +import org.junit.Test; + +import static org.junit.Assert.*; + +import static org.mockito.Mockito.*; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.IOException; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * + * @author Sebastian Sdorra + */ +public class JsonContentTransformerTest +{ + + /** + * Method description + * + */ + @Test + public void testDoNotFailOnUnknownProperties() { + ByteSource bs = ByteSource.wrap("{\"value\": \"test\", \"other\": \"test2\"}".getBytes(Charsets.UTF_8)); + TestObject obj = transformer.unmarshall(TestObject.class, bs); + assertEquals("test", obj.value); + } + + /** + * Method description + * + */ + @Test + public void testIsResponsible() + { + assertTrue(transformer.isResponsible(String.class, "application/json")); + assertTrue(transformer.isResponsible(String.class, "application/json;charset=UTF-8")); + assertFalse(transformer.isResponsible(String.class, "text/plain")); + } + + /** + * Method description + * + * + * @throws IOException + */ + @Test + public void testMarshallAndUnmarshall() throws IOException + { + ByteSource bs = transformer.marshall(new TestObject("test")); + TestObject to = transformer.unmarshall(TestObject.class, bs); + + assertEquals("test", to.value); + } + + /** + * Method description + * + * + * @throws IOException + */ + @Test(expected = ContentTransformerException.class) + public void testUnmarshallIOException() throws IOException + { + ByteSource bs = mock(ByteSource.class); + + when(bs.openBufferedStream()).thenThrow(IOException.class); + transformer.unmarshall(String.class, bs); + } + + //~--- inner classes -------------------------------------------------------- + + /** + * Class description + * + * + * @version Enter version here..., 15/10/21 + * @author Enter your name here... + */ + @XmlRootElement(name = "test") + @XmlAccessorType(XmlAccessType.FIELD) + private static class TestObject + { + + /** + * Constructs ... + * + */ + public TestObject() {} + + /** + * Constructs ... + * + * + * @param value + */ + public TestObject(String value) + { + this.value = value; + } + + //~--- fields ------------------------------------------------------------- + + /** Field description */ + private String value; + } + + + /** + * Class description + * + * + * @version Enter version here..., 15/10/21 + * @author Enter your name here... + */ + private static class TestObject2 {} + + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + private final JsonContentTransformer transformer = new JsonContentTransformer(); +} diff --git a/scm-webapp/src/test/java/sonia/scm/net/ahc/XmlContentTransformerTest.java b/scm-webapp/src/test/java/sonia/scm/net/ahc/XmlContentTransformerTest.java new file mode 100644 index 0000000000..ce87752b4f --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/net/ahc/XmlContentTransformerTest.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.net.ahc; + +import com.google.common.io.ByteSource; +import java.io.IOException; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * + * @author Sebastian Sdorra + */ +public class XmlContentTransformerTest { + + private final XmlContentTransformer transformer = new XmlContentTransformer(); + + @Test + public void testIsResponsible() + { + assertTrue(transformer.isResponsible(String.class, "application/xml")); + assertTrue(transformer.isResponsible(String.class, "application/xml;charset=UTF-8")); + assertFalse(transformer.isResponsible(String.class, "text/plain")); + } + + @Test + public void testMarshallAndUnmarshall() throws IOException{ + ByteSource bs = transformer.marshall(new TestObject("test")); + TestObject to = transformer.unmarshall(TestObject.class, bs); + assertEquals("test", to.value); + } + + @Test(expected = ContentTransformerException.class) + public void testUnmarshallIOException() throws IOException{ + ByteSource bs = mock(ByteSource.class); + when(bs.openBufferedStream()).thenThrow(IOException.class); + transformer.unmarshall(String.class, bs); + } + + private static class TestObject2 {} + + @XmlRootElement(name = "test") + @XmlAccessorType(XmlAccessType.FIELD) + private static class TestObject { + + private String value; + + public TestObject() + { + } + + public TestObject(String value) + { + this.value = value; + } + + } + +} \ No newline at end of file diff --git a/scm-webapp/src/test/java/sonia/scm/resources/AbstractResourceManagerTest.java b/scm-webapp/src/test/java/sonia/scm/resources/AbstractResourceManagerTest.java new file mode 100644 index 0000000000..184831f15a --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/resources/AbstractResourceManagerTest.java @@ -0,0 +1,95 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.resources; + +import com.google.common.collect.ImmutableSet; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import static org.hamcrest.Matchers.*; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.plugin.PluginLoader; + +/** + * Unit tests for {@link AbstractResourceManager}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class AbstractResourceManagerTest extends ResourceManagerTestBase +{ + + private DummyResourceManager resourceManager; + + @Before + public void setUp() + { + Set resourceHandlers = ImmutableSet.of(resourceHandler); + resourceManager = new DummyResourceManager(pluginLoader, resourceHandlers); + } + + /** + * Test {@link AbstractResourceManager#getScriptResources()} in the correct order. + * + * @throws java.io.IOException + * + * @see Issue 809 + */ + @Test + public void testGetScriptResources() throws IOException + { + appendScriptResources("a/b.js", "z/a.js", "a/a.js"); + List scripts = resourceManager.getScriptResources(); + assertThat(scripts, contains("a/a.js", "a/b.js", "z/a.js")); + } + + private static class DummyResourceManager extends AbstractResourceManager + { + + public DummyResourceManager(PluginLoader pluginLoader, Set resourceHandlers) + { + super(pluginLoader, resourceHandlers); + } + + @Override + protected void collectResources(Map resourceMap) + { + } + + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/resources/DefaultResourceManagerTest.java b/scm-webapp/src/test/java/sonia/scm/resources/DefaultResourceManagerTest.java new file mode 100644 index 0000000000..a082656016 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/resources/DefaultResourceManagerTest.java @@ -0,0 +1,76 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.resources; + +import com.google.common.collect.ImmutableSet; +import java.io.IOException; +import java.util.List; +import java.util.Set; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +/** + * Unit tests for {@link DefaultResourceManager}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class DefaultResourceManagerTest extends ResourceManagerTestBase { + + private DefaultResourceManager resourceManager; + + /** + * Set up {@link DefaultResourceManager} for tests. + */ + @Before + public void setUp() + { + Set resourceHandlers = ImmutableSet.of(resourceHandler); + resourceManager = new DefaultResourceManager(pluginLoader, resourceHandlers); + } + + /** + * Test {@link DefaultResourceManager#getResources(sonia.scm.resources.ResourceType)} method. + * @throws java.io.IOException + */ + @Test + public void testGetResources() throws IOException + { + appendScriptResources("a/b.js", "z/a.js", "a/a.js"); + List resources = resourceManager.getResources(ResourceType.SCRIPT); + assertEquals(1, resources.size()); + } + +} \ No newline at end of file diff --git a/scm-webapp/src/test/java/sonia/scm/resources/ResourceManagerTestBase.java b/scm-webapp/src/test/java/sonia/scm/resources/ResourceManagerTestBase.java new file mode 100644 index 0000000000..164caf6f8c --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/resources/ResourceManagerTestBase.java @@ -0,0 +1,112 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.resources; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Sets; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import javax.servlet.ServletContext; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; +import org.mockito.Mock; +import static org.mockito.Mockito.when; +import sonia.scm.plugin.Plugin; +import sonia.scm.plugin.PluginCondition; +import sonia.scm.plugin.PluginInformation; +import sonia.scm.plugin.PluginLoader; +import sonia.scm.plugin.PluginResources; +import sonia.scm.plugin.PluginWrapper; +import sonia.scm.plugin.WebResourceLoader; + +/** + * Base class for {@link ResourceManager} tests. + * + * @author Sebastian Sdorra + */ +public abstract class ResourceManagerTestBase +{ + + @Mock + protected ServletContext servletContext; + + @Mock + protected PluginLoader pluginLoader; + + @Mock + protected ResourceHandler resourceHandler; + + @Mock + protected WebResourceLoader webResourceLoader; + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + /** + * Append scripts resources to plugin loader. + * + * @param resources resource names + * + * @throws IOException + */ + protected void appendScriptResources(String... resources) throws IOException + { + Set scripts = Sets.newHashSet(resources); + Set styles = Sets.newHashSet(); + Set dependencies = Sets.newHashSet(); + + + Plugin plugin = new Plugin( + 2, + new PluginInformation(), + new PluginResources(scripts, styles), + new PluginCondition(), + false, + dependencies + ); + + Path pluginPath = tempFolder.newFolder().toPath(); + + PluginWrapper wrapper = new PluginWrapper( + plugin, + Thread.currentThread().getContextClassLoader(), + webResourceLoader, + pluginPath + ); + + List plugins = ImmutableList.of(wrapper); + + when(pluginLoader.getInstalledPlugins()).thenReturn(plugins); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/schedule/InjectionEnabledJobTest.java b/scm-webapp/src/test/java/sonia/scm/schedule/InjectionEnabledJobTest.java new file mode 100644 index 0000000000..9545577c9f --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/schedule/InjectionEnabledJobTest.java @@ -0,0 +1,168 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.schedule; + +import com.google.inject.Injector; +import com.google.inject.Provider; +import org.junit.Test; +import static org.mockito.Mockito.*; +import org.junit.Rule; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import sonia.scm.web.security.AdministrationContext; +import sonia.scm.web.security.PrivilegedAction; + +/** + * Unit tests for {@link InjectionEnabledJob}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class InjectionEnabledJobTest { + + @Mock + private Injector injector; + + @Mock + private JobDataMap dataMap; + + @Mock + private JobDetail detail; + + @Mock + private JobExecutionContext jec; + + @Mock + private Provider runnable; + + @Mock + private AdministrationContext context; + + @Rule + public ExpectedException expected = ExpectedException.none(); + + /** + * Tests {@link InjectionEnabledJob#execute(org.quartz.JobExecutionContext)} without context. + * + * @throws JobExecutionException + */ + @Test + public void testExecuteWithoutContext() throws JobExecutionException + { + expected.expect(NullPointerException.class); + expected.expectMessage("execution context"); + new InjectionEnabledJob().execute(null); + } + + /** + * Tests {@link InjectionEnabledJob#execute(org.quartz.JobExecutionContext)} without job detail. + * + * @throws JobExecutionException + */ + @Test + public void testExecuteWithoutJobDetail() throws JobExecutionException + { + expected.expect(NullPointerException.class); + expected.expectMessage("detail"); + new InjectionEnabledJob().execute(jec); + } + + /** + * Tests {@link InjectionEnabledJob#execute(org.quartz.JobExecutionContext)} without data map. + * + * @throws JobExecutionException + */ + @Test + public void testExecuteWithoutDataMap() throws JobExecutionException + { + when(jec.getJobDetail()).thenReturn(detail); + expected.expect(NullPointerException.class); + expected.expectMessage("data map"); + new InjectionEnabledJob().execute(jec); + } + + /** + * Tests {@link InjectionEnabledJob#execute(org.quartz.JobExecutionContext)} without injector. + * + * @throws JobExecutionException + */ + @Test + public void testExecuteWithoutInjector() throws JobExecutionException + { + when(jec.getJobDetail()).thenReturn(detail); + when(detail.getJobDataMap()).thenReturn(dataMap); + expected.expect(NullPointerException.class); + expected.expectMessage("injector"); + new InjectionEnabledJob().execute(jec); + } + + /** + * Tests {@link InjectionEnabledJob#execute(org.quartz.JobExecutionContext)} without runnable. + * + * @throws JobExecutionException + */ + @Test + public void testExecuteWithoutRunnable() throws JobExecutionException + { + when(jec.getJobDetail()).thenReturn(detail); + when(detail.getJobDataMap()).thenReturn(dataMap); + when(dataMap.get(Injector.class.getName())).thenReturn(injector); + expected.expect(JobExecutionException.class); + expected.expectMessage("runnable"); + new InjectionEnabledJob().execute(jec); + } + + /** + * Tests {@link InjectionEnabledJob#execute(org.quartz.JobExecutionContext)}. + * + * @throws JobExecutionException + */ + @Test + public void testExecute() throws JobExecutionException + { + when(jec.getJobDetail()).thenReturn(detail); + when(detail.getJobDataMap()).thenReturn(dataMap); + when(dataMap.get(Injector.class.getName())).thenReturn(injector); + when(dataMap.get(Runnable.class.getName())).thenReturn(runnable); + when(injector.getInstance(AdministrationContext.class)).thenReturn(context); + new InjectionEnabledJob().execute(jec); + verify(context).runAsAdmin(Mockito.any(PrivilegedAction.class)); + } + +} \ No newline at end of file diff --git a/scm-webapp/src/test/java/sonia/scm/schedule/QuartzSchedulerTest.java b/scm-webapp/src/test/java/sonia/scm/schedule/QuartzSchedulerTest.java new file mode 100644 index 0000000000..2f3dc86ea1 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/schedule/QuartzSchedulerTest.java @@ -0,0 +1,220 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.schedule; + +import com.google.inject.Injector; +import com.google.inject.Provider; +import java.io.IOException; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import static org.hamcrest.Matchers.*; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.quartz.CronTrigger; +import org.quartz.JobDetail; +import org.quartz.SchedulerException; +import org.quartz.Trigger; + +/** + * Unit tests for {@link QuartzScheduler}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class QuartzSchedulerTest { + + @Mock + private Injector injector; + + @Mock + private org.quartz.Scheduler quartzScheduler; + + private QuartzScheduler scheduler; + + @Before + public void setUp() + { + scheduler = new QuartzScheduler(injector, quartzScheduler); + } + + /** + * Tests {@link QuartzScheduler#schedule(java.lang.String, java.lang.Runnable)}. + * + * @throws SchedulerException + */ + @Test + public void testSchedule() throws SchedulerException + { + DummyRunnable dr = new DummyRunnable(); + Task task = scheduler.schedule("42 2 * * * ?", dr); + assertNotNull(task); + + ArgumentCaptor detailCaptor = ArgumentCaptor.forClass(JobDetail.class); + ArgumentCaptor triggerCaptor = ArgumentCaptor.forClass(Trigger.class); + verify(quartzScheduler).scheduleJob(detailCaptor.capture(), triggerCaptor.capture()); + + Trigger trigger = triggerCaptor.getValue(); + assertThat(trigger, is(instanceOf(CronTrigger.class))); + CronTrigger cron = (CronTrigger) trigger; + assertEquals("42 2 * * * ?", cron.getCronExpression()); + + JobDetail detail = detailCaptor.getValue(); + assertEquals(InjectionEnabledJob.class, detail.getJobClass()); + Provider runnable = (Provider) detail.getJobDataMap().get(Runnable.class.getName()); + assertNotNull(runnable); + assertSame(dr, runnable.get()); + assertEquals(injector, detail.getJobDataMap().get(Injector.class.getName())); + } + + /** + * Tests {@link QuartzScheduler#schedule(java.lang.String, java.lang.Class)}. + * + * @throws SchedulerException + */ + @Test + public void testScheduleWithClass() throws SchedulerException + { + scheduler.schedule("42 * * * * ?", DummyRunnable.class); + + verify(injector).getProvider(DummyRunnable.class); + + ArgumentCaptor detailCaptor = ArgumentCaptor.forClass(JobDetail.class); + ArgumentCaptor triggerCaptor = ArgumentCaptor.forClass(Trigger.class); + verify(quartzScheduler).scheduleJob(detailCaptor.capture(), triggerCaptor.capture()); + + Trigger trigger = triggerCaptor.getValue(); + assertThat(trigger, is(instanceOf(CronTrigger.class))); + CronTrigger cron = (CronTrigger) trigger; + assertEquals("42 * * * * ?", cron.getCronExpression()); + + JobDetail detail = detailCaptor.getValue(); + assertEquals(InjectionEnabledJob.class, detail.getJobClass()); + assertEquals(injector, detail.getJobDataMap().get(Injector.class.getName())); + } + + /** + * Tests {@link QuartzScheduler#init(sonia.scm.SCMContextProvider)}. + * + * @throws SchedulerException + */ + @Test + public void testInit() throws SchedulerException + { + when(quartzScheduler.isStarted()).thenReturn(Boolean.FALSE); + scheduler.init(null); + verify(quartzScheduler).start(); + } + + /** + * Tests {@link QuartzScheduler#init(sonia.scm.SCMContextProvider)} when the underlying scheduler is already started. + * + * @throws SchedulerException + */ + @Test + public void testInitAlreadyRunning() throws SchedulerException + { + when(quartzScheduler.isStarted()).thenReturn(Boolean.TRUE); + scheduler.init(null); + verify(quartzScheduler, never()).start(); + } + + /** + * Tests {@link QuartzScheduler#init(sonia.scm.SCMContextProvider)} when the underlying scheduler throws an exception. + * + * @throws SchedulerException + */ + @Test + public void testInitException() throws SchedulerException + { + when(quartzScheduler.isStarted()).thenThrow(SchedulerException.class); + scheduler.init(null); + verify(quartzScheduler, never()).start(); + } + + /** + * Tests {@link QuartzScheduler#close()}. + * + * @throws IOException + * @throws SchedulerException + */ + @Test + public void testClose() throws IOException, SchedulerException + { + when(quartzScheduler.isStarted()).thenReturn(Boolean.TRUE); + scheduler.close(); + verify(quartzScheduler).shutdown(); + } + + /** + * Tests {@link QuartzScheduler#close()} when the underlying scheduler is not running. + * + * @throws IOException + * @throws SchedulerException + */ + @Test + public void testCloseNotRunning() throws IOException, SchedulerException + { + when(quartzScheduler.isStarted()).thenReturn(Boolean.FALSE); + scheduler.close(); + verify(quartzScheduler, never()).shutdown(); + } + + /** + * Tests {@link QuartzScheduler#close()} when the underlying scheduler throws an exception. + * + * @throws IOException + * @throws SchedulerException + */ + @Test + public void testCloseException() throws IOException, SchedulerException + { + when(quartzScheduler.isStarted()).thenThrow(SchedulerException.class); + scheduler.close(); + verify(quartzScheduler, never()).shutdown(); + } + + + public static class DummyRunnable implements Runnable { + + @Override + public void run() + { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + } +} \ No newline at end of file diff --git a/scm-webapp/src/test/java/sonia/scm/schedule/QuartzTaskTest.java b/scm-webapp/src/test/java/sonia/scm/schedule/QuartzTaskTest.java new file mode 100644 index 0000000000..5ba5c19373 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/schedule/QuartzTaskTest.java @@ -0,0 +1,89 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.schedule; + +import org.junit.Test; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import static org.hamcrest.Matchers.*; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.quartz.JobKey; +import org.quartz.SchedulerException; + +/** + * Unit tests for {@link QuartzTask}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class QuartzTaskTest { + + @Mock + private org.quartz.Scheduler scheduler; + + private final JobKey jobKey = new JobKey("sample"); + + private QuartzTask task; + + @Before + public void setUp(){ + task = new QuartzTask(scheduler, jobKey); + } + + /** + * Tests {@link QuartzTask#cancel()}. + * + * @throws SchedulerException + */ + @Test + public void testCancel() throws SchedulerException + { + task.cancel(); + verify(scheduler).deleteJob(jobKey); + } + + /** + * Tests {@link QuartzTask#cancel()} when the scheduler throws an exception. + * @throws org.quartz.SchedulerException + */ + @Test(expected = RuntimeException.class) + public void testCancelWithException() throws SchedulerException + { + when(scheduler.deleteJob(jobKey)).thenThrow(SchedulerException.class); + task.cancel(); + } + +} \ No newline at end of file diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java new file mode 100644 index 0000000000..edec33aa59 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java @@ -0,0 +1,401 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.security; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import com.google.common.base.Predicate; +import com.google.common.collect.Lists; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.authz.Permission; +import org.apache.shiro.authz.SimpleAuthorizationInfo; +import org.apache.shiro.authz.permission.PermissionResolver; +import org.apache.shiro.authz.permission.WildcardPermission; +import org.apache.shiro.subject.SimplePrincipalCollection; +import org.apache.shiro.subject.Subject; +import org.hamcrest.Matchers; +import org.junit.Test; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import static org.mockito.Mockito.*; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.HandlerEventType; +import sonia.scm.cache.Cache; +import sonia.scm.cache.CacheManager; +import sonia.scm.group.Group; +import sonia.scm.group.GroupEvent; +import sonia.scm.group.GroupModificationEvent; +import sonia.scm.group.GroupNames; +import sonia.scm.repository.PermissionType; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryDAO; +import sonia.scm.repository.RepositoryEvent; +import sonia.scm.repository.RepositoryModificationEvent; +import sonia.scm.repository.RepositoryTestData; +import sonia.scm.user.User; +import sonia.scm.user.UserEvent; +import sonia.scm.user.UserModificationEvent; +import sonia.scm.user.UserTestData; + +/** + * Unit tests for {@link AuthorizationCollector}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class DefaultAuthorizationCollectorTest { + + @Mock + private Cache cache; + + @Mock + private CacheManager cacheManager; + + @Mock + private RepositoryDAO repositoryDAO; + + @Mock + private SecuritySystem securitySystem; + + private DefaultAuthorizationCollector collector; + + @Rule + public ShiroRule shiro = new ShiroRule(); + + /** + * Set up object to test. + */ + @Before + public void setUp(){ + when(cacheManager.getCache(Mockito.any(String.class))).thenReturn(cache); + + collector = new DefaultAuthorizationCollector(cacheManager, repositoryDAO, securitySystem); + } + + /** + * Tests {@link AuthorizationCollector#onEvent(sonia.scm.user.UserEvent)}. + */ + @Test + public void testOnUserEvent() + { + User user = UserTestData.createDent(); + collector.onEvent(new UserEvent(HandlerEventType.BEFORE_CREATE, user)); + verify(cache, never()).clear(); + + collector.onEvent(new UserEvent(HandlerEventType.CREATE, user)); + verify(cache).removeAll(Mockito.any(Predicate.class)); + } + + /** + * Tests {@link AuthorizationCollector#onEvent(sonia.scm.user.UserEvent)} with modified user. + */ + @Test + public void testOnUserModificationEvent() + { + User user = UserTestData.createDent(); + User userModified = UserTestData.createDent(); + userModified.setDisplayName("Super Dent"); + + collector.onEvent(new UserModificationEvent(HandlerEventType.BEFORE_CREATE, userModified, user)); + verify(cache, never()).removeAll(Mockito.any(Predicate.class)); + + collector.onEvent(new UserModificationEvent(HandlerEventType.CREATE, userModified, user)); + verify(cache, never()).removeAll(Mockito.any(Predicate.class)); + + userModified.setAdmin(true); + + collector.onEvent(new UserModificationEvent(HandlerEventType.BEFORE_CREATE, userModified, user)); + verify(cache, never()).removeAll(Mockito.any(Predicate.class)); + + collector.onEvent(new UserModificationEvent(HandlerEventType.CREATE, userModified, user)); + verify(cache).removeAll(Mockito.any(Predicate.class)); + } + + /** + * Tests {@link AuthorizationCollector#onEvent(sonia.scm.group.GroupEvent)}. + */ + @Test + public void testOnGroupEvent() + { + Group group = new Group("xml", "base"); + collector.onEvent(new GroupEvent(HandlerEventType.BEFORE_CREATE, group)); + verify(cache, never()).clear(); + + collector.onEvent(new GroupEvent(HandlerEventType.CREATE, group)); + verify(cache).clear(); + } + + /** + * Tests {@link AuthorizationCollector#onEvent(sonia.scm.group.GroupEvent)} with modified groups. + */ + @Test + public void testOnGroupModificationEvent() + { + Group group = new Group("xml", "base"); + Group modifiedGroup = new Group("xml", "base"); + collector.onEvent(new GroupModificationEvent(HandlerEventType.BEFORE_MODIFY, modifiedGroup, group)); + verify(cache, never()).clear(); + + collector.onEvent(new GroupModificationEvent(HandlerEventType.MODIFY, modifiedGroup, group)); + verify(cache, never()).clear(); + + modifiedGroup.add("test"); + collector.onEvent(new GroupModificationEvent(HandlerEventType.MODIFY, modifiedGroup, group)); + verify(cache).clear(); + } + + /** + * Tests {@link AuthorizationCollector#onEvent(sonia.scm.repository.RepositoryEvent)}. + */ + @Test + public void testOnRepositoryEvent() + { + Repository repository = RepositoryTestData.createHeartOfGold(); + collector.onEvent(new RepositoryEvent(HandlerEventType.BEFORE_CREATE, repository)); + verify(cache, never()).clear(); + + collector.onEvent(new RepositoryEvent(HandlerEventType.CREATE, repository)); + verify(cache).clear(); + } + + /** + * Tests {@link AuthorizationCollector#onEvent(sonia.scm.repository.RepositoryEvent)} with modified repository. + */ + @Test + public void testOnRepositoryModificationEvent() + { + Repository repositoryModified = RepositoryTestData.createHeartOfGold(); + repositoryModified.setName("test123"); + repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test"))); + + Repository repository = RepositoryTestData.createHeartOfGold(); + repository.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test"))); + + collector.onEvent(new RepositoryModificationEvent(HandlerEventType.BEFORE_CREATE, repositoryModified, repository)); + verify(cache, never()).clear(); + + collector.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository)); + verify(cache, never()).clear(); + + repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test"))); + collector.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository)); + verify(cache, never()).clear(); + + repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test123"))); + collector.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository)); + verify(cache).clear(); + + repositoryModified.setPermissions( + Lists.newArrayList(new sonia.scm.repository.Permission("test", PermissionType.READ, true)) + ); + collector.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository)); + verify(cache, times(2)).clear(); + + repositoryModified.setPermissions( + Lists.newArrayList(new sonia.scm.repository.Permission("test", PermissionType.WRITE)) + ); + collector.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository)); + verify(cache, times(3)).clear(); + } + + /** + * Tests {@link AuthorizationCollector#onEvent(sonia.scm.security.StoredAssignedPermissionEvent)}. + */ + @Test + public void testOnStoredAssignedPermissionEvent() + { + StoredAssignedPermission groupPermission = new StoredAssignedPermission( + "123", new AssignedPermission("_authenticated", true, "repository:read:*") + ); + collector.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.BEFORE_CREATE, groupPermission)); + verify(cache, never()).clear(); + + collector.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.CREATE, groupPermission)); + verify(cache).clear(); + + + StoredAssignedPermission userPermission = new StoredAssignedPermission( + "123", new AssignedPermission("trillian", false, "repository:read:*") + ); + collector.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.BEFORE_CREATE, userPermission)); + verify(cache, never()).removeAll(Mockito.any(Predicate.class)); + verify(cache).clear(); + + collector.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.CREATE, userPermission)); + verify(cache).removeAll(Mockito.any(Predicate.class)); + verify(cache).clear(); + } + + /** + * Tests {@link AuthorizationCollector#collect()} without user role. + */ + @Test + @SubjectAware + public void testCollectWithoutUserRole() + { + AuthorizationInfo authInfo = collector.collect(); + assertThat(authInfo.getRoles(), nullValue()); + assertThat(authInfo.getStringPermissions(), nullValue()); + assertThat(authInfo.getObjectPermissions(), nullValue()); + } + + /** + * Tests {@link AuthorizationCollector#collect()} from cache. + */ + @Test + @SubjectAware( + configuration = "classpath:sonia/scm/shiro-001.ini" + ) + public void testCollectFromCache() + { + AuthorizationInfo info = new SimpleAuthorizationInfo(); + when(cache.get(anyObject())).thenReturn(info); + authenticate(UserTestData.createTrillian(), "main"); + + AuthorizationInfo authInfo = collector.collect(); + assertSame(info, authInfo); + } + + /** + * Tests {@link AuthorizationCollector#collect()} with cache. + */ + @Test + @SubjectAware( + configuration = "classpath:sonia/scm/shiro-001.ini" + ) + public void testCollectWithCache(){ + authenticate(UserTestData.createTrillian(), "main"); + + AuthorizationInfo authInfo = collector.collect(); + verify(cache).put(any(), any()); + } + + /** + * Tests {@link AuthorizationCollector#collect()} without permissions. + */ + @Test + @SubjectAware( + configuration = "classpath:sonia/scm/shiro-001.ini" + ) + public void testCollectWithoutPermissions() + { + authenticate(UserTestData.createTrillian(), "main"); + + AuthorizationInfo authInfo = collector.collect(); + assertThat(authInfo.getRoles(), Matchers.contains(Role.USER)); + assertThat(authInfo.getStringPermissions(), hasSize(0)); + assertThat(authInfo.getObjectPermissions(), nullValue()); + } + + /** + * Tests {@link AuthorizationCollector#collect()} as admin. + */ + @Test + @SubjectAware( + configuration = "classpath:sonia/scm/shiro-001.ini" + ) + public void testCollectAsAdmin() + { + User trillian = UserTestData.createTrillian(); + trillian.setAdmin(true); + authenticate(trillian, "main"); + + AuthorizationInfo authInfo = collector.collect(); + assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER, Role.ADMIN)); + assertThat(authInfo.getObjectPermissions(), nullValue()); + assertThat(authInfo.getStringPermissions(), Matchers.contains("*")); + } + + /** + * Tests {@link AuthorizationCollector#collect()} with repository permissions. + */ + @Test + @SubjectAware( + configuration = "classpath:sonia/scm/shiro-001.ini" + ) + public void testCollectWithRepositoryPermissions() + { + String group = "heart-of-gold-crew"; + authenticate(UserTestData.createTrillian(), group); + Repository heartOfGold = RepositoryTestData.createHeartOfGold(); + heartOfGold.setId("one"); + heartOfGold.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("trillian"))); + Repository puzzle42 = RepositoryTestData.create42Puzzle(); + puzzle42.setId("two"); + sonia.scm.repository.Permission permission = new sonia.scm.repository.Permission(group, PermissionType.WRITE, true); + puzzle42.setPermissions(Lists.newArrayList(permission)); + when(repositoryDAO.getAll()).thenReturn(Lists.newArrayList(heartOfGold, puzzle42)); + + // execute and assert + AuthorizationInfo authInfo = collector.collect(); + assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); + assertThat(authInfo.getObjectPermissions(), nullValue()); + assertThat(authInfo.getStringPermissions(), containsInAnyOrder("repository:read:one", "repository:read,write:two")); + } + + /** + * Tests {@link AuthorizationCollector#collect()} with global permissions. + */ + @Test + @SubjectAware( + configuration = "classpath:sonia/scm/shiro-001.ini" + ) + public void testCollectWithGlobalPermissions(){ + authenticate(UserTestData.createTrillian(), "main"); + + StoredAssignedPermission p1 = new StoredAssignedPermission("one", new AssignedPermission("one", "one:one")); + StoredAssignedPermission p2 = new StoredAssignedPermission("two", new AssignedPermission("two", "two:two")); + when(securitySystem.getPermissions(Mockito.any(Predicate.class))).thenReturn(Lists.newArrayList(p1, p2)); + + // execute and assert + AuthorizationInfo authInfo = collector.collect(); + assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); + assertThat(authInfo.getObjectPermissions(), nullValue()); + assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two")); + } + + private void authenticate(User user, String group, String... groups) + { + SimplePrincipalCollection spc = new SimplePrincipalCollection(); + spc.add(user.getName(), "unit"); + spc.add(user, "unit"); + spc.add(new GroupNames(group, groups), "unit"); + Subject subject = new Subject.Builder().authenticated(true).principals(spc).buildSubject(); + shiro.setSubject(subject); + } + +} \ No newline at end of file diff --git a/scm-webapp/src/test/java/sonia/scm/security/XsrfCookiesTest.java b/scm-webapp/src/test/java/sonia/scm/security/XsrfCookiesTest.java new file mode 100644 index 0000000000..64dd27bc3b --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/XsrfCookiesTest.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.security; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import static org.mockito.Mockito.*; +import org.mockito.runners.MockitoJUnitRunner; + +/** + * Tests for the {@link XsrfCookies} util class. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class XsrfCookiesTest { + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + /** + * Prepare mocks for testing. + */ + @Before + public void prepareMocks(){ + when(request.getContextPath()).thenReturn("/scm"); + } + + /** + * Tests create method. + */ + @Test + public void testCreate() + { + XsrfCookies.create(request, response, "mytoken"); + + // capture cookie + ArgumentCaptor captor = ArgumentCaptor.forClass(Cookie.class); + verify(response).addCookie(captor.capture()); + + // check for cookie + Cookie cookie = captor.getValue(); + assertEquals(XsrfProtectionFilter.KEY, cookie.getName()); + assertEquals("/scm", cookie.getPath()); + assertEquals("mytoken", cookie.getValue()); + } + + /** + * Tests remove method. + */ + @Test + public void testRemove(){ + Cookie cookie = new Cookie(XsrfProtectionFilter.KEY, "mytoken"); + cookie.setMaxAge(15); + when(request.getCookies()).thenReturn(new Cookie[]{cookie}); + XsrfCookies.remove(request, response); + + // capture cookie + ArgumentCaptor captor = ArgumentCaptor.forClass(Cookie.class); + verify(response).addCookie(captor.capture()); + + // check the captured cookie + Cookie c = captor.getValue(); + assertEquals("cookie max age should be set to 0", 0, c.getMaxAge()); + assertEquals("cookie path should be equals", cookie.getPath(), c.getPath()); + assertNull("cookie value shuld be null", c.getValue()); + } + +} \ No newline at end of file diff --git a/scm-webapp/src/test/java/sonia/scm/security/XsrfProtectionFilterTest.java b/scm-webapp/src/test/java/sonia/scm/security/XsrfProtectionFilterTest.java new file mode 100644 index 0000000000..0a2145de04 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/XsrfProtectionFilterTest.java @@ -0,0 +1,211 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.security; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import static org.mockito.Mockito.*; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.util.HttpUtil; + +/** + * Unit tests for {@link XsrfProtectionFilter}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class XsrfProtectionFilterTest { + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private HttpSession session; + + @Mock + private FilterChain chain; + + private final ScmConfiguration configuration = new ScmConfiguration(); + + private final XsrfProtectionFilter filter = new XsrfProtectionFilter(configuration); + + /** + * Prepare mocks for testing. + */ + @Before + public void setUp(){ + when(request.getSession(true)).thenReturn(session); + when(request.getContextPath()).thenReturn("/scm"); + configuration.setEnabledXsrfProtection(true); + } + + /** + * Test filter method for non web interface clients. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterFromNonWuiClient() throws IOException, ServletException + { + filter.doFilter(request, response, chain); + verify(chain).doFilter(request, response); + } + + /** + * Test filter method with disabled xsrf protection. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterWithDisabledXsrfProtection() throws IOException, ServletException + { + // disable xsrf protection + configuration.setEnabledXsrfProtection(false); + + // set webui user-agent + when(request.getHeader(HttpUtil.HEADER_SCM_CLIENT)).thenReturn(HttpUtil.SCM_CLIENT_WUI); + + // call the filter + filter.doFilter(request, response, chain); + + // verify that no xsrf other any other cookie was set + verify(response, never()).addCookie(Mockito.any(Cookie.class)); + + // ensure filter chain is called + verify(chain).doFilter(request, response); + } + + /** + * Test filter method for first web interface request. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterIssuesTokenOnFirstWuiRequest() throws IOException, ServletException + { + when(request.getHeader(HttpUtil.HEADER_SCM_CLIENT)).thenReturn(HttpUtil.SCM_CLIENT_WUI); + + // call the filter + filter.doFilter(request, response, chain); + + // capture cookie + ArgumentCaptor captor = ArgumentCaptor.forClass(Cookie.class); + verify(response).addCookie(captor.capture()); + + // check for cookie + Cookie cookie = captor.getValue(); + assertEquals(XsrfProtectionFilter.KEY, cookie.getName()); + assertEquals("/scm", cookie.getPath()); + assertNotNull(cookie.getValue()); + + // ensure filter chain is called + verify(chain).doFilter(request, response); + } + + /** + * Test filter method on protected session with an invalid xsrf token. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterWithInvalidToken() throws IOException, ServletException { + when(request.getHeader(HttpUtil.HEADER_SCM_CLIENT)).thenReturn(HttpUtil.SCM_CLIENT_WUI); + when(request.getHeader(XsrfProtectionFilter.KEY)).thenReturn("invalidtoken"); + when(session.getAttribute(XsrfProtectionFilter.KEY)).thenReturn("mytoken"); + + // call the filter + filter.doFilter(request, response, chain); + + // ensure response send forbidden and the chain was never called + verify(response).sendError(HttpServletResponse.SC_FORBIDDEN); + verify(chain, never()).doFilter(request, response); + } + + /** + * Test filter method on protected session without xsrf token. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterOnProtectedSessionWithoutToken() throws IOException, ServletException { + when(request.getHeader(HttpUtil.HEADER_SCM_CLIENT)).thenReturn(HttpUtil.SCM_CLIENT_WUI); + when(session.getAttribute(XsrfProtectionFilter.KEY)).thenReturn("mytoken"); + + // call the filter + filter.doFilter(request, response, chain); + + // ensure response send forbidden and the chain was never called + verify(response).sendError(HttpServletResponse.SC_FORBIDDEN); + verify(chain, never()).doFilter(request, response); + } + + /** + * Test filter method on protected session with valid xsrf token. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterOnProtectedSessionWithValidToken() throws IOException, ServletException { + when(request.getHeader(HttpUtil.HEADER_SCM_CLIENT)).thenReturn(HttpUtil.SCM_CLIENT_WUI); + when(request.getHeader(XsrfProtectionFilter.KEY)).thenReturn("mytoken"); + when(session.getAttribute(XsrfProtectionFilter.KEY)).thenReturn("mytoken"); + + // call the filter + filter.doFilter(request, response, chain); + + // ensure chain was called + verify(chain).doFilter(request, response); + } + +} \ No newline at end of file diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/AuthenticationITCase.java b/scm-webapp/src/test/java/sonia/scm/selenium/AuthenticationITCase.java index a2b0d0fe6d..34013e7e76 100644 --- a/scm-webapp/src/test/java/sonia/scm/selenium/AuthenticationITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/selenium/AuthenticationITCase.java @@ -34,25 +34,27 @@ package sonia.scm.selenium; //~--- non-JDK imports -------------------------------------------------------- +import sonia.scm.selenium.page.Pages; +import sonia.scm.selenium.page.MainPage; +import sonia.scm.selenium.page.LoginPage; +import static org.junit.Assert.*; import org.junit.Test; /** - * + * Authentication related selenium integration tests. + * * @author Sebastian Sdorra */ -public class AuthenticationITCase extends SeleniumTestBase -{ +public class AuthenticationITCase extends SeleniumITCaseBase { /** - * Method description - * - * - * @throws Exception + * Authenticates an user and call logout function. */ @Test - public void testAuthentication() throws Exception - { - login("scmadmin", "scmadmin"); - logout(); + public void testAuthentication() { + MainPage main = Pages.get(driver, LoginPage.class).login("scmadmin", "scmadmin"); + assertEquals("scmadmin", main.getUserInfo()); + main.logout(); } + } diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/RepositoryCRUDITCase.java b/scm-webapp/src/test/java/sonia/scm/selenium/RepositoriesITCase.java similarity index 66% rename from scm-webapp/src/test/java/sonia/scm/selenium/RepositoryCRUDITCase.java rename to scm-webapp/src/test/java/sonia/scm/selenium/RepositoriesITCase.java index 64760ae774..05898f6529 100644 --- a/scm-webapp/src/test/java/sonia/scm/selenium/RepositoryCRUDITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/selenium/RepositoriesITCase.java @@ -34,61 +34,55 @@ package sonia.scm.selenium; //~--- non-JDK imports -------------------------------------------------------- +import sonia.scm.selenium.page.Pages; +import sonia.scm.selenium.page.MainPage; +import sonia.scm.selenium.page.LoginPage; import org.junit.After; import org.junit.Before; import org.junit.Test; -import static org.junit.Assert.*; +import sonia.scm.repository.Repository; /** - * + * Repository related selenium integration tests. + * * @author Sebastian Sdorra */ -public class RepositoryCRUDITCase extends SeleniumTestBase -{ +public class RepositoriesITCase extends SeleniumITCaseBase { + private MainPage main; + /** - * Method description - * - */ - @After - public void after() - { - logout(); - } - - /** - * Method description - * - */ - @Test - public void createRepository() throws InterruptedException - { - waitAndClick("#repositoryAddButton"); - waitForPresence("input[name=name]").sendKeys("scm"); - select("#x-form-el-repositoryType img").click(); - waitAndClick("div.x-combo-list-item:nth-of-type(2)"); - type("input[name=contact]", "scmadmin@scm-manager.org"); - type("textarea[name=description]", "SCM-Manager"); - waitAndClick("div.x-panel-btns button:nth-of-type(1)"); - - String name = - waitForPresence( - "div.x-grid3-row-selected div.x-grid3-col-name").getText(); - - assertEquals("scm", name); - - waitAndClick("#repoRmButton button"); - waitAndClick("div.x-window button:nth-of-type(1)"); - } - - /** - * Method description - * + * Authenticates admin user, before each test. */ @Before - public void login() - { - login("scmadmin", "scmadmin"); + public void login() { + main = Pages.get(driver, LoginPage.class) + .login("scmadmin", "scmadmin"); + } + + /** + * Creates, select and removes a repository. + */ + @Test + public void createRepository() { + Repository repository = new Repository(); + repository.setName("scm"); + repository.setType("git"); + repository.setContact("scmadmin@scm-manager.org"); + repository.setDescription("SCM-Manager"); + + main.repositories() + .add(repository) + .select(repository.getName()) + .remove(); + } + + /** + * Logs the user out, after each test. + */ + @After + public void logout() { + main.logout(); } } diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/SeleniumITCaseBase.java b/scm-webapp/src/test/java/sonia/scm/selenium/SeleniumITCaseBase.java new file mode 100644 index 0000000000..fde04d9cad --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/selenium/SeleniumITCaseBase.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.selenium; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.firefox.FirefoxDriver; + +/** + * Base class for selenium integration tests. + * + * @author Sebastian Sdorra + */ +public class SeleniumITCaseBase { + + /** + * Selenium test driver. + */ + protected static WebDriver driver; + + /** + * Setup selenium test driver. + */ + @BeforeClass + public static void setUpDriver() { + // DesiredCapabilities capa = DesiredCapabilities.chrome(); + // capa.setBrowserName("firefox"); + // capa.setPlatform(Platform.ANY); + // RemoteWebDriver driver = new RemoteWebDriver(new URL("http://localhost:4444/wd/hub"), capa); + + driver = new FirefoxDriver(); + driver.get("http://localhost:8082/scm/index.html"); + } + + /** + * Closes the selenium test driver. + */ + @AfterClass + public static void tearDownDriver() { + driver.close(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/SeleniumTestBase.java b/scm-webapp/src/test/java/sonia/scm/selenium/SeleniumTestBase.java deleted file mode 100644 index 1ae5e86b3d..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/selenium/SeleniumTestBase.java +++ /dev/null @@ -1,271 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - -package sonia.scm.selenium; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.io.Files; - -import org.junit.After; -import org.junit.Before; - -import org.openqa.selenium.By; -import org.openqa.selenium.OutputType; -import org.openqa.selenium.TakesScreenshot; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.firefox.FirefoxDriver; -import org.openqa.selenium.support.ui.ExpectedConditions; -import org.openqa.selenium.support.ui.WebDriverWait; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.File; -import java.io.IOException; - -import java.util.concurrent.TimeUnit; - -/** - * - * @author Sebastian Sdorra - */ -public class SeleniumTestBase -{ - - /** - * the logger for SeleniumTestBase - */ - private static final Logger logger = - LoggerFactory.getLogger(SeleniumTestBase.class); - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @throws Exception - */ - @After - public void tearDown() throws Exception - { - driver.quit(); - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @throws Exception - */ - @Before - public void setUp() throws Exception - { - driver = new FirefoxDriver(); - baseUrl = "http://localhost:8082/scm/"; - open("index.html"); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param username - * @param password - */ - protected void login(String username, String password) - { - type("input[name=username]", username); - type("input[name=password]", password); - waitAndClick("#loginButton button"); - - String ue = waitForPresence("#scm-userinfo-tip").getText(); - - assertEquals(username, ue); - } - - /** - * Method description - * - */ - protected void logout() - { - waitAndClick("#navLogout a"); - } - - /** - * Method description - * - * - * @param url - */ - protected void open(String url) - { - driver.get(baseUrl + url); - pause(500, TimeUnit.MILLISECONDS); - } - - /** - * Method description - * - * - * @param value - * @param unit - */ - protected void pause(int value, TimeUnit unit) - { - driver.manage().timeouts().implicitlyWait(value, unit); - } - - /** - * Method description - * - * - * @param target - */ - protected void screenshot(String target) - { - screenshot(new File(target)); - } - - /** - * Method description - * - * - * @param target - */ - protected void screenshot(File target) - { - try - { - File scrFile = - ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); - - Files.copy(scrFile, target); - } - catch (IOException ex) - { - logger.error("could not create screenshot", ex); - } - } - - /** - * Method description - * - * - * @param cssSelector - * - * @return - */ - protected WebElement select(String cssSelector) - { - WebElement element = driver.findElement(By.cssSelector(cssSelector)); - - assertNotNull(element); - - return element; - } - - /** - * Method description - * - * - * @param cssLocator - * @param value - */ - protected void type(String cssLocator, String value) - { - select(cssLocator).clear(); - select(cssLocator).sendKeys(value); - } - - /** - * Method description - * - * - * @param query - */ - protected void waitAndClick(String query) - { - waitToBeClickable(query).click(); - } - - /** - * Method description - * - * - * @param query - * - * @return - */ - protected WebElement waitForPresence(String query) - { - WebDriverWait wait = new WebDriverWait(driver, 5); - - return wait.until( - ExpectedConditions.presenceOfElementLocated(By.cssSelector(query))); - } - - /** - * Method description - * - * - * @param query - * - * @return - */ - protected WebElement waitToBeClickable(String query) - { - WebDriverWait wait = new WebDriverWait(driver, 5); - - return wait.until( - ExpectedConditions.elementToBeClickable(By.cssSelector(query))); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - protected WebDriver driver; - - /** Field description */ - private String baseUrl; -} diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/page/BasePage.java b/scm-webapp/src/test/java/sonia/scm/selenium/page/BasePage.java new file mode 100644 index 0000000000..28c27457a8 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/selenium/page/BasePage.java @@ -0,0 +1,168 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.selenium.page; + +import com.google.common.base.Throwables; +import com.google.common.io.Files; +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.openqa.selenium.By; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +/** + * Abstract selenium base page. + * + * @author Sebastian Sdorra + * + * @param

    concrete page implementation + */ +public abstract class BasePage

    { + + /** + * Selenium test driver. + */ + protected final WebDriver driver; + + /** + * Constructs a new base page. + * + * @param driver selenium test driver + */ + protected BasePage(WebDriver driver) { + this.driver = driver; + } + + /** + * Performs a {@link Thread#sleep(long)} for the given timeout. + * + * @param time timeout + * @param unit time unit of timeout + */ + protected void sleep(long time, TimeUnit unit) { + try { + unit.sleep(time); + } catch (InterruptedException ex) { + throw Throwables.propagate(ex); + } + } + + /** + * Wait for the element until it is clickable. + * + * @param by element selector + * + * @return web element + */ + protected WebElement waitToBeClickable(By by){ + return waitToBeClickable(driver.findElement(by)); + } + + /** + * Waits for the element until it is clickable. + * + * @param element web element + * + * @return web element + */ + protected WebElement waitToBeClickable(WebElement element) { + WebDriverWait wait = new WebDriverWait(driver, 5); + + return wait.until(ExpectedConditions.elementToBeClickable(element)); + } + + /** + * Waits until the element is present. + * + * @param by element locator + * + * @return web element + */ + protected WebElement waitFor(By by){ + WebDriverWait wait = new WebDriverWait(driver, 1); + return wait.until(ExpectedConditions.presenceOfElementLocated(by)); + } + + /** + * Waits until the elements are present. + * + * @param by element selector + * + * @return list of web elements + */ + protected List waitForAll(By by){ + WebDriverWait wait = new WebDriverWait(driver, 1); + return wait.until(ExpectedConditions.presenceOfAllElementsLocatedBy(by)); + } + + /** + * Creates a screenshot of the current browser content and stores it at the given path. + * + * @param target target file path + * + * @return {@code this} + */ + public P screenshot(String target) { + return screenshot(new File(target)); + } + + /** + * Creates a screenshot of the current browser content and stores it at the file. + * + * @param target target file + * + * @return {@code this} + */ + public P screenshot(File target) { + try { + File scrFile = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); + + Files.copy(scrFile, target); + } catch (IOException ex) { + throw Throwables.propagate(ex); + } + return self(); + } + + /** + * Returns {@code this}. + * + * @return {@code this} + */ + protected abstract P self(); + +} diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/page/LoginPage.java b/scm-webapp/src/test/java/sonia/scm/selenium/page/LoginPage.java new file mode 100644 index 0000000000..cf9d231510 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/selenium/page/LoginPage.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.selenium.page; + +import java.util.concurrent.TimeUnit; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +/** + * Page object for the scm-manager login page. + * + * @author Sebastian Sdorra + */ +public class LoginPage extends BasePage { + + @FindBy(css = "input[name=username]") + private WebElement usernameInput; + + @FindBy(css = "input[name=password]") + private WebElement passwordInput; + + @FindBy(css = "#loginButton button") + private WebElement loginButton; + + /** + * Constructs a new page. This constructor should only be called from {@link Pages}. + * + * @param driver selenium test driver + */ + LoginPage(WebDriver driver) { + super(driver); + } + + @Override + protected LoginPage self() { + return this; + } + + /** + * Authenticates the user and returns the {@link MainPage}. + * + * @param username username + * @param password password + * + * @return {@link MainPage} after successful authentication + */ + public MainPage login(String username, String password) { + usernameInput.clear(); + usernameInput.sendKeys(username); + + passwordInput.clear(); + passwordInput.sendKeys(password); + + sleep(250, TimeUnit.MILLISECONDS); + + waitToBeClickable(loginButton).click(); + + return Pages.get(driver, MainPage.class); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/page/MainPage.java b/scm-webapp/src/test/java/sonia/scm/selenium/page/MainPage.java new file mode 100644 index 0000000000..42fc807e67 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/selenium/page/MainPage.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.selenium.page; + +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +/** + * Page object for scm-manager's main page. + * + * @author Sebastian Sdorra + */ +public class MainPage extends BasePage { + + @FindBy(css = "#navLogout a") + private WebElement logoutLink; + + @FindBy(linkText = "Repositories") + private WebElement repositoriesLink; + + @FindBy(css = "#scm-userinfo-tip") + private WebElement userInfoTip; + + /** + * Constructs a new page. This constructor should only be called from {@link Pages}. + * + * @param driver selenium test driver + */ + MainPage(WebDriver driver) { + super(driver); + } + + @Override + protected MainPage self() { + return this; + } + + /** + * Returns the name of the current authenticated user from the user info tip. + * + * @return name of the current authenticated user + */ + public String getUserInfo(){ + return userInfoTip.getText(); + } + + /** + * Navigates to the repositories page and returns the page object for this page. + * + * @return page object for repositories page + */ + public RepositoriesPage repositories(){ + repositoriesLink.click(); + return Pages.get(driver, RepositoriesPage.class); + } + + /** + * Logs the current user out. + * + * @return page object for the login + */ + public LoginPage logout(){ + waitToBeClickable(logoutLink).click(); + return Pages.get(driver, LoginPage.class); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/page/Pages.java b/scm-webapp/src/test/java/sonia/scm/selenium/page/Pages.java new file mode 100644 index 0000000000..b8d85ea08b --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/selenium/page/Pages.java @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.selenium.page; + +import com.google.common.base.Throwables; +import com.google.common.collect.Lists; +import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.List; +import org.openqa.selenium.By; +import org.openqa.selenium.SearchContext; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.PageFactory; +import org.openqa.selenium.support.pagefactory.DefaultElementLocatorFactory; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +/** + * Helper class for selenium page objects. + * + * @author Sebastian Sdorra + */ +public final class Pages { + + private Pages() { + } + + /** + * Creates an instance of the given page object. + * + * @param page object type + * @param driver selenium driver + * @param clazz page object type + * @param otherArguments other constructor arguments + * + * @return instance of page object + */ + public static T get(WebDriver driver, Class clazz, Object... otherArguments) + { + T page = null; + try { + List> argumentTypes = Lists.newArrayList(); + argumentTypes.add(WebDriver.class); + for (Object argument : otherArguments) { + argumentTypes.add(argument.getClass()); + } + + List arguments = Lists.newArrayList(); + arguments.add(driver); + arguments.addAll(Arrays.asList(otherArguments)); + + Constructor constructor = clazz.getDeclaredConstructor( + argumentTypes.toArray(new Class[argumentTypes.size()]) + ); + page = constructor.newInstance(arguments.toArray(new Object[arguments.size()])); + + PageFactory.initElements(new DefaultElementLocatorFactory(new WaitingSearchContext(driver)), page); + } catch (Exception ex) { + throw Throwables.propagate(ex); + } + return page; + } + + private static class WaitingSearchContext implements SearchContext { + + private final WebDriver driver; + private final WebDriverWait wait; + + private WaitingSearchContext(WebDriver driver) { + this.driver = driver; + this.wait = new WebDriverWait(driver, 1); + } + + @Override + public List findElements(By by) { + return wait.until(ExpectedConditions.presenceOfAllElementsLocatedBy(by)); + } + + @Override + public WebElement findElement(By by) { + return wait.until(ExpectedConditions.presenceOfElementLocated(by)); + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/page/RepositoriesAddPage.java b/scm-webapp/src/test/java/sonia/scm/selenium/page/RepositoriesAddPage.java new file mode 100644 index 0000000000..f9f00056ae --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/selenium/page/RepositoriesAddPage.java @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.selenium.page; + +import com.google.common.base.Objects; +import java.util.List; +import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.NotFoundException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; +import org.openqa.selenium.support.ui.WebDriverWait; +import sonia.scm.repository.Repository; + +/** + * Page object for scm-manager's repository creation page. + * + * @author Sebastian Sdorra + */ +public class RepositoriesAddPage extends BasePage { + + @FindBy(css = "input[name=name]") + private WebElement nameInput; + + @FindBy(css = "input[name=contact]") + private WebElement contactInput; + + @FindBy(css = "#x-form-el-repositoryType img") + private WebElement typeInput; + + @FindBy(css = "textarea[name=description]") + private WebElement descriptionInput; + + @FindBy(css = "div.x-panel-btns button:nth-of-type(1)") + private WebElement okButton; + + private final RepositoriesPage repositoriesPage; + + /** + * Constructs a new page. This constructor should only be called from {@link Pages}. + * + * @param driver selenium test driver + * @param repositoriesPage repositories page object + */ + RepositoriesAddPage(WebDriver driver, RepositoriesPage repositoriesPage) { + super(driver); + this.repositoriesPage = repositoriesPage; + } + + @Override + protected RepositoriesAddPage self() { + return this; + } + + /** + * Creates a new {@link Repository}. + * + * @param repository repository for creation + * + * @return repositories overview page + */ + public RepositoriesPage add(Repository repository) { + nameInput.sendKeys(repository.getName()); + + selectType(repository.getType()); + + contactInput.sendKeys(repository.getContact()); + descriptionInput.sendKeys(repository.getDescription()); + + waitToBeClickable(okButton).click(); + + return repositoriesPage; + } + + private void selectType(String type) { + typeInput.click(); + + String displayName = findDisplayName(type); + + WebDriverWait wait = new WebDriverWait(driver, 1); + List elements = waitForAll(By.className("x-combo-list-item")); + WebElement typeElement = null; + for (WebElement te : elements){ + if (te.getText().trim().equalsIgnoreCase(displayName)){ + typeElement = te; + break; + } + } + + if (typeElement == null){ + throw new NotFoundException("could not find type element with type " + displayName); + } + + typeElement.click(); + } + + private String findDisplayName(String type) { + String displayName = null; + if (driver instanceof JavascriptExecutor) { + // TODO seams not to work + String script = "Sonia.repository.getTypeByName('" + type + "').displayName;"; + displayName = (String) ((JavascriptExecutor)driver).executeScript(script); + } + return Objects.firstNonNull(displayName, type); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/page/RepositoriesPage.java b/scm-webapp/src/test/java/sonia/scm/selenium/page/RepositoriesPage.java new file mode 100644 index 0000000000..fef75683ff --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/selenium/page/RepositoriesPage.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.selenium.page; + +import java.util.List; +import java.util.Locale; +import org.openqa.selenium.By; +import org.openqa.selenium.NotFoundException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; +import sonia.scm.repository.Repository; + +/** + * Page object for scm-manager's repositories overview page. + * + * @author Sebastian Sdorra + */ +public class RepositoriesPage extends BasePage { + + @FindBy(id = "repositoryAddButton") + private WebElement addButton; + + /** + * Constructs a new page. This constructor should only be called from {@link Pages}. + * + * @param driver selenium test driver + */ + RepositoriesPage(WebDriver driver) { + super(driver); + } + + @Override + protected RepositoriesPage self() { + return this; + } + + /** + * Creates a new {@link Repository}. + * + * @param repository repository for creation + * + * @return {@link this} + */ + public RepositoriesPage add(Repository repository){ + addButton.click(); + RepositoriesAddPage addPage = Pages.get(driver, RepositoriesAddPage.class, this); + return addPage.add(repository); + } + + /** + * Selects the repository with the given name and returns the detail page object for the selected repository. + * + * @param repositoryName name of the repository + * + * @return page object for selected repository + */ + public RepositoryPage select(String repositoryName){ + WebElement repositoryNameColumn = null; + + List elements = waitForAll(By.className("x-grid3-col-name")); + for (WebElement element : elements){ + if (element.getText().trim().toLowerCase(Locale.ENGLISH).equals(repositoryName)){ + repositoryNameColumn = element; + break; + } + } + + if ( repositoryNameColumn == null ) { + throw new NotFoundException("could not find repository " + repositoryName); + } + + return Pages.get(driver, RepositoryPage.class, this); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/page/RepositoryPage.java b/scm-webapp/src/test/java/sonia/scm/selenium/page/RepositoryPage.java new file mode 100644 index 0000000000..d09a0debb5 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/selenium/page/RepositoryPage.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2014, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.selenium.page; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +/** + * Page object for scm-manager's repository detail page. + * + * @author Sebastian Sdorra + */ +public class RepositoryPage extends BasePage { + + @FindBy(css = "#repoRmButton button") + private WebElement removeButton; + + private final RepositoriesPage repositoriesPage; + + /** + * Constructs a new page. This constructor should only be called from {@link Pages}. + * + * @param driver selenium test driver + * @param repositoriesPage repositories page object + */ + RepositoryPage(WebDriver driver, RepositoriesPage repositoriesPage) { + super(driver); + this.repositoriesPage = repositoriesPage; + } + + @Override + protected RepositoryPage self() { + return this; + } + + /** + * Removes the selected repository. + * + * @return repositories overview page object + */ + public RepositoriesPage remove(){ + removeButton.click(); + waitToBeClickable(By.cssSelector("div.x-window button:nth-of-type(1)")).click(); + return repositoriesPage; + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/web/BasicWebTokenGeneratorTest.java b/scm-webapp/src/test/java/sonia/scm/web/BasicWebTokenGeneratorTest.java index ff6ba8a59a..7228af07fb 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/BasicWebTokenGeneratorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/BasicWebTokenGeneratorTest.java @@ -55,13 +55,25 @@ import static org.mockito.Mockito.*; import javax.servlet.http.HttpServletRequest; +import org.junit.Before; + /** - * + * TODO add test with {@link UserAgentParser}. + * * @author Sebastian Sdorra */ @RunWith(MockitoJUnitRunner.class) public class BasicWebTokenGeneratorTest { + + /** + * Set up object under test. + * Use {@code null} as {@link UserAgentParser}. + */ + @Before + public void setUpObjectUnderTest() { + generator = new BasicWebTokenGenerator(null); + } /** * Method description @@ -132,10 +144,9 @@ public class BasicWebTokenGeneratorTest } //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final BasicWebTokenGenerator generator = new BasicWebTokenGenerator(); - + + private BasicWebTokenGenerator generator; + /** Field description */ @Mock private HttpServletRequest request; diff --git a/scm-webapp/src/test/java/sonia/scm/web/BrowserUserAgentProviderTest.java b/scm-webapp/src/test/java/sonia/scm/web/BrowserUserAgentProviderTest.java new file mode 100644 index 0000000000..c01b76a313 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/web/BrowserUserAgentProviderTest.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.web; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Strings; + +import org.junit.Test; + + +import static org.junit.Assert.*; + + +//~--- JDK imports ------------------------------------------------------------ + +import java.util.Locale; + +/** + * + * @author Sebastian Sdorra + */ +public class BrowserUserAgentProviderTest +{ + + /** Field description */ + private static final String CHROME = + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36"; + + /** Field description */ + private static final String FIREFOX = + "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-GB; rv:1.8.1.18) Gecko/20081029 Firefox/2.0.0.18"; + + /** Field description */ + private static final String MSIE = + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; )"; + + /** Field description */ + private static final String OPERA = + "Opera/9.80 (Windows NT 5.1; U; cs) Presto/2.2.15 Version/10.00"; + + /** Field description */ + private static final String SAFARI = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/6.1.3 Safari/537.75.14"; + + /** Field description */ + private static final String WGET = "Wget/1.5.3"; + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + */ + @Test + public void testParseUserAgent() + { + assertEquals(BrowserUserAgentProvider.MSIE, parse(MSIE)); + assertEquals(BrowserUserAgentProvider.FIREFOX, parse(FIREFOX)); + assertEquals(BrowserUserAgentProvider.OPERA, parse(OPERA)); + assertEquals(BrowserUserAgentProvider.CHROME, parse(CHROME)); + assertEquals(BrowserUserAgentProvider.SAFARI, parse(SAFARI)); + assertNull(parse(WGET)); + } + + /** + * Method description + * + * + * @param v + * + * @return + */ + private UserAgent parse(String v) + { + return provider.parseUserAgent( + Strings.nullToEmpty(v).toLowerCase(Locale.ENGLISH)); + } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + private final BrowserUserAgentProvider provider = + new BrowserUserAgentProvider(); +} diff --git a/scm-webapp/src/test/resources/sonia/scm/shiro-001.ini b/scm-webapp/src/test/resources/sonia/scm/shiro-001.ini new file mode 100644 index 0000000000..54741bcf4d --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/shiro-001.ini @@ -0,0 +1,7 @@ +[users] +trillian = secret, user +dent = secret, admin + +[roles] +admin = * +user = something:*