Jonathan Mort avatar Jonathan Mort committed d90b9f8

initial code drop - working plugin

Comments (0)

Files changed (25)

+target
+.idea
+*.iml
+*.ipr
+Copyright 2012 Jonathan Mort
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+I realise that this plugin is the same as this one from Go2Group https://marketplace.atlassian.com/plugins/com.go2group.hipchat-plugin
+but I has already written most of this before that was released so I thought I'd release it anyway.
+
+Here are the SDK commands you'll use immediately:
+
+* atlas-run   -- installs this plugin into Stash and starts it on http://localhost:7990/stash
+* atlas-debug -- same as atlas-run, but allows a debugger to attach at port 5005
+* atlas-cli   -- after atlas-run or atlas-debug, opens a Maven command line window:
+                 - 'pi' reinstalls the plugin into the running Stash instance
+* atlas-help  -- prints description for all commands in the SDK
+
+Full documentation is always available at:
+
+https://developer.atlassian.com/display/DOCS/Developing+with+the+Atlassian+Plugin+SDK
+<?xml version="1.0" encoding="UTF-8"?>
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>com.jonmort.stash.hipchat</groupId>
+    <artifactId>stash-hipchat</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <organization>
+        <name>Jon Mort</name>
+        <url>http://www.bitbucket.org/jonmort</url>
+    </organization>
+    <name>Stash HipChat Integration</name>
+    <description>This plugin provides HipChat integration for Atlassian Stash.</description>
+    <packaging>atlassian-plugin</packaging>
+
+    <scm>
+        <developerConnection>scm:git:ssh://git@bitbucket.org/jonmort/stash-hipchat.git</developerConnection>
+    </scm>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.atlassian.stash</groupId>
+            <artifactId>stash-api</artifactId>
+            <version>${stash.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.atlassian.soy</groupId>
+            <artifactId>soy-template-renderer-api</artifactId>
+            <version>1.1.1</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.atlassian.plugins</groupId>
+            <artifactId>atlassian-plugins-webfragment</artifactId>
+            <version>2.12.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.atlassian.stash</groupId>
+            <artifactId>stash-page-objects</artifactId>
+            <version>${stash.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+            <version>2.4</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>commons-lang</groupId>
+            <artifactId>commons-lang</artifactId>
+            <version>2.6</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.atlassian.plugins.rest</groupId>
+            <artifactId>atlassian-rest-common</artifactId>
+            <version>2.6.5</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.atlassian.plugins.rest</groupId>
+            <artifactId>atlassian-rest-module</artifactId>
+            <version>2.6.5</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.atlassian.sal</groupId>
+            <artifactId>sal-api</artifactId>
+            <version>2.7.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>1.6.4</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.8.1</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.wink</groupId>
+            <artifactId>wink-client</artifactId>
+            <version>1.1.3-incubating</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <version>1.8.5</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>com.atlassian.maven.plugins</groupId>
+                <artifactId>maven-stash-plugin</artifactId>
+                <version>${amps.version}</version>
+                <extensions>true</extensions>
+                <configuration>
+                    <products>
+                        <product>
+                            <id>stash</id>
+                            <instanceId>stash</instanceId>
+                            <version>${stash.version}</version>
+                            <dataVersion>${stash.data.version}</dataVersion>
+                        </product>
+                    </products>
+                    <instructions/>
+                    <enableFastdev>false</enableFastdev>
+                </configuration>
+            </plugin>
+            <plugin>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>1.6</source>
+                    <target>1.6</target>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>it/**/*.java</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+    <properties>
+        <stash.version>1.1.0</stash.version>
+        <stash.data.version>1.1.0</stash.data.version>
+        <amps.version>3.11</amps.version>
+    </properties>
+</project>

src/main/java/com/jonmort/stash/hipchat/HipChatConfiguredCondition.java

+package com.jonmort.stash.hipchat;
+
+import com.atlassian.plugin.PluginParseException;
+import com.atlassian.plugin.web.Condition;
+import org.apache.commons.lang.StringUtils;
+
+import java.util.Map;
+
+public class HipChatConfiguredCondition implements Condition {
+    private final RoomNotificationManager roomNotificationManager;
+
+    public HipChatConfiguredCondition(RoomNotificationManager roomNotificationManager) {
+        this.roomNotificationManager = roomNotificationManager;
+    }
+
+    @Override
+    public void init(Map<String, String> stringStringMap) throws PluginParseException {
+    }
+
+    @Override
+    public boolean shouldDisplay(Map<String, Object> stringObjectMap) {
+        return StringUtils.isNotBlank(roomNotificationManager.getApiKey());
+    }
+}

src/main/java/com/jonmort/stash/hipchat/HipChatUriBuilder.java

+package com.jonmort.stash.hipchat;
+
+import javax.ws.rs.core.UriBuilder;
+import java.net.URI;
+
+public class HipChatUriBuilder {
+
+    public static URI roomsList(String authToken) {
+        return hipchatUri().path("rooms").path("list").queryParam("format", "json").queryParam("auth_token", authToken).build();
+    }
+
+    public static URI roomsMessage(String authToken) {
+        return hipchatUri().path("rooms").path("message").queryParam("format", "json").queryParam("auth_token", authToken).build();
+    }
+
+    private static UriBuilder hipchatUri() {
+        return UriBuilder.fromUri(System.getProperty("hipchat.url", "https://api.hipchat.com/v1/"));
+    }
+}

src/main/java/com/jonmort/stash/hipchat/RoomNotificationManager.java

+package com.jonmort.stash.hipchat;
+
+import com.atlassian.event.api.EventListener;
+import com.atlassian.sal.api.net.Request;
+import com.atlassian.sal.api.net.RequestFactory;
+import com.atlassian.sal.api.net.ResponseException;
+import com.atlassian.sal.api.pluginsettings.PluginSettings;
+import com.atlassian.sal.api.pluginsettings.PluginSettingsFactory;
+import com.atlassian.soy.renderer.SoyException;
+import com.atlassian.soy.renderer.SoyTemplateRenderer;
+import com.atlassian.stash.event.RepositoryPushEvent;
+import com.atlassian.stash.nav.NavBuilder;
+import com.atlassian.stash.project.Project;
+import com.atlassian.stash.repository.Repository;
+import com.atlassian.stash.server.ApplicationPropertiesService;
+import com.google.common.collect.ImmutableMap;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+public class RoomNotificationManager {
+    public static final String HIPCHAT_USERNAME_KEY = "hipchat-username";
+    private final Logger log = LoggerFactory.getLogger(RoomNotificationManager.class);
+
+    public static final String SETTINGS_KEY = "com.jonmort.stash.stash-hipcaht";
+    public static final String API_KEY_KEY = "api-key";
+    public static final String PROJECT_MAP_KEY = "project-map";
+    private final PluginSettings pluginSettings;
+    private final RequestFactory requestFactory;
+    private final SoyTemplateRenderer soyTemplateRenderer;
+    private final NavBuilder navBuilder;
+
+
+    public RoomNotificationManager(final PluginSettingsFactory pluginSettingsFactory, RequestFactory requestFactory, SoyTemplateRenderer soyTemplateRenderer, NavBuilder navBuilder) {
+        this.requestFactory = requestFactory;
+        this.soyTemplateRenderer = soyTemplateRenderer;
+        this.navBuilder = navBuilder;
+        pluginSettings = pluginSettingsFactory.createSettingsForKey(SETTINGS_KEY);
+    }
+
+    public String getApiKey() {
+        final Object apiKey = pluginSettings.get(API_KEY_KEY);
+        if (apiKey instanceof String)
+            return (String) apiKey;
+        else return "";
+    }
+
+    public void setApiKey(String apiKey) {
+        if (StringUtils.isBlank(apiKey)) pluginSettings.remove(API_KEY_KEY);
+        else pluginSettings.put(API_KEY_KEY, apiKey);
+    }
+
+    public String getHipchatUsername() {
+        final Object apiKey = pluginSettings.get(HIPCHAT_USERNAME_KEY);
+        if (apiKey instanceof String)
+            return (String) apiKey;
+        else return "";
+    }
+
+    public void setHipchatUsername(String username) {
+        if (StringUtils.isBlank(username)) pluginSettings.remove(HIPCHAT_USERNAME_KEY);
+        else pluginSettings.put(HIPCHAT_USERNAME_KEY, username);
+    }
+
+    public Integer getRoomForProject(Project project) {
+        return getRoomForProjectKey(project.getKey());
+    }
+
+    public Integer getRoomForProjectKey(String projectKey) {
+        final String room = getProjectKeyToRoomMap().get(projectKey);
+        if (room == null) {
+            return null;
+        }
+        return Integer.parseInt(room);
+    }
+
+    public void setRoomForProject(Project project, int room) {
+        final String projectKey = project.getKey();
+        setRoomForProjectKey(projectKey, room);
+    }
+
+    public void setRoomForProjectKey(String projectKey, int room) {
+        final Map<String, String> projectIdToRoomMap = getProjectKeyToRoomMap();
+        projectIdToRoomMap.put(projectKey, Integer.toString(room));
+        storeProjectKeyToRoomMap(projectIdToRoomMap);
+    }
+
+    private void storeProjectKeyToRoomMap(Map<String, String> projectKeyToRoomMap) {
+        pluginSettings.put(PROJECT_MAP_KEY, projectKeyToRoomMap);
+    }
+
+    private Map<String, String> getProjectKeyToRoomMap() {
+        final Object o = pluginSettings.get("project-map");
+        if (o instanceof Map) {
+            return (Map<String, String>) o;
+        } else {
+            Map<String, String> roomMap = new HashMap<String, String>();
+            pluginSettings.put("project-map", roomMap);
+            return roomMap;
+        }
+    }
+
+    @EventListener
+    public void pushEvent(RepositoryPushEvent pushEvent) throws UnsupportedEncodingException {
+        final Project project = pushEvent.getRepository().getProject();
+        final Integer room = getRoomForProject(project);
+        if (room != null) {
+            final Request request = requestFactory.createRequest(Request.MethodType.POST, HipChatUriBuilder.roomsMessage(getApiKey()).toASCIIString());
+            request.setRequestContentType("application/x-www-form-urlencoded");
+            final String hipchatUsername = StringUtils.defaultIfBlank(getHipchatUsername(), "Stash");
+            request.addRequestParameters("room_id", Integer.toString(room),
+                    "from", hipchatUsername,
+                    "message", generateMessage(pushEvent));
+            try {
+                request.execute();
+            } catch (ResponseException e) {
+                log.error("Couldn't post message to HipChat", e);
+            }
+        }
+
+    }
+
+    private String generateMessage(RepositoryPushEvent pushEvent) {
+        final Repository repository = pushEvent.getRepository();
+        final Project project = repository.getProject();
+
+        try {
+            return soyTemplateRenderer.render("com.jonmort.stash.hipchat.stash-hipchat:hipchat-message-soy", "hipchat.stash.message.message",
+                    ImmutableMap.<String, Object>of(
+                            "user", pushEvent.getUser().getDisplayName(),
+                            "project", project,
+                            "projectUrl", navBuilder.project(project).buildAbsolute(),
+                            "repository", repository,
+                            "repositoryUrl", navBuilder.repo(repository).commits().buildAbsolute()));
+        } catch (SoyException e) {
+            return e.getMessage();
+        }
+    }
+}

src/main/java/com/jonmort/stash/hipchat/rest/ApiConfigurationModel.java

+package com.jonmort.stash.hipchat.rest;
+
+import org.codehaus.jackson.annotate.JsonProperty;
+import org.codehaus.jackson.map.annotate.JsonSerialize;
+
+@JsonSerialize
+public class ApiConfigurationModel {
+
+    @JsonProperty
+    private String apiKey;
+
+    @JsonProperty
+    private String username;
+
+    public ApiConfigurationModel(String apiKey, String username) {
+        this.apiKey = apiKey;
+        this.username = username;
+    }
+
+    public ApiConfigurationModel() {
+    }
+
+    public String getApiKey() {
+        return apiKey;
+    }
+
+    public void setApiKey(String apiKey) {
+        this.apiKey = apiKey;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+}

src/main/java/com/jonmort/stash/hipchat/rest/ApiConfigurationResource.java

+package com.jonmort.stash.hipchat.rest;
+
+import com.jonmort.stash.hipchat.RoomNotificationManager;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+@Path("/api")
+
+@Produces({MediaType.APPLICATION_JSON})
+public class ApiConfigurationResource {
+    private final RoomNotificationManager roomNotificationManager;
+
+    public ApiConfigurationResource(RoomNotificationManager roomNotificationManager) {
+        this.roomNotificationManager = roomNotificationManager;
+    }
+
+    @GET
+    public Response getSettings() {
+        final ApiConfigurationModel apiModel = new ApiConfigurationModel(roomNotificationManager.getApiKey(), roomNotificationManager.getHipchatUsername());
+        return Response.ok(apiModel).build();
+    }
+
+    @POST
+    @Consumes({MediaType.APPLICATION_FORM_URLENCODED})
+    public Response setSettings(@FormParam("apikey") final String apiKey, @FormParam("username") final String username) {
+        roomNotificationManager.setApiKey(apiKey);
+        roomNotificationManager.setHipchatUsername(username);
+        return Response.ok(new ApiConfigurationModel(apiKey, username)).build();
+    }
+}

src/main/java/com/jonmort/stash/hipchat/rest/RoomConfigurationModel.java

+package com.jonmort.stash.hipchat.rest;
+
+import org.codehaus.jackson.annotate.JsonProperty;
+import org.codehaus.jackson.map.annotate.JsonSerialize;
+
+@JsonSerialize
+public class RoomConfigurationModel {
+
+    @JsonProperty
+    private Integer room_id;
+
+    @JsonProperty
+    private String projectKey;
+
+    public RoomConfigurationModel() {
+    }
+
+    public RoomConfigurationModel(String projectKey, Integer room_id) {
+        this.projectKey = projectKey;
+        this.room_id = room_id;
+    }
+
+    public String getProjectKey() {
+        return projectKey;
+    }
+
+    public void setProjectKey(String projectKey) {
+        this.projectKey = projectKey;
+    }
+
+    public Integer getRoom_id() {
+        return room_id;
+    }
+
+    public void setRoom_id(Integer room_id) {
+        this.room_id = room_id;
+    }
+}

src/main/java/com/jonmort/stash/hipchat/rest/RoomConfigurationResource.java

+package com.jonmort.stash.hipchat.rest;
+
+import com.jonmort.stash.hipchat.RoomNotificationManager;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+@Path("/room/{project-key}")
+@Produces(MediaType.APPLICATION_JSON)
+public class RoomConfigurationResource {
+
+    private final RoomNotificationManager roomNotificationManager;
+
+    public RoomConfigurationResource(RoomNotificationManager roomNotificationManager) {
+        this.roomNotificationManager = roomNotificationManager;
+    }
+
+    @GET
+    public Response getRoom(@PathParam("project-key") String projectKey) {
+        final Integer roomForProjectId = roomNotificationManager.getRoomForProjectKey(projectKey);
+        final RoomConfigurationModel room = new RoomConfigurationModel(projectKey, roomForProjectId);
+        return Response.ok(room).build();
+    }
+
+    @POST
+    @Consumes({MediaType.APPLICATION_FORM_URLENCODED})
+    public Response setRoom(@PathParam("project-key") String projectKey, @FormParam("room") final int room) {
+        roomNotificationManager.setRoomForProjectKey(projectKey, room);
+        final RoomConfigurationModel model = new RoomConfigurationModel(projectKey, room);
+        return Response.ok(model).build();
+    }
+}

src/main/java/com/jonmort/stash/hipchat/rest/RoomListResource.java

+package com.jonmort.stash.hipchat.rest;
+
+import com.atlassian.sal.api.net.*;
+import com.jonmort.stash.hipchat.HipChatUriBuilder;
+import com.jonmort.stash.hipchat.RoomNotificationManager;
+import org.apache.commons.lang.StringUtils;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+@Path("/rooms")
+@Produces(MediaType.APPLICATION_JSON)
+public class RoomListResource {
+
+    private final RoomNotificationManager roomNotificationManager;
+    private final RequestFactory requestFactory;
+
+    public RoomListResource(RoomNotificationManager roomNotificationManager, RequestFactory requestFactory) {
+        this.roomNotificationManager = roomNotificationManager;
+        this.requestFactory = requestFactory;
+    }
+
+    @GET
+    public Response getRooms() {
+        final String apiKey = roomNotificationManager.getApiKey();
+        if (StringUtils.isBlank(apiKey)) {
+            return Response.noContent().build();
+        }
+        final Request request = requestFactory.createRequest(Request.MethodType.GET, HipChatUriBuilder.roomsList(apiKey).toASCIIString());
+        try {
+            RoomsModel roomsModel = (RoomsModel) request.executeAndReturn(RoomsModel.responseHandler());
+            return Response.ok(roomsModel).build();
+        } catch (ResponseException e) {
+            return Response.serverError().entity(e).build();
+        }
+    }
+
+}

src/main/java/com/jonmort/stash/hipchat/rest/RoomModel.java

+package com.jonmort.stash.hipchat.rest;
+
+import org.codehaus.jackson.annotate.JsonProperty;
+import org.codehaus.jackson.map.annotate.JsonSerialize;
+
+@JsonSerialize
+public class RoomModel {
+
+    @JsonProperty
+    private int room_id;
+    @JsonProperty
+    private String name;
+    @JsonProperty
+    private String topic;
+    @JsonProperty
+    private long last_active;
+    @JsonProperty
+    private long created;
+    @JsonProperty
+    private String owner_user_id;
+    @JsonProperty
+    private boolean is_archived;
+    @JsonProperty
+    private boolean is_private;
+    @JsonProperty
+    private String xmpp_jid;
+
+    public long getCreated() {
+        return created;
+    }
+
+    public void setCreated(long created) {
+        this.created = created;
+    }
+
+    public boolean isIs_archived() {
+        return is_archived;
+    }
+
+    public void setIs_archived(boolean is_archived) {
+        this.is_archived = is_archived;
+    }
+
+    public boolean isIs_private() {
+        return is_private;
+    }
+
+    public void setIs_private(boolean is_private) {
+        this.is_private = is_private;
+    }
+
+    public long getLast_active() {
+        return last_active;
+    }
+
+    public void setLast_active(long last_active) {
+        this.last_active = last_active;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getOwner_user_id() {
+        return owner_user_id;
+    }
+
+    public void setOwner_user_id(String owner_user_id) {
+        this.owner_user_id = owner_user_id;
+    }
+
+    public int getRoom_id() {
+        return room_id;
+    }
+
+    public void setRoom_id(int room_id) {
+        this.room_id = room_id;
+    }
+
+    public String getTopic() {
+        return topic;
+    }
+
+    public void setTopic(String topic) {
+        this.topic = topic;
+    }
+
+    public String getXmpp_jid() {
+        return xmpp_jid;
+    }
+
+    public void setXmpp_jid(String xmpp_jid) {
+        this.xmpp_jid = xmpp_jid;
+    }
+}

src/main/java/com/jonmort/stash/hipchat/rest/RoomsModel.java

+package com.jonmort.stash.hipchat.rest;
+
+import com.atlassian.sal.api.net.Response;
+import com.atlassian.sal.api.net.ResponseException;
+import com.atlassian.sal.api.net.ReturningResponseHandler;
+import org.codehaus.jackson.annotate.JsonProperty;
+import org.codehaus.jackson.map.annotate.JsonSerialize;
+
+import java.util.List;
+
+@JsonSerialize
+public class RoomsModel {
+    @JsonProperty
+    private List<RoomModel> rooms;
+
+    public List<RoomModel> getRooms() {
+        return rooms;
+    }
+
+    public void setRooms(List<RoomModel> rooms) {
+        this.rooms = rooms;
+    }
+
+    public static ReturningResponseHandler<Response, RoomsModel> responseHandler() {
+        return new RoomsModelResponseHandler();
+    }
+
+    private static class RoomsModelResponseHandler implements ReturningResponseHandler<Response, RoomsModel> {
+
+        @Override
+        public RoomsModel handle(Response response) throws ResponseException {
+            return response.getEntity(RoomsModel.class);
+        }
+    }
+}

src/main/resources/atlassian-plugin.xml

+<?xml version="1.0" encoding="UTF-8"?>
+
+<atlassian-plugin key="${project.groupId}.${project.artifactId}" name="${project.name}" plugins-version="2">
+    <plugin-info>
+        <description>${project.description}</description>
+        <version>${project.version}</version>
+        <vendor name="${project.organization.name}" url="${project.organization.url}"/>
+    </plugin-info>
+
+    <resource type="i18n" name="i18n" location="com.jonmort.stash.hipchat.stash-hipchat"/>
+
+    <rest name="Configuration Resource" i18n-name-key="configuration-resource.name" key="configuration-resource"
+          path="/stash-hipchat" version="1.0">
+        <description>Rest resourcs for configuration</description>
+    </rest>
+
+    <component key="notification-manager" class="com.jonmort.stash.hipchat.RoomNotificationManager"/>
+
+    <component-import key="requestFactory" interface="com.atlassian.sal.api.net.RequestFactory"/>
+    <component-import key="soyTemplateRenderer" interface="com.atlassian.soy.renderer.SoyTemplateRenderer"/>
+    <component-import key="applicationPropertiesService"
+                      interface="com.atlassian.stash.server.ApplicationPropertiesService"/>
+    <component-import key="pluginSettingsFactory"
+                      interface="com.atlassian.sal.api.pluginsettings.PluginSettingsFactory"/>
+
+    <web-item key="repo-config" name="Repository Config Link" section="stash.repository.settings.action" weight="10">
+        <condition class="com.jonmort.stash.hipchat.HipChatConfiguredCondition"/>
+        <description>Link to the repository configuration</description>
+        <label key="jonmort.hipchat-stash.repo-config.link"/>
+        <link/>
+        <styleClass>hipchat-repo-config-link</styleClass>
+    </web-item>
+
+    <web-item key="hipchat-config" name="HipChat Config Link" section="atl.admin/admin-plugins-section" weight="500">
+        <description>Link to the HipChat configuration</description>
+        <label key="jonmort.hipchat-stash.hipchat-config.link"/>
+        <link/>
+        <styleClass>hipchat-config-link</styleClass>
+    </web-item>
+
+    <stash-resource key="hipchat-admin-soy" name="Admin Soy Templates">
+        <directory location="/views/hipchat/admin/">
+            <exclude>/**/*-min.*</exclude>
+        </directory>
+        <context>internal.layout.admin</context>
+        <context>atl.admin</context>
+    </stash-resource>
+
+    <stash-resource key="hipchat-room-soy" name="Room Config Soy Templates">
+        <directory location="/views/hipchat/room/">
+            <exclude>/**/*-min.*</exclude>
+        </directory>
+        <context>atl.general</context>
+    </stash-resource>
+
+    <stash-resource key="hipchat-message-soy" name="Message Soy Templates">
+        <directory location="/views/hipchat/message/">
+            <exclude>/**/*-min.*</exclude>
+        </directory>
+        <dependency>com.atlassian.stash.stash-web-plugin:base-layout</dependency>
+    </stash-resource>
+
+    <web-resource key="admin-resources" name="Admin Resources">
+        <resource type="download" name="hipchat-admin.js" location="js/hipchat-admin.js"/>
+        <context>internal.layout.admin</context>
+        <context>atl.admin</context>
+        <dependency>com.atlassian.auiplugin:ajs</dependency>
+        <dependency>${project.groupId}.${project.artifactId}:hipchat-admin-soy</dependency>
+    </web-resource>
+
+    <web-resource key="room-resources" name="Room Config Resources">
+        <resource type="download" name="hipchat-rooms.js" location="js/hipchat-rooms.js"/>
+        <context>atl.general</context>
+        <dependency>com.atlassian.auiplugin:ajs</dependency>
+        <dependency>${project.groupId}.${project.artifactId}:hipchat-room-soy</dependency>
+    </web-resource>
+
+
+    <web-panel key="aui-message-area" weight="10" location="stash.notification.banner.header">
+        <resource name="view" type="velocity">
+            <![CDATA[
+                <div id="hipchat-stash-aui-message-bar"></div>
+            ]]>
+        </resource>
+    </web-panel>
+</atlassian-plugin>

src/main/resources/com/jonmort/stash/hipchat/stash-hipchat.properties

+
+jonmort.hipchat-stash.repo-config.link=HipChat
+jonmort.hipchat-stash.hipchat-config.link = HipChat Configuration

src/main/resources/js/hipchat-admin.js

+AJS.$(document).ready(function ($) {
+    var dialog = new AJS.Dialog({id:"hipchat-config-dialog", width:450, height:250, closeOnOutsideClick:true});
+
+    function save(dialog) {
+        var form = $('#' + dialog.id).find("form");
+        $.ajax({
+            url:form.attr('action'),
+            type:'POST',
+            data:form.serialize(),
+            dataType:'json',
+            success:function (data) {
+                AJS.messages.success("#hipchat-stash-aui-message-bar", {
+                    title:"HipChat Config Saved Successfully"
+                });
+            },
+            complete:function (data) {
+                dialog.hide();
+            },
+            error:function (jqXHR, textStatus, errorThrown) {
+                AJS.messages.error("#hipchat-stash-aui-message-bar", {
+                    title:"Could not save HipChat Config",
+                    body:textStatus
+                });
+            }
+        })
+    }
+
+    function cancel(dialog) {
+        dialog.hide();
+    }
+
+    dialog.addButton("Save", save);
+    dialog.addButton("Cancel", cancel);
+
+    var initAndShow = function () {
+        $.ajax({
+            url:AJS.contextPath() + "/rest/stash-hipchat/1.0/api",
+            type:'GET',
+            dataType:'json',
+            success:function (data) {
+                data.context = AJS.contextPath();
+                dialog.addPanel("HipChat Config", hipchat.stash.dialog(data));
+                dialog.show();
+            },
+            error:function (jqXHR, textStatus, errorThrown) {
+                AJS.messages.error("#hipchat-stash-aui-message-bar", {
+                    title:"Could not show HipChat Config",
+                    body:textStatus
+                });
+            }
+        });
+        initAndShow = function () {
+            dialog.show();
+        };
+    };
+
+
+    $("a.hipchat-config-link").on("click", function (e) {
+        e.preventDefault();
+        initAndShow();
+    });
+
+});

src/main/resources/js/hipchat-rooms.js

+AJS.$(document).ready(function ($) {
+
+    function save(dialog) {
+        $.ajax({
+            url:AJS.contextPath() + "/rest/stash-hipchat/1.0/room/" + $("#content").data("projectkey"),
+            type:'POST',
+            data:$(dialog).find("form").serialize(),
+            dataType:'json',
+            success:function (data) {
+                AJS.messages.success("#hipchat-stash-aui-message-bar", {
+                    title:"HipChat Config Saved Successfully"
+                });
+            },
+            complete:function (data) {
+                dialog.hide();
+            },
+            error:function (jqXHR, textStatus, errorThrown) {
+                AJS.messages.error("#hipchat-stash-aui-message-bar", {
+                    title:"Could not save HipChat Config",
+                    body:textStatus
+                });
+            }
+        })
+    }
+
+    var initAndShow = function (contents, showPopup) {
+        $.when(
+            $.ajax({
+                url:AJS.contextPath() + "/rest/stash-hipchat/1.0/rooms",
+                type:'GET',
+                dataType:'json'
+            }),
+            $.ajax({
+                url:AJS.contextPath() + "/rest/stash-hipchat/1.0/room/" + $("#content").data("projectkey"),
+                type:'GET',
+                dataType:'json'
+            })
+        ).then(function (roomsReq, configReq) {
+                var rooms = roomsReq[0];
+                rooms.selected = configReq[0].room_id;
+                $(contents).append(hipchat.stash.rooms.form(rooms));
+                showPopup();
+            },
+            function () {
+                AJS.messages.error("#hipchat-stash-aui-message-bar", {
+                    title:"Could not show HipChat Config"
+                });
+            });
+        initAndShow = function () {
+            showPopup();
+        };
+
+    };
+
+
+    var dialog = AJS.InlineDialog($("a.hipchat-repo-config-link"), "hipchat-room-config-dialog", function (contents, trigger, showPopup) {
+        initAndShow(contents, showPopup);
+    });
+
+    $(document).on("change", "#hipchat-room", function (e) {
+        save(dialog);
+        dialog.hide();
+    });
+
+});

src/main/resources/views/hipchat/admin/admin.soy

+{namespace hipchat.stash}
+
+/**
+ * @param apiKey A String api key
+ * @param username A String username to set
+ * @param context A String containing the context path
+ **/
+{template .dialog}
+<div>
+<form action="{$context}/rest/stash-hipchat/1.0/api" method="post" id="hipchat-api-config" name="hipchat-api-config" class="aui unsectioned">
+  <h2>HipChat Config</h3>
+  <fieldset>
+    <div class="field-group">
+        <label for="hipchat-api-key">HipChat Admin API Key<span class="aui-icon icon-required"></span></label>
+        <input name="apikey" id="hipchat-api-key" type="text" class="text" value="{$apiKey}" placeholder="HipChat Admin API Key" />
+    </div>
+    <div class="field-group">
+        <label for="hipchat-username">Username</label>
+        <input name="username" id="hipchat-username" type="text" class="text" value="{$username}" placeholder="Username for display in HipChat" />
+    </div>
+  </fieldset>
+</form>
+</div>
+{/template}

src/main/resources/views/hipchat/message/message.soy

+{namespace hipchat.stash.message}
+
+/**
+ * @param user A list of room maps
+ * @param project the project object
+ * @param projectUrl the url to the project
+ * @param repository The repository object
+ * @param repositoryUrl The url to the repository
+ **/
+{template .message}
+
+{$user} pushed to repository <a href="{$repositoryUrl}">{$repository.name}</a> of project <a href="{$projectUrl}">{$project.name}</a>
+{/template}

src/main/resources/views/hipchat/room/rooms.soy

+{namespace hipchat.stash.rooms}
+
+/**
+ * @param rooms A list of room maps
+ * @param selected An integer specifying the selected room
+ **/
+{template .form}
+<div style="padding:10px">
+    <form method="post" id="hipchat-api-config" name="hipchat-api-config" class="aui unsectioned">
+        <label for="hipchat-room"><h2>HipChat Room</h2></label>
+        <select class="select" id="hipchat-room" name="room">
+            <option value="" {if not $selected}selected="selected"{/if}>Disabled</disabled>
+            {foreach $room in $rooms}
+            <option value="{$room.room_id}" {if $room.room_id == $selected}selected="selected"{/if} tooltip="{$room.topic}">{$room.name} ({if $room.is_private}private{else}public{/if})</option>
+            {/foreach}
+        </select>
+    </form>
+</div>
+
+{/template}

src/test/java/testplugin/HipChatStashRepoConfigDialogPage.java

+package testplugin;
+
+import com.atlassian.pageobjects.elements.ElementBy;
+import com.atlassian.pageobjects.elements.PageElement;
+import com.atlassian.webdriver.stash.page.RepositoryEditSettingsPage;
+import com.atlassian.webdriver.stash.page.StashPage;
+
+import java.util.List;
+
+public class HipChatStashRepoConfigDialogPage extends RepositoryEditSettingsPage {
+    @ElementBy(id = "hipchat-room")
+    private PageElement roomList;
+
+    public HipChatStashRepoConfigDialogPage(String projectKey, String slug) {
+        super(projectKey, slug);
+    }
+
+}

src/test/java/testplugin/HipChatStashRepoConfigPage.java

+package testplugin;
+
+import com.atlassian.pageobjects.elements.ElementBy;
+import com.atlassian.pageobjects.elements.PageElement;
+import com.atlassian.webdriver.stash.page.RepositoryEditSettingsPage;
+
+public class HipChatStashRepoConfigPage extends RepositoryEditSettingsPage {
+     @ElementBy(className = "hipchat-repo-config-link")
+    private PageElement hipchatConfig;
+    public HipChatStashRepoConfigPage(String projectKey, String slug) {
+        super(projectKey, slug);
+    }
+
+    public HipChatStashRepoConfigDialogPage clickHipchatConfigButton() {
+        hipchatConfig.click();
+        return pageBinder.bind(HipChatStashRepoConfigDialogPage.class, projectKey, slug);
+    }
+}

src/test/java/testplugin/MockHipchatResource.java

+package testplugin;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+@Path("/rooms")
+@Produces(MediaType.APPLICATION_JSON)
+public class MockHipchatResource {
+
+    @GET
+    @Path("/list")
+    public Response rooms(@QueryParam("format") final String format, @QueryParam("auth_token") final String authToken) {
+        if (format.equals("json") && authToken != null)
+
+            // from https://www.hipchat.com/docs/api/method/rooms/list
+            return Response.ok("{\n" +
+                    "  \"rooms\": [\n" +
+                    "    {\n" +
+                    "      \"room_id\": 7,\n" +
+                    "      \"name\": \"Development\",\n" +
+                    "      \"topic\": \"Make sure to document your API functions well!\",\n" +
+                    "      \"last_active\": 1269020400,\n" +
+                    "      \"created\": 1269010311,\n" +
+                    "      \"owner_user_id\": 1,\n" +
+                    "      \"is_archived\": false,\n" +
+                    "      \"is_private\": false,\n" +
+                    "      \"xmpp_jid\": \"7_development@conf.hipchat.com\"\n" +
+                    "    },\n" +
+                    "    {\n" +
+                    "      \"room_id\": 10,\n" +
+                    "      \"name\": \"Ops\",\n" +
+                    "      \"topic\": \"Chef is so awesome.\",\n" +
+                    "      \"last_active\": 1269010500,\n" +
+                    "      \"created\": 1269010211,\n" +
+                    "      \"owner_user_id\": 5,\n" +
+                    "      \"is_archived\": false,\n" +
+                    "      \"is_private\": true,\n" +
+                    "      \"xmpp_jid\": \"10_ops@conf.hipchat.com\"\n" +
+                    "    }\n" +
+                    "  ]\n" +
+                    "}" +
+                    "}", MediaType.APPLICATION_JSON_TYPE).build();
+        else
+            return Response.status(Response.Status.BAD_REQUEST).build();
+    }
+
+    @POST
+    @Path("/message")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response message(@FormParam("message") final String message,
+                            @FormParam("room_id") final String roomId,
+                            @FormParam("from") final String from,
+                            @QueryParam("format") final String format,
+                            @QueryParam("auth_token") final String authToken
+    ) {
+        if (message != null && roomId != null && from != null && format.equals("json") && authToken != null)
+            // from https://www.hipchat.com/docs/api/method/rooms/message
+            return Response.ok("{\n" +
+                    "  \"status\": \"sent\"\n" +
+                    "}", MediaType.APPLICATION_JSON).build();
+        else
+            return Response.status(Response.Status.BAD_REQUEST).build();
+
+    }
+}

src/test/resources/atlassian-plugin.xml

+<?xml version="1.0" encoding="UTF-8"?>
+
+<atlassian-plugin key="hip-chat-stash-test-plugin" name="HipChat Stash Test Plugin" plugins-version="2">
+    <plugin-info>
+        <description>${project.description}</description>
+        <version>${project.version}</version>
+        <vendor name="${project.organization.name}" url="${project.organization.url}"/>
+    </plugin-info>
+
+    <resource type="i18n" name="i18n" location="com.jonmort.stash.hipchat.stash-hipchat"/>
+
+    <rest name="Mock Hipchat" key="mock-resource" path="/mock-hipchat" version="1.0">
+    </rest>
+
+</atlassian-plugin>
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.