Soy template form validation

Josh Wheeler
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
July 21, 2014

Hi all,

I've written a soy template that I'd like to validate before I call doPost. Is there something available in a built-in library that can perform the validation for me (ie: required fields)? And if so, how do I need to modify my atlassian-plugin.xml file and where can I find an example of a validation. If this is not an option, the closest match that I could find on how to include js properly was here - Is there a way to use the existing soy to display an error message?

Also, assuming there was an error processing the request in the Java backend, is there a way to display an error using the soy temlpate?

MainServlet.java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.atlassian.sal.api.pluginsettings.PluginSettingsFactory;
import com.atlassian.soy.renderer.SoyException;
import com.atlassian.soy.renderer.SoyTemplateRenderer;
import com.atlassian.stash.hook.repository.AsyncPostReceiveRepositoryHook;
import com.atlassian.stash.hook.repository.RepositoryHookContext;
import com.atlassian.stash.repository.RefChange;
import com.atlassian.stash.repository.Repository;
import com.atlassian.stash.repository.RepositoryCloneLinksRequest;
import com.atlassian.stash.repository.RepositoryService;
import com.atlassian.stash.setting.RepositorySettingsValidator;
import com.atlassian.stash.setting.Settings;
import com.atlassian.stash.setting.SettingsValidationErrors;
import com.atlassian.stash.util.NamedLink;
import com.google.common.collect.ImmutableMap;

import javax.annotation.Nonnull;
import javax.servlet.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import java.util.Set;

@SuppressWarnings({"serial", "unused"})
public class MainServlet extends HttpServlet implements AsyncPostReceiveRepositoryHook, RepositorySettingsValidator{
    private static final Logger log = LoggerFactory.getLogger(MainServlet.class);
    
    private final RepositoryService repositoryService;
    private final PluginSettingsFactory pluginSettingsFactory;
    
    protected final SoyTemplateRenderer soyTemplateRenderer;
	protected final String completeModuleKey = "org.lds.alm.anthillbuild:anthill-soy-templates"; 
    
    private boolean debug = true;
        
    public MainServlet(SoyTemplateRenderer soyTemplateRenderer, RepositoryService repositoryService, PluginSettingsFactory pluginSettingsFactory){
    	this.repositoryService = repositoryService;
        this.pluginSettingsFactory = pluginSettingsFactory;
        this.soyTemplateRenderer = soyTemplateRenderer;
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
    {
    	String[] components = req.getPathInfo().split("/");
    	if(components.length < 3){
    		if(debug) resp.sendError(HttpServletResponse.SC_NOT_FOUND, "components.length < 3");
    		else resp.sendError(HttpServletResponse.SC_NOT_FOUND);
    		return;
    	}
    	
    	Repository repository = repositoryService.getBySlug(components[1], components[2]);
    	
    	if(repository == null){
    		if(debug) resp.sendError(HttpServletResponse.SC_NOT_FOUND, "repository == null");
    		else resp.sendError(HttpServletResponse.SC_NOT_FOUND);
    		return;
    	}
    	    	
    	//Get SSH URL
    	Set<NamedLink> lnk = repositoryService.getCloneLinks(new RepositoryCloneLinksRequest.Builder().repository(repository).protocol("ssh").build());
    	String cloneUrl = lnk.iterator().next().getHref();

    	String templateName = "anthill.ui.main";

    	render(resp, templateName, ImmutableMap.<String, Object> builder()
    			.put("repository", repository)
    			.put("ssh", cloneUrl)
    			.put("projectName", "(projectName here)")
    			.build()
    			);
    }
    
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
    {
    	System.out.println("doPost");
    	doGet(req, resp);
    }
    
    protected void render(HttpServletResponse resp, String templateName, Map<String, Object> data) throws IOException, ServletException {
		render(resp, templateName, data, null);
	}
	
	protected void render(HttpServletResponse resp, String templateName, Map<String, Object> data, Map<String, Object> injectedData) throws IOException, ServletException{
		resp.setContentType("text/html;charset=UTF-8");

        try {
        	if(injectedData == null){        	
        		soyTemplateRenderer.render(resp.getWriter(), completeModuleKey, templateName, data);
        	}
        	else{
        		soyTemplateRenderer.render(resp.getWriter(), completeModuleKey, templateName, data, injectedData);
        	}
        } catch (SoyException e) {
            Throwable cause = e.getCause();
            if (cause instanceof IOException) {
                throw (IOException) cause;
            }
            throw new ServletException(e);
        }
	}

	@Override
	public void validate(Settings settings, SettingsValidationErrors errors, Repository repository) {
		errors.addFormError("testing errors");
		if(settings.getString("projectName").equalsIgnoreCase("value"))
			errors.addFieldError("projectName", "project name equals value");
	}

	@Override
	public void postReceive(RepositoryHookContext arg0,	Collection<RefChange> arg1) {
		System.out.println("");		
	}

}

Anthill.soy

{namespace anthill.ui}

/**
 * UI for Anthill/Stash
 * @param repository Repository
 * @param ssh Link
 * @param? errors Errors (if applicable)
 */
{template .main}
<html>
	<head>
		<meta name="decorator" content="stash.repository.settings">
    	<meta name="repositorySlug" content="{$repository.slug}">
    	<meta name="projectKey" content="{$repository.project.key}">
    	<meta name="activeTab" content="repository-settings-plugin-tab">
	</head>
	<title>
		{$ssh}
	</title>
	<body>
		<h2>My Form</h2>
		<div class="aui">
			{call aui.form.form}
				{param action: ''/}
				{param content}
					{call aui.form.textField}
				        {param id: 'projectName' /}
				        {param value: 'Value' /}
				        {param labelContent: 'Project Name' /}
				        {param isRequired: true /}
				        {param extraClasses: 'long' /}
				        {param errorTexts: $errors ? 'there was an error' : null /}
				    {/call}
				    {call aui.form.submit}
						{param id: 'submit' /}
				  		{param type: 'submit' /}
				  		{param text: 'Create' /}
				    {/call}
			    {/param}
		    {/call}
		</div>
	</body>
</html>
{/template}

4 answers

1 accepted

Comments for this post are closed

Community moderators have prevented the ability to post new answers.

Post a new question

0 votes
Answer accepted
Balázs Szakmáry
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
July 22, 2014

Not sure if I get the question right, but if you simply want to make sure that required fields are filled out before the form can be submitted, you can do it with

{param isRequired: 'true' /}

in the .soy file, like this:

{namespace com.tekla.stash.myplugin}
/**
 * @param config
 * @param? errors
 */
{template .formContents}
    {call aui.form.textField}
        {param id: 'project' /}
        {param value: $config['project'] /}
        {param labelContent}
            {stash_i18n('com.tekla.stash.myplugin.config.label', 'Project name')}
        {/param}
        {param isRequired: 'true' /}
        {param descriptionText: stash_i18n('com.tekla.stash.myplugin.config.description', 'Description.') /}
        {param extraClasses: 'long' /}
        {param errorTexts: $errors ? $errors['project'] : null /}
    {/call}
    {call aui.form.textField}
        {param id: 'repo' /}
        {param value: $config['repo'] /}
        {param labelContent}
            {stash_i18n('com.tekla.stash.myplugin.config.label', 'Repo name')}
        {/param}
        {param isRequired: 'true' /}
        {param descriptionText: stash_i18n('com.tekla.stash.myplugin.config.description', 'Description.') /}
        {param extraClasses: 'long' /}
        {param errorTexts: $errors ? $errors['repo'] : null /}
    {/call}
{/template}

Josh Wheeler
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
July 22, 2014

@BalazsYou've hit it right on the head. I wasn't aware of the errorTexts param. The next question I'd have to ask is how to add content to the errors param? Is there a Java function provided by Stash that I can use to add custom errors and then display them? I recently stumbled across this question which is exactly what I'm trying to do but I don't understand how to get the errors back to the form.

Balázs Szakmáry
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
July 22, 2014

You do it with SettingsValidationErrors.addFormError and SettingsValidationErrors.addFieldError, like this:

/**
     * Validates the parent URL on the configuration form.
     * @param settings contains the values from the config form
     * @param errors validation errors need to be put into this object
     * @param repository the repo for which the hook is being configured
     */
    @Override
    public void validate(@Nonnull Settings settings, @Nonnull SettingsValidationErrors errors, @Nonnull Repository repository) {
        String projectName = settings.getString("project");
        String repoName = settings.getString("repo");
        if (null == projectName) {
            errors.addFormError("The project field is not defined on the config form.");
            return;
        }
        if (null == repoName) {
            errors.addFormError("The repo field is not defined on the config form.");
            return;
        }
        if (projectName.isEmpty()) {
            errors.addFieldError("project", "Please supply a project name.");
        }
        if (repoName.isEmpty()) {
            errors.addFieldError("repo", "Please supply a repo name.");
        }
        Project proj = projectService.getByName(projectName);
        if (null == proj) {
           errors.addFieldError("project", "There is no such project.");
        }
        else if (null == repositoryService.getBySlug(proj.getKey(), repoName)) {
            errors.addFieldError("repo", "There is no such repo in the given project.");
        }
    }

Deleted user July 22, 2014

You do it with SettingsValidationErrors.addFormError and SettingsValidationErrors.addFieldError, like this:

public void validate(@Nonnull Settings settings, @Nonnull SettingsValidationErrors errors, @Nonnull Repository repository) {
        String projectName = settings.getString("project");
        String repoName = settings.getString("repo");
        if (null == projectName) {
            errors.addFormError("The project field is not defined on the config form.");
            return;
        }
        if (null == repoName) {
            errors.addFormError("The repo field is not defined on the config form.");
            return;
        }
        if (projectName.isEmpty()) {
            errors.addFieldError("project", "Please supply a project name.");
        }
        if (repoName.isEmpty()) {
            errors.addFieldError("repo", "Please supply a repo name.");
        }
        Project proj = projectService.getByName(projectName);
        if (null == proj) {
           errors.addFieldError("project", "There is no such project.");
        }
        else if (null == repositoryService.getBySlug(proj.getKey(), repoName)) {
            errors.addFieldError("repo", "There is no such repo in the given project.");
        }
    }

Josh Wheeler
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
July 23, 2014

So I'll have to imlpement the Settings, SettingsValidationErrors, Repository, and RepositorySettingsValidator interfaces - correct? Once I go through that mess, do I just call RepositorySettingsValidator.validate in my doPost inside of my repository servlet?

Balázs Szakmáry
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
July 23, 2014

It is simpler than that. Your class has to implement RepositorySettingsValidator and override the validate method. I updated the code snippet above, the @override and the XML comments were missing. The validate method will be called by Stash at the right time, no need to worry about that.

Josh Wheeler
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
July 27, 2014

@BalazsSorry, I was out of town for the weekend.

I created a class that implements RepositorySettingsValidator (below) but when my doPost is called from my servlet class, validate is never called. I included a validator in my atlassian-plugin.xml but unfortunately that didn't solve the problem.

atlassian-plugin.xml

<servlet name="Main Servlet" i18n-name-key="main-servlet.name" key="main-servlet" class="com.example.servlet.MainServlet">
    <description key="main-servlet.description">The Main Servlet Plugin</description>
    <url-pattern>/mainservlet/*</url-pattern>
    <validator>com.example.servlet.Validation</validator>  
</servlet>

Validation.java

package com.example.servlet;

import javax.annotation.Nonnull;

import com.atlassian.stash.repository.Repository;
import com.atlassian.stash.setting.RepositorySettingsValidator;
import com.atlassian.stash.setting.Settings;
import com.atlassian.stash.setting.SettingsValidationErrors;

public class Validation implements RepositorySettingsValidator{

   /**
     * Validates the parent URL on the configuration form.
     * @param settings contains the values from the config form
     * @param errors validation errors need to be put into this object
     * @param repository the repo for which the hook is being configured
     */
    @Override
    public void validate(@Nonnull Settings settings, @Nonnull SettingsValidationErrors errors, @Nonnull Repository repository) {
    	errors.addFormError("testing errors"); //test error
	if(settings.getString("projectName").equalsIgnoreCase("value"))
		errors.addFieldError("projectName", "project name equals value");
    }	
}

I know I'm missing something small - I'm just not sure what it is.

Balázs Szakmáry
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
July 30, 2014

The RepositorySettingsValidator interface has to be implemented by your main plugin class, not a separate one:

public class MyClass implements AsyncPostReceiveRepositoryHook, RepositorySettingsValidator  {
...
    @Override
    public void validate(@Nonnull Settings settings, @Nonnull SettingsValidationErrors errors, @Nonnull Repository repository) {
    ...
    }
}

Josh Wheeler
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
July 31, 2014

Honestly, this is becoming very frustrating. I don't understand how the validation works via creating your own form as a servlet. Above I've included the contents of my servlet class as it stands as well as the soy file. I've inserted breakpoints in the validate and postReceive functions and they're never called. Is what I'm trying to accomplish even possible?

Balázs Szakmáry
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
August 3, 2014

Try checking your code against this example. Something is not wired together correctly.

0 votes
Yagnesh Bhat
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
July 22, 2014

Oh ok, I am not sure about stash.

0 votes
Josh Wheeler
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
July 22, 2014

Thank you for the quick response Yagnesh. I read through the documentation and it's good for Confluence, but can I follow the same tutorials for Stash? I added a line for the js file in my atlassian-plugin.xml (below) and the AJS.toInit method to my main.js file but it's not called when the page loads.

atlassian-plugin.xml (Stash):

<web-resource key="anthill-resources" name="Anthill Build Web Resources">
	<dependency>com.atlassian.auiplugin:ajs</dependency>	
        <transformation extension="soy">
            <transformer key="soyTransformer" />
        </transformation>

        <resource type="download" name="main.js" location="js/main.js" />
        <resource type="download" name="anthill.soy.js" location="/templates/anthill.soy"/>
        <!--  -->
</web-resource>

0 votes
Yagnesh Bhat
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
July 21, 2014

Hi Josh,

The soy templates are actually rendered as javascript in the atlassian-plugin.xml, which means they are actually defined as a javascript file in the web-resource element, like so

<resource type="download" name="someform.soy.js" location="/soy/someform.soy" />

With that being said, confluence provides APIs for doing some stuff whenever the soy template is rendered. This is mostly used in context of blueprint wizards:

https://developer.atlassian.com/display/CONFDEV/Javascript+API+for+blueprint+wizards

As given in the doc above, you can do the validation in the submit method of the blueprint. To sum up you can do the following steps (Refer teh doc for more info)

1) Create a javascript file and define it in your web-resource.

2) The basic structure of your js would be like this:

AJS.toInit(function($) {

         function postRender(e, state) {



         }



         function validate() {





        }

   

         function submit(e, state) {

            // Call validate from here



        }





        //Do Confluence.Blueprint.setWizard here



});

Also if you want to do any server validation, u can do it via AJAX calls in the body of validate function provided in the code snippet above. One common way to do that would be to configure your own Action classes. More on that here :

https://developer.atlassian.com/display/CONFDEV/Adding+a+Custom+Action+to+Confluence

Comments for this post are closed

Community moderators have prevented the ability to post new answers.

Post a new question

TAGS
AUG Leaders

Atlassian Community Events