HTML

JIRA DEV

Az Atlassian termékek felhasználási területei, paraméterezésük és alkalmazásfejlesztési lehetőségeik. Esettanulmány és érdekességek a JIRA-ról, Confluence-ről, GreenHopper-ről és a hozzájuk kapcsolódó plugin fejlesztésről, egy kevés Scrum módszertannal fűszerezve.

Kérdéseket és észrevételeidet a lacimol kukatc gmail pont com címre tudod elküldeni.

Üdv:
lacimol ()

Legutóbbi bejegyzések

Nincs megjeleníthető elem

Címkék

JiraDevTutor

  • Hibás feed URL.

Friss topikok

09.
szeptember

Jira plugin fejlesztés 8.

PluginSettings |  lacimol

Beállítások mentése

A korábbi SAL Scheduling példa szerint az időzítés már megfelelő, de egyelőre kódba égetett értéket használtunk az intervallumra. Ha ezt az action hívásával módosítottuk volna (reschedule), akkor a következő szerver újraindításkor a rendszer elfelejtette volna amit megadtunk. Ennek kezelésére a módosításokat adatbázisba menthetjük saját (ActiveObject) vagy a JIRA által kínált megoldással (PluginSettings).

Időzítő felület

Az időzítés ismétlődési ciklusának megadásához befejezzük a JIRA példa második felének Todo-ba integrálását. Így a SchedulerAction hívásával átállítható lesz az intervallum.

SchedulerAction.java

public class SchedulerAction extends JiraWebActionSupport {
	
	private static final long serialVersionUID = -1993489970401317470L;
	
	private final TodoMonitor todoMonitor;
	private long interval;
	
	public SchedulerAction(TodoMonitor todoMonitor, PluginSettingsFactory pluginSettingsFactory) {
		this.todoMonitor = todoMonitor;
		this.interval = todoMonitor.getInterval();
	}
	
	@Override
	protected String doExecute() throws Exception {
		return SUCCESS;
	}
	
	public String doReschedule() {
		this.todoMonitor.reschedule(this.interval);
		return getRedirect("TodoScheduler.jspa");
	}
	
	public long getInterval() {
		return this.interval;
	}
	
	public void setInterval(long interval) {
		this.interval = interval;
	}
	
	public Date getLastRun() {
		return this.todoMonitor.getLastRun();
	}
	
	public Date getNextRun() {
		return this.todoMonitor.getNextRun();
	}
	
	public int getTodoSize() {
		return this.todoMonitor.getTodoSize();
	}
	
	public SimpleDateFormat getHunDateFormatter() {
		return GlobalSettings.createHunDateFormatter();
	}
	
}

PluginScheduler.java

private final PluginScheduler pluginScheduler;
	private final TodoService todoService;
	private final GlobalSettings settings;
	
	private final HashMap<String, Object> jobDataMap = new HashMap<String, Object>();
	
	private Date lastRun;
	private Date nextRun;
	private long interval = 0;
	
	public TodoMonitorImpl(PluginScheduler pluginScheduler, TodoService todoService,
			PluginSettingsFactory pluginSettingsFactory) {
		
		this.pluginScheduler = pluginScheduler;
		this.todoService = todoService;
		this.settings = new GlobalSettings(pluginSettingsFactory.createGlobalSettings());
		
		Long loadInterval = this.settings.loadInterval();
		if (loadInterval != null) {
			this.interval = loadInterval;
		}
		
	}
	
	@Override
	public void onStart() {
		
		this.jobDataMap.put("TodoMonitorImpl:instance", TodoMonitorImpl.this);
		this.jobDataMap.put("TodoService", this.todoService);
		
		if (this.interval > 0) {
			schedule();
		}
	}
	
	@Override
	public void reschedule(long interval) {
		this.interval = interval;
		this.settings.storeInterval(interval);
		this.schedule();
	}

PluginSettings

A beállításokat akár a PluginSettingsFactory-n keresztül is közvetlenül elérhetjük, de érdemes a funkciót kiemelni. Így az átállított értéket a GlobalSettings osztályon keresztül tölthetjük be és menthetjük el, ezzel leválasztva a beállított értékek kezelését a kód többi funkciójától.

GlobalSettings,java

public class GlobalSettings {
	
	private final PluginSettings pluginSettings;
	
	public GlobalSettings(PluginSettings pluginSettings) {
		this.pluginSettings = pluginSettings;
	}
	
	public Long loadLong(String key) {
		Object object = this.pluginSettings.get(key);
		if (object == null) {
			return null;
		}
		return Long.valueOf((String) object);
	}
	
	public Long loadInterval() {
		return this.loadLong("todo.monitor.interval");
	}
	
	public void storeInterval(Long interval) {
		this.pluginSettings.put("todo.monitor.interval", String.valueOf(interval));
	}
	
	public static SimpleDateFormat createHunDateFormatter() {
		return new SimpleDateFormat("yyyy.MM.dd. HH:mm");
	}
}

Plugin konfigurációs hivatkozás

A pluginhez tartozó beállítási oldalt kiemelhetjük a JIRA admin oldalára is a configure.url paraméter megadásával. Itt az időzítés beállításához használt TodoScheduler.jspa oldalt adjuk meg.

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}
		<version>${project.version}
		<vendor name="${project.organization.name}" url="${project.organization.url}" />
		<param name="plugin-icon">images/pluginIcon.png</param>
		<param name="plugin-logo">images/pluginLogo.png</param>
		<param name="configure.url">/secure/TodoScheduler.jspa</param>
	</plugin-info>
	...
</atlassian-plugin>

Az admin oldalról a plugin beállításokat megnyitva vagy közvetlenül elérve (http://localhost:2990/jira/plugins/servlet/upm) a Configure linkre kattintva érhetjük el a plugin beállításokat.

pluginsettings_conf_2.png

Eredmény

A szerver indítása után még nincs beállított érték, de ha az időzítő felületen megadunk egyet, akkor az "Apply" után visszatérve már látjuk a következő futtatási időt (Legközelebb). Ha lefutott az első task, utána már a "Legutóbb" dátum is látható, és ezzel egy időben nő a rekordok száma is.

sal_scheduler.png

Apró hiba a működésben: Az időzítés első futásakor ismétlődően fut le a task, amit a 2.7.x verzióban még nem javítottak.

A fenti módosítások letölthetők SVN-en keresztül és becsomagolva a blog GoogleCode oldaláról a JiraDevTutor projektből.

Címkék: programozás scheduling PluginSettings

29.
július

Jira plugin fejlesztés 7.

időzített feladatok - SAL scheduling |  lacimol

Mi az a SAL?

A SAL a Shared Access Layer rövidítése. Segítségével Atlassian termékfüggetlen plugin kód írható, azaz nem kifejezetten a JIRA vagy Confluence alatt működő, hanem mindkettő által használható közös elem lehet (pl: időzített feladatok).

Ismétlődő feladatok kezelése

Az időzített taszkok egy felhasználási módját megmutatja a JIRA scheduling példa, amit kisebb átalakítással a Todo példánkba is felhasználhatunk. Akkor érdemes ilyen időzítést használunk, ha nagy számítás vagy időigényű háttérfeladatot futtatunk vagy nincs jelentősége az eredmény azonnali megjelenítésének (pl: nagy adathalmazok összefésülése, rendszer monitorozás), esetleg egy bizonyos feladatot időponthoz kötötten vagy ismételődően kell megvalósítanunk (pl: éjszakai adatátadások). A saját példába átültetett kód által végzett feladat nem ilyen, csak az időzítés bemutatását szolgálja.

JIRA scheduling

A plugin SDK ad egy alap szerkezetet az időzítés implementálásához, így nem kell Quartz job-okat megírnunk, hanem használhatjuk a belső megoldásokat. A Todo példában beállítunk egy percenkénti ismétlődést, ami létrehoz egy új rekordot a Todo táblában, majd az INFO logba kiírja, hogy lefutott a taszk. A TodoTask végzi az ismétlődő feladatot, a TodoMonitorImpl pedig időzít és eltárolja az esetleges eredményeket.

TodoTask.java

public class TodoTask implements PluginJob {
	
	private final static Logger logger = LoggerFactory.getLogger(TodoTask.class);
	
	@Override
	public void execute(Map<String, Object> jobDataMap) {
		
		final TodoMonitor monitor = (TodoMonitor) jobDataMap.get("TodoMonitorImpl:instance");
		final TodoService service = (TodoService) jobDataMap.get("TodoService");
		
		Date now = DateTime.now().toDate();
		service.create("TodoTask description " + now.getTime());
		monitor.setLastRun(now);
		logger.info("TodoTask executed at " + monitor.getLastRun());
	}
}

TodoMonitorImpl.java

public class TodoMonitorImpl implements TodoMonitor, LifecycleAware {
	
	private final static Logger logger = LoggerFactory.getLogger(TodoMonitorImpl.class);
	private static final String JOB_NAME = TodoMonitorImpl.class.getName() + ":job";
	private static final long INTERVAL = DateTimeConstants.MILLIS_PER_MINUTE;
	
	private final PluginScheduler pluginScheduler;
	private TodoService todoService;
	private Date lastRun;
	
	private HashMap<String, Object> jobDataMap = new HashMap<String, Object>();
	
	public TodoMonitorImpl(PluginScheduler pluginScheduler, TodoService todoService) {
		this.pluginScheduler = pluginScheduler;
		this.todoService = todoService;
	}
	
	@Override
	public void schedule() {
		
		jobDataMap.put("TodoMonitorImpl:instance", TodoMonitorImpl.this);
		jobDataMap.put("TodoService", todoService);
		
		Date startTime = DateTime.now().toDate();
		pluginScheduler.scheduleJob(JOB_NAME, TodoTask.class, jobDataMap, startTime, INTERVAL);
		
		logger.info(String.format("TodoMonitorImpl scheduled to run every %d ms", INTERVAL));
		
	}
	
	@Override
	public void onStart() {
		schedule();
	}
	
	@Override
	public void setLastRun(Date lastRun) {
		this.lastRun = lastRun;
	}
	
	@Override
	public Date getLastRun() {
		return lastRun;
	}
	
}
[INFO] hu.lacimol.tutorial.todo.scheduling.TodoMonitorImpl - TodoMonitorImpl scheduled to run every 60000 ms
[INFO] Starting Coyote HTTP/1.1 on http-2990
[DEBUG] net.java.ao.sql - INSERT INTO AO_595B84_TODO (ID) VALUES (NULL)
[DEBUG] net.java.ao.sql - UPDATE PUBLIC.AO_595B84_TODO SET DESCRIPTION = ?,COMPLETE = ? WHERE ID = ?
[INFO] hu.lacimol.tutorial.todo.scheduling.TodoTask - TodoTask executed at Wed Jul 17 21:07:53 CEST 2013
[INFO] Server startup in 68986 ms
[INFO] Tomcat 6.x started on port [2990]
[INFO] jira started successfully in 113s at http://Laci-PC:2990/jira
[INFO] Type Ctrl-D to shutdown gracefully
[INFO] Type Ctrl-C to exit
[DEBUG] net.java.ao.sql - INSERT INTO AO_595B84_TODO (ID) VALUES (NULL)
[DEBUG] net.java.ao.sql - UPDATE PUBLIC.AO_595B84_TODO SET DESCRIPTION = ?,COMPLETE = ? WHERE ID = ?
[INFO] hu.lacimol.tutorial.todo.scheduling.TodoTask - TodoTask executed at Wed Jul 17 21:08:52 CEST 2013

Az eredmény

Két perc futás után a lenti lista két új elemmel bővült:

scheduilng_view.png

A fenti módosítások letölthetők SVN-en keresztül és becsomagolva a blog GoogleCode oldaláról a JiraDevTutor projektből.

Címkék: jira sal scheduling

15.
július

Jira plugin fejlesztés 6.

CRUD műveletek |  lacimol

Mi is az a CRUD?

A CRUD egy mozaikszó az alapvető adattárolási műveletekre: Create, Retrieve (vagy Read), Update és Delete. Azaz létrehozás, lekérdezés, módosítás és törlés.

Hol tartunk most?

A legutóbbi plugin fejlesztés posztban (JIRA Active Objects) létrehoztam egy rekordot, majd  a plugin megjelenítette a többivel együtt egy listában, mindezt egy oldalon. Így a CRUD műveletekből a C-t és az R-t már megnéztük. A JIRA dokumentációk közül egy CRUD tutor leírás és egy elérhető SVN plugin kód együttese jól mutatja az adatbázis, a megjelenítés és a háttérműveletek kapcsolatát. Ezek ötvözetével folytatom a ToDo entitásra épülő miniprojektet.

CRUD Action

A JIRA által létrehozott CRUD példa jól mutatja a lehetőségeket, de szervletet használ, így a felelősségi szintek nem válnak szét teljesen. A két példa összefésülésével a következő csomagszintű bontást használom: action (request-ek kezelése), model (adatszerkezet), service (DAO műveletek összefogása). A megjelenítéshez szükséges velocity fájlok továbbra is a templates alatt vannak.

crud_package.png

Az Action-ök a TodoAction-ből származnak, ahol a TodoService-en keresztül mentjük és kapjuk vissza az adatokat. A TodoAction végzi az alapvető és ismétlődő validálási folyamatokat is.

TodoAction.java részlet

public class TodoAction extends JiraWebActionSupport {

	protected final static String VIEW_PAGE = "ViewTodoAction.jspa";

	protected final TodoService todoService;

	public TodoAction(TodoService todoService) {
		this.todoService = checkNotNull(todoService);
	}

	protected void doKeyValidation() {
		log.debug("Entering doKeyValidation");
		final String id = request.getParameter("key");
		if (id == null || id.isEmpty()) {
			//?key=
			addErrorMessage(getText("todo.form.id.missing"));
		} else {
			try {
				todo = this.todoService.findById(Integer.valueOf(id));
				todo.getDescription();
			} catch (NumberFormatException e) {
				//?key=aaa
				addErrorMessage(getText("todo.form.id.wrong.format"));
			} catch (NullPointerException e) {
				//?key=111
				addErrorMessage(getText("todo.form.not.found"));
			}

		}
	}

	protected void doFieldValidation() {
		final String description = request.getParameter("description");
		if (description == null || description.isEmpty()) {
			addErrorMessage(getText("todo.form.description.missing"));
		}
		int maxLength = 50;
		if (description != null && description.length() > maxLength) {
			addErrorMessage(getText("todo.form.description.too.long"));
		}
	}

	protected Todo todo;
	public Todo getTodo() {
		return todo;
	}

}

TodoService.java részlet

@Transactional
public interface TodoService {

	Todo create(String description);

	List<todo> findAll();

	List<todo> find(String description);

	Todo findById(int id);

	void delete(int id);

	void addComment(Todo todo, String text);

}

A végeredmény

A fejlesztés eredménye egy kiindulási oldal (lista), ahonnan indíthatunk egy keresést a description mező teljes egyezésére (R), törölhetjük (D) vagy szerkeszthetjük (U) az eddig felvitt rekordokat vagy újat hozhatunk létre (C).

ViewTodoAction_list_2.png

A fenti módosítások letölthetők SVN-en keresztül és becsomagolva a blog GoogleCode oldaláról a JiraDevTutor projektből.

Címkék: crud webwork

03.
június

Confluence ékezetek

Magyar szöveges ékezetek kezelése az URL-ben |  lacimol

Hivatkozás megjelenítése

A Confluence-ben az oldalak és blogok létrehozása után a megadott címből generálódik az elérési út, pl: az "Ekezet nelkuli szoveg"-hez a  "/display/COM/Ekezet+nelkuli+szoveg" olvasható elérés. Ez egészen addig jól működik, amíg rövid szöveget és angol karaktereket (pontosabban ASCII-t) használunk. A magyar ékezeteknél az elérési út a fenti könnyen olvasható megoldástól eltérően nem a cím, hanem csak egy pageId alapján tölti be a hivatkozott oldalt, pl: az "Ékezetes szöveg"-nél "/pages/viewpage.action?pageId=123456". Ennek az oldal használata közben nincs különösebb jelentősége, de ha linkelnénk egy külső oldalra vagy e-mailben küldjük, akkor szebb és olvashatóbb, ha látszik a cím (és a space: COM) is. Publikus szervereknél a keresési találatokhoz szükséges indexelés is jobb lehet egy olvasható linkkel (pl: Google találatok).

confluence_pages.png

Az ékezetek normalizálása

A Page és a BlogPost osztály is az AbstractPage-ből származik, aminek a "getUrlPath()" metódusa adja azt a hivatkozást, amit a Confluence oldalakon láthatunk. Magát az URL generálást az innen meghívott GeneralUtil.getPageUrl(AbstractPage page) metódusa végzi két egymástól eltérő módon:

  • toDisplayUrl: ha a cím (title) megfelelő és az adott oldal a legkésőbbi, akkor a "szép" URL-t adja
  • getIdBasedPageUrl: ha ez nem teljesük, akkor a pageId szerintit

A feltételeket az "isSafeTitleForUrl()"-ben találjuk, melyek szerint:

  • nem lehet a cím üres és 150 karakternél hosszabb
  • nem végződhet írásjellel
  • nem tartalmazhat illegális karaktereket: '+', '?', '%', '&', '"', '/', '\\', ';'
  • nem lehet benne nem ASCII karakter

A magyar ékezetek pageId szerinti kezelését ez utóbbi okozza. A problémát a cím normalizálásával oldhatjuk meg:

private static String decompose(String input) {
	return Normalizer.normalize(input, Normalizer.Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
}

Ékezetes oldal megjelenítése

A linkek beszédesé alakítása csak a feladat egyik része, mert ha így kattintunk rájuk, akkor nem tölti be a kívánt oldalt (PageNotFoundAction). Ennek oka, hogy a request-ek feldolgozásánál (PageAwareInterceptor) is két módon történik az adott oldal keresése: pageId vagy cím (+ space) alapján. A gondot az okozza, hogy az ékezetek nélküli linkből kivett cím (title) alapján nem lesz egyezés az adatbázisban az ékezetes címre. A legegyszerűbb megoldás, ha az ilyen normalizált URL-ek mellé betesszük a pageId-t is, így beszédes link mellett az oldal is egyértelműen azonosítható. Ennél szebb lenne, ha magát a DAO-t írnánk át, úgy hogy megtalálja az ékezetes rekordokat is (az ékezet nélküli címmel), de ezt majd a Confluence fejlesztők megoldják :)

GeneralUtil.java részlet

public static String getPageUrl(AbstractPage page) {
    if (page == null || (page.getOriginalVersion() == null && page.getSpace() == null)) {
        return "";
    }

    String title = page.getTitle();

    // only use simple/nice page url if the page does not contain:
    // - non-ASCII characters
    // - '+' or '-' characters because these can be picked up by the insert and stripe through filters
    // - double quotes (") because orion doesn't play nicely with them (CONF-1287)
    // - ends in punctuation (CONFDEV-3995)
    if (isSafeTitleForUrl(title) && page.isLatestVersion()) {
        return toDisplayUrl(page);
    } else if (isSafeTitleForNormalize(title) && page.isLatestVersion()) {
        return toDisplayNormalizedUrl(page);
    } else {
    	return getIdBasedPageUrl(page);
    }
}

/**
 * Get page URL that is id based (i.e. in the format /pages/viewpage.action?pageId=<pageId>)
 * @param page the page to generate a url for
 * @return page URL that is id based
 */
public static String getIdBasedPageUrl(AbstractPage page) {
    if (page == null) {
        return "";
    }

    return "/pages/viewpage.action?pageId=" + page.getId();
}

/**
 * Is "title" something we can safely put in a /foo/bar/title URL? Or should we reference this thing by ID
 * just to be safe?
 * @param title The title to check
 * @return True of the title can be put in a URL, false otherwise
 */
public static boolean isSafeTitleForUrl(String title) {
    if (!isSafeTitleForNormalize(title)) {
        return false;
    }

    for (int i = 0; i < title.length(); ++i) {
        char c = title.charAt(i);

        if (!isAscii(c))
            return false;
    }

    return true;
}

private static boolean isSafeTitleForNormalize(String title) {
    if (StringUtils.isEmpty(title) || title.length() >= 150) {
        return false;
    }

    if (ENDS_WITH_PUNCTUATION.matcher(title).find()) {
        return false;
    }

    if (".".equals(title)) {
        return false;
    }

    for (int i = 0; i < title.length(); ++i) {
        char c = title.charAt(i);

        if (ILLEGAL_URL_TITLE_CHARS.contains(c)) {
            return false;
        }
    }

    return true;
}

/**
 * This method is only ever called when we know that the page title consists of
 * ASCII characters, so we only need to single-encode it, not double-encode.
 * @param page The page to get the URL of
 * @return The URL for the given page object
 */
private static String toDisplayUrl(AbstractPage page) {
	 return toDisplayUrl(page, false);
}

private static String toDisplayNormalizedUrl(AbstractPage page) {
	 return toDisplayUrl(page, true) + "?pageId=" + page.getId();
}

private static String toDisplayUrl(AbstractPage page, boolean normalize) {
    StringBuilder displayUrl = new StringBuilder("/display/");
    displayUrl.append(GeneralUtil.urlEncode(page.getSpace().getKey()));
    displayUrl.append("/");
    if ("blogpost".equals(page.getType())) {
    	//Why shouldn't a blog have any creation date? It happens in my case... Maybe a bug or so!
        if (page.getCreationDate() != null) {
            displayUrl.append(new SimpleDateFormat("yyyy/MM/dd").format(page.getCreationDate()));
            displayUrl.append("/");
        }
    }
    
    String title = normalize ? decompose(page.getTitle()) : page.getTitle();
    displayUrl.append(urlEncode(title));
    return displayUrl.toString();
}

private static String decompose(String input) {
	return Normalizer.normalize(input, Normalizer.Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
}

Tesztelés

TestGeneralUtil.java részlet

public void testNonAsciiPageUrl() {

		final String spaceKey = "HUN";
		final String pageTitle= "Ékezetes szöveg";

		Space space = new Space();
		space.setKey(spaceKey);

		Page page = new Page();
		page.setSpace(space);
		page.setTitle(pageTitle);

		assertEquals("/display/" + spaceKey + "/Ekezetes+szoveg?pageId=0", GeneralUtil.getPageUrl(page));

}

A végeredmény

confluence_pages_ékezet_m.png

A meglévő 2 megoldás mellé létrehoztunk egy harmadik típusú link generálást.  Az így kialakított kompromisszumos megoldás (beszédes hivatkozás pageId-val kiegészítve):
/display/COM/Uj+szoveg+ekezettel?pageId=1081346

Címkék: confluence

29.
május

JIRA forráskód és build

Maven build JIRA forrásból |  lacimol

JIRA forráskód

A 10$-os licence megvásárlásával legtöbb esetben az adott termék vagy plugin forráskódját is megkapjuk. A https://my.atlassian.com -ról a forrás letölthető és (windows alatt) a kicsomagolt mappa gyökerében lévő build.bat-tal lefordíthatjuk, amiből a végén telepíthető war fájlt kapunk. Ez az elmélet, de lássuk a gyakorlatot.

Build

A build során a Maven biztosítja, hogy ne kelljen tucatnyi jar fájlt csatolnunk a forráshoz, ezeket inkább majd ő letöltögeti az Atlassian Maven repository-ból a helyi repository-ba. Ennek helye alapértelmezetten a localrepo könyvtárra van beállítva a mellékelt batch fájlban, amit érdemes a saját (jó esetben a korábban már a JIRA pluginekhez használt) repo-nkra állítanunk (build.bat --> c:\Users\[USERNAME]\.m2\repository), így a kb 1 GB letöltés egy részét megúszhatjuk.

maven-atlassian-com.png

Hibák és megoldásaik

Ha nem olvastuk a build-elési leírást, akkor rögtön hibát kapunk (Unable to find resource 'jta:jta:pom:1.0.1' in repository atlassian-proxy (https://m2proxy.atlassian.com/repository/public)), mert néhány jar fájl nem letölthető a repo-ból, így azokat magunknak kell telepítenünk. A fenti linken jelenleg a 6-os JIRA-hoz szükséges jarokat sorolja, az 5-öshöz nekem ezekre volt szükségem (JIRA 5.2.8):

  • jta-1.0.1.jar
  • jndi-1.2.1.jar
  • activation-1.0.1.jar
  • jms-1.1.jar
  • jmxri és jmxtools-1.2.1.jar

 

XMLInputFactory exception:

Bizonyos JDK verzókból hiányozhat a StAX implementáció (6 előtti) vagy azok egyes metódusai (6u12: newFactory()), ezért érdemes a legfrissebb JDK-t használnunk (itt a 6-oson belül a jdk-6u45-windows-i586-ot).

ResourceDocletJSON (error has occurred in JavaDocs report generation):

Az AMPS által hívott start() metódusnak rosszul adja át az "-output" paramétert Windows alatt (az elválasztó jeleket rosszul kezeli), így a fenti osztály nem találja a fájlt: "java.io.FileNotFoundException: F:jiratlassian-jira-5.2.8-sourcejira-admin-helper-plugin argetclasses". Helyesen: "F:\jira\atlassian-jira-5.2.8-source\..." lenne. A legegyszerűbb javítási módszer, ha kikommentezzük a két érintett modult és utólag telepítjük őket (jira-admin-helper és jira-welcome).

A hibát már a JIRA-ban is javították, de csak az AMPS 3.9-től felfelé, így az 5.2.8-asban használt 3.8-as még hibás. A kikommentezés helyett átírhatjuk ResourceDocletJSON.java-t a csatolt atlassian-rest-doclet 2.7.1-es verzióban és ehhez hozzáigazítva a build.bat-ot és a pom.xml-eket, végigfut hiba nélkül a build. A bemeneti paramétereknél az output hibás elérési utat ad, de a classpath jót, és utóbbi első eleme maga a keresett út. A módosítás csak egy ideiglenes megoldás, de a buildhez használható.

ResourceDocletJSON.java

public class ResourceDocletJSON {

    ...
	private static final String OPTION_OUTPUT = "-output";
    private static final String OPTION_CLASSPATH = "-classpath";
	...

    public static boolean start( RootDoc root ) {
		// final String output = getOptionArg( root.options(), OPTION_OUTPUT );
		final String classpath = getOptionArg( root.options(), OPTION_CLASSPATH );
		final String output = classpath.split(";")[0] + "/resourcedoc.xml";
		...
	}
	...
}

jira-admin-helper-plugin: pom.xml

com.atlassian.plugins.restatlassian-rest-doclet2.7.1

jira-welcome-plugin (a belső jira-welcome-plugin mappában): pom.xml

com.atlassian.plugins.restatlassian-rest-common2.7.1providedcom.atlassian.plugins.restatlassian-rest-doclet2.7.1

build.bat

REM call mvn2.bat clean install -f "jira-welcome-plugin/pom.xml" -pl jira-welcome-plugin -Dmaven.test.skip -s %SETTINGSFILE% -Dmaven.repo.local=%cd%\%LOCALREPO% %*
call mvn2.bat clean install -f "jira-welcome-plugin/jira-welcome-plugin/pom.xml" -Dmaven.test.skip -s %SETTINGSFILE% -Dmaven.repo.local=%cd%\%LOCALREPO% %* 

Címkék: jira build

17.
május

Jira plugin fejlesztés 5.

Active Objects - JIRA ORM |  lacimol

Az adatok kezelése a JIRA-ban

Az 5-ös verzió előtt a JIRA az OpenSymphony-tól átvett PropertySet interfészt használta az adatok tárolására, ami az adatokat kulcs/érték párok kapcsolatával írja le. Ez a megoldás jól használható rendszerbeállításoknál, de bonyolultabb szerkezeteknél (pl: entitiy) nagyon körülményes vele dolgozni. Az új tárolási elv az ORM, mellyel már valódi adatkapcsolatokat is könnyen le lehet írni OO módon, valamint egy esetleges adatbázis implementáció csere is egyszerűvé válik.

Active Objects

Az Active Objects egy új ORM (Objektum-relációs leképzés) réteg az Atlassian termékekben. Segítségével az eddigi PropertySet-es félmegoldás helyett valódi objektum alapú adatbázis leképzés alapján xml paraméterezéssel és annotációk megadásával tárolhatjuk és kezelhetjük adatainkat (hasonlóan a Hibernate-hez). Jelenlegi verziója a 0.22.1-es, de a korábbi verziók (pl: 0.19.7) is mennek JIRA 5 alatt.

Példák

Kezdeti lépésként a JIRA hivatalos AO oldalát és Matthew B. Doar által írt könyv a Practical JIRA Plugins egyik példáját vegyítettem. Így kerül a példába OneToMany adatbázis kapcsolat (egy Todo több Comment) és Webwork action a szervlet helyett. A fenti könyv és Atlassian tutorial részletesen leírja a szükséges lépéseket, így itt csak a fontosabbakat emelem ki.

ao.png

Paraméterezés

A pom.xml-ben és az atlassian-plugin.xml-ben a lenti kiegészítések után használható az ActiveObjects. Eclipse-ből érdemes az "atlas-mvn eclipse:eclipse" paranccsal frissíteni a függőségeket.

pom.xml részlet

com.atlassian.activeobjectsactiveobjects-plugin0.19.7provided

atlassian-plugin.xml részlet

The module configuring the Active Objects service used by this pluginhu.lacimol.tutorial.todo.Todohu.lacimol.tutorial.todo.CommentAccess to the Active Objects service

Implementáció

A vezérlést a TodoAction osztály végzi, melynek 3 lehetséges kimenete lehet: Todo lista (üres hívásnál), Todo létrehozás (Create hívásnál) és egyszerű keresés (Select hívásnál). A Todo / Comment osztály egy egyszerű bean (POJO), az adott adatbázis tábla mezőivel. Az Action osztály nem közvetlenül, hanem egy Service-en keresztül éri el az AO-t, amit a JIRA a konstruktoron keresztül automatikusan létrehoz. A OneToMany kapcsolat leírásához elég a collection-t tartalmazó osztályban (Todo) egy @OneToMany annotációt használnunk, és a kollekció elemeit alkotóban (Comment) egy getter-setter párost a Todo-ra.

TodoServiceImpl.java részlet

private final ActiveObjects ao;
public TodoServiceImpl(ActiveObjects ao) {
	this.ao = checkNotNull(ao);
}

@Override
public Todo create(String description) {
	final Todo todo = ao.create(Todo.class);
	todo.setDescription(description);
	todo.setComplete(false);
	todo.save();
	return todo;
}

@Override
public List<Todo> findAll() {
	return newArrayList(ao.find(Todo.class));
}

@Override
public List<Todo> find(String description) {
	return newArrayList(ao.find(Todo.class, Query.select().where("description = ?", description)));
}

@Override
public void addComment(Todo todo, String text) {
	Comment comment = ao.create(Comment.class);
	comment.setTodo(todo); // a OneToMany kapcsolat beállítása
	comment.setText(text);
	comment.save();
}

Teszt

A teszteléshez nem szükséges speciális adatbázis konfiguráció, csak néhány annotációt kell ott is megadnunk.

TodoServiceImpl annotáció

@RunWith(ActiveObjectsJUnitRunner.class)
@Jdbc(DynamicJdbcConfiguration.class)
@Data(TodoServiceImplTest.TodoServiceImplTestDatabaseUpdater.class)
public class TodoServiceImplTest {
...
}

TodoServiceImplTest log

--- Create test data ---
DEBUG - net.java.ao.sql                - CREATE TABLE PUBLIC.AO_000000_TODO (
    COMPLETE BOOLEAN,
    DESCRIPTION VARCHAR(255),
    ID INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
    PRIMARY KEY(ID)
)
DEBUG - net.java.ao.sql                - CREATE TABLE PUBLIC.AO_000000_COMMENT (
    ID INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
    TEXT VARCHAR(255),
    TODO_ID INTEGER,
    CONSTRAINT fk_ao_000000_comment_todo_id FOREIGN KEY (TODO_ID) REFERENCES PUBLIC.AO_000000_TODO(ID),
    PRIMARY KEY(ID)
)
DEBUG - net.java.ao.sql                - CREATE INDEX PUBLIC.index_ao_000000_comment_todo_id ON PUBLIC.AO_000000_COMMENT(TODO_ID)
DEBUG - net.java.ao.sql                - INSERT INTO AO_000000_TODO (ID) VALUES (NULL)
DEBUG - net.java.ao.sql                - UPDATE PUBLIC.AO_000000_TODO SET DESCRIPTION = ? WHERE ID = ?
DEBUG - net.java.ao.sql                - INSERT INTO AO_000000_COMMENT (ID) VALUES (NULL)
DEBUG - net.java.ao.sql                - UPDATE PUBLIC.AO_000000_COMMENT SET TEXT = ?,TODO_ID = ? WHERE ID = ?
DEBUG - net.java.ao.sql                - INSERT INTO AO_000000_COMMENT (ID) VALUES (NULL)
DEBUG - net.java.ao.sql                - UPDATE PUBLIC.AO_000000_COMMENT SET TEXT = ?,TODO_ID = ? WHERE ID = ?
--- Do test ---
DEBUG - net.java.ao.sql                - SELECT * FROM PUBLIC.AO_000000_COMMENT
DEBUG - net.java.ao.sql                - SELECT * FROM PUBLIC.AO_000000_TODO
DEBUG - net.java.ao.sql                - SELECT * FROM PUBLIC.AO_000000_COMMENT WHERE TODO_ID = ?
--- Do test ---
DEBUG - net.java.ao.sql                - SELECT * FROM PUBLIC.AO_000000_TODO
DEBUG - net.java.ao.sql                - INSERT INTO AO_000000_TODO (ID) VALUES (NULL)
DEBUG - net.java.ao.sql                - UPDATE PUBLIC.AO_000000_TODO SET DESCRIPTION = ?,COMPLETE = ? WHERE ID = ?
DEBUG - net.java.ao.sql                - SELECT * FROM PUBLIC.AO_000000_TODO
--- Do test ---
DEBUG - net.java.ao.sql                - SELECT * FROM PUBLIC.AO_000000_TODO
DEBUG - net.java.ao.sql                - INSERT INTO AO_000000_TODO (ID) VALUES (NULL)
DEBUG - net.java.ao.sql                - UPDATE PUBLIC.AO_000000_TODO SET DESCRIPTION = ? WHERE ID = ?
DEBUG - net.java.ao.sql                - INSERT INTO AO_000000_COMMENT (ID) VALUES (NULL)
DEBUG - net.java.ao.sql                - UPDATE PUBLIC.AO_000000_COMMENT SET TEXT = ?,TODO_ID = ? WHERE ID = ?
DEBUG - net.java.ao.sql                - INSERT INTO AO_000000_COMMENT (ID) VALUES (NULL)
DEBUG - net.java.ao.sql                - UPDATE PUBLIC.AO_000000_COMMENT SET TEXT = ?,TODO_ID = ? WHERE ID = ?
DEBUG - net.java.ao.sql                - SELECT * FROM PUBLIC.AO_000000_TODO
DEBUG - net.java.ao.sql                - SELECT * FROM PUBLIC.AO_000000_COMMENT WHERE TODO_ID = ?

Az ActiveObjects interfészen keresztül létrehozhatunk (create), lekérdezhetjük (get és find), megszámolhatjuk (count) és törölhetjük (delete) az adott rekordokat. A fenti módosítások letölthetők SVN-en keresztül és becsomagolva a blog GoogleCode oldaláról a JiraDevTutor projektből.

Címkék: jira plugin programozás orm

30.
április

Jira plugin fejlesztés 4.

Velocity és a sablonok |  lacimol

Velocity engine

A JIRA pluginok funkcióinak megjelenítését a Velocity template engine végzi, amely a velocity context-be bepakolt változókhoz nyújt hozzáférést a megjelenítési (view) részen. A működése a JSP/JSTL-hez hasonló, HTML kódba ágyazott velocity kód kerül a vm fájlokba, amiből tetszőleges kimenet nyerhető (itt *.jspa kiterjesztésű HTML). A szintaktika egyszerű: a változók előtt $ karaktert, a tulajdonságok minősítésére pontot, a vezérlési szerkezetek kelölésére # karaktert kell használnunk (pl: #if($authcontext.loggedInUser)...). A getter-ek a BSH-hoz és a Groovy-hoz hasonlóan elérhetők kompakt hivatkozással is, így a getLoggedInUser() helyett elég a loggedInUser is.

Veloeclipse

Az Eclipse a vm fájloknál nem emeli ki a kódot, így nehezen választható szét a html és a velocity szintaktika. Hozzárendelhetjük a vm fájlokhoz a beépített html editort, de akkor a velocity minden eleme egyszínű (fekete) lesz. Egy fokkal jobb megoldás a veloeclipse plugin telepítése az Eclipse-hez. Sajnos kódkiegészítést nem tud, de kiemeli a velocity vezérlést és változókat.

veloeclipse.png

Velocity context

A JIRA nagy segítséget ad a context-be előre bepakolt utils osztályokkal, de sajnos 3 különböző dokumentáció 3 eltérő halmazt ad meg. A régebbi verzióknál elérhető volt a ctx változó a vm fájlokból, de az 5-ös verziótól felfelé már trükközni kell, hogy a teljes és friss listát megnézhessük (lásd lentebb).

HelloAction.java

package hu.lacimol.tutorial.webwork;

import java.util.Map;
import java.util.TreeMap;

import com.atlassian.crowd.embedded.api.User;
import com.atlassian.jira.component.ComponentAccessor;
import com.atlassian.jira.user.UserUtils;
import com.atlassian.jira.web.action.JiraWebActionSupport;

public class HelloAction extends JiraWebActionSupport {

	private static final long serialVersionUID = 6545090412954507661L;

	private String userName;

	@Override
	protected String doExecute() throws Exception {
		log.debug("Entering doExecute");
		String loggedUserName = getLoggedUserName();
		User admin = UserUtils.getUser("admin");
		userName = loggedUserName != null ? loggedUserName : admin.getDisplayName();
		return SUCCESS;
	}

	public String getUserName() {
		return userName;
	}

	public String getLoggedUserName() {
		User loggedInUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser();
		return loggedInUser == null ? null : loggedInUser.getDisplayName();
	}

	public Map<String, Object> getCtx() {
		// rendezett velocity paraméterek
		return new TreeMap<String, Object>(ComponentAccessor.getVelocityParamFactory().getDefaultVelocityParams());
	}



}

succes.vm

<html>
	<head>
		<title>HelloAction Success</title>
	</head>
	<body>
		<div style="background: #FFFFFF">
			<h3>Velocity context examples</h3>
    		<table>
    			<tr>
					<td>action.userName:</td>
					<td>$action.userName</td></tr>
				<tr>
					<td>userutils.getUser("admin"):</td>
					<td>$userutils.getUser("admin")</td></tr>
				<tr>
					<td>authcontext.loggedInUser.displayName:</td>
					## Feltétel
					<td>#if($authcontext.loggedInUser) $authcontext.loggedInUser.displayName #else Unknown #end</td></tr>
				<tr>
					<td>i18n.getText("lacimol.description"):</td>
					<td>$i18n.getText("lacimol.description")</td></tr>
			</table>
    		<h3>Velocity context parameters ($ctx.size())</h3>
    		<table>
    			<tr>
    				<td>Parameter</td>
    				<td>Class</td>
    			</tr>
				## ciklus
    			#foreach($param in $ctx.entrySet())
    			<tr>
    				<td>$param.key</td>
    				<td>$param.value.class</td>
    			</tr>
    			#end
    		</table>
		</div>
	</body>
</html>

Az eredmény

A lenti képen látható, hogy a context-ben lévő változók hogyan használhatók. Például az action.userName a HelloAction getUserName() metódusát hívja és jeleníti meg a kapott Stringet. A userutils-szal elkérhetjük egy adott felhasználó adatait, az authcontext-ből kivehetjük az aktuálisan belépett usert, és az i18n-nel pedig az aktuális nyelvnek megfelelő szöveget jeleníthetjük meg.

velocity_context.png

A fenti módosítások letölthetők SVN-en keresztül és becsomagolva a blog GoogleCode oldaláról a JiraDevTutor projektből.

22.
április

Jira plugin fejlesztés 3.

Webwork és az MVC |  lacimol

Saját oldal a JIRA-n belül

A HelloWorld példában láthattuk hogyan illeszthető be egy külső link a menübe. A lenti példában pedig megnézzük, hogy hogyan hivatkozhatunk egy saját (belső) oldalra. A Webwork plugin modullal létrehozhatunk a Struts-hoz hasonló Action alapú, MVC-re épülő architektúrát (Modell-Nézet-Vezérlő). A Webwork időközben egyesült a Struts-al a Struts 2-ben, de itt még a Webwork 1-et használjuk. Ennek lényege, hogy xml-ben konfiguráljuk a folyamatot, amit egy Action-ben megvalósítunk, majd egy Velocity által feldolgozott *.vm lapra továbbítjuk a kérést, és megjelenítjük az eredményt. Jelen esetben a vm sablonból egy jspa lap generálódik (WebWork Framework JSP Action).

velocity_struts_diag.png

Konkrét példa

A lenti xml-ben beállítottam, hogy a Client Sites menüpont linkjére kattintva a "/secure/Hello.jspa" kérésre a HelloAction kerüljön feldolgozásra, aminek a sikeres futását követően a success.vm alapján a Hello.jspa fog megjelenni. Az Action beállítja a felhasználónevet, amit a vm lapon kinyerünk az action-ből. A példában az MVC-ből csak a View és a Controller valósul meg igazán, mert az egyszerűség miatt itt nincs egy külön osztály a User-nek, ami a Model lenne.

atlassian-plugin.xml

<!-- webwork1 plugin -->
<webwork1 key="newactions" name="New actions" class="java.lang.Object">
	<description>These actions do not exist in JIRA until they were defined here.</description>
	<actions>
		<action name="hu.lacimol.tutorial.webwork.HelloAction" alias="Hello">
			<view name="success">/templates/hu/lacimol/tutorial/webwork/success.vm</view>
		</action>
	</actions>
</webwork1>
<web-item name="Client Sites" i18n-name-key="client-sites.name" key="client-sites" section="system.top.navigation.bar" weight="1000">
	<description key="client-sites.description">The Client Sites Plugin</description>
	<label key="client-sites.label" />
	<link linkId="client-sites-link">/secure/Hello.jspa</link>
</web-item>

HelloAction.java

package hu.lacimol.tutorial.webwork;

import com.atlassian.jira.web.action.JiraWebActionSupport;

public class HelloAction extends JiraWebActionSupport {

	private static final long serialVersionUID = 6545090412954507661L;

	private String userName;

	@Override
	protected String doExecute() throws Exception {
		log.debug("Entering doExecute");
		userName = "lacimol";
		return SUCCESS;
	}

	public String getUserName() {
		return userName;
	}

}

success.wm

<html>
	<head>
		<title>HelloAction Success</title>
	</head>
	<body>
		<div style="background: #FFFFFF">
			<span>Siker. Hello $action.userName!</span>
		</div>
	</body>
</html>

A fenti módosítások letölthetők SVN-en keresztül és becsomagolva a blog GoogleCode oldaláról a JiraDevTutor projektből. További Webwork példák: https://bitbucket.org/mdoar/webwork-sample

Címkék: programozás velocity webwork

18.
április

Jira plugin fejlesztés 2.

A Maven és a pom.xml |  lacimol

A pom.xml felépítése

Ha az Atlassian Plugin SDK-val generálunk egy projektet, akkor a parancssorban megadott adatok alapján feltölti a pom.xml-t is adatokkal. A pom.xml felépítését az Atlassian dokumentáció leírja, de a főbb elemeket itt is kiemelem.

<?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>hu.lacimol.tutorial</groupid>
	<artifactid>jiradevtutor</artifactid>
	<version>1.0-SNAPSHOT</version>
	<organization>
		<name>Molnár László</name>
		<url>http://jiradev.blog.hu</url>
	</organization>
	<name>JiraDevTutor</name>
	<description>This is the hu.lacimol.tutorial:helloworld plugin for Atlassian JIRA.</description>
	<packaging>atlassian-plugin</packaging>
	<!-- további elemek, pl: dependencies, build -->
	<properties>
    	<jira.version>5.1.8</jira.version>
    	<amps.version>4.1.6</amps.version>
    	<plugin.testrunner.version>1.1.1</plugin.testrunner.version>
    </properties>
</project>

A fent kiemelt részek így fognak megjelenni az admin oldalon a Plugin-ek alatt:

plugin_installed_mod.png

Maven beállítások

A telepített SDK tartalmazza a Maven-t is (2.1.0), de ha más verziót használnánk, akkor a lenti konfigurációs beállításokat át kell vezetnünk. A Maven a függőségek kezelését a [MAVEN_HOME]/conf/settings.xml-ben lévő adatok alapján végzi, az ott megadott szerverekről tölti le a helyi reporsitory-ba.

settings.xml

<plugingroups>
     <plugingroup>com.atlassian.maven.plugins</plugingroup>
</plugingroups>
<!-- további elemek -->
<!-- Default profile containing Atlassian servers -->
<profile>
	<id>defaultProfile</id>
	<activation>
		<activebydefault>true</activebydefault>
	</activation>

	<repositories>
		<repository>
			<id>atlassian-public</id>
			<url>https://m2proxy.atlassian.com/repository/public</url>
			<snapshots>
				<enabled>true</enabled>
				<updatepolicy>never</updatepolicy>
				<checksumpolicy>warn</checksumpolicy>
			</snapshots>
			<releases>
				<enabled>true</enabled>
				<checksumpolicy>warn</checksumpolicy>
			</releases>
		</repository>
		<repository>
			<id>atlassian-plugin-sdk</id>
			<url>file://${env.ATLAS_HOME}/repository</url>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
			<releases>
				<enabled>true</enabled>
				<checksumpolicy>warn</checksumpolicy>
			</releases>
		</repository>
	</repositories>

	<pluginrepositories>
		<pluginrepository>
			<id>atlassian-public</id>
			<url>https://m2proxy.atlassian.com/repository/public</url>
			<releases>
				<enabled>true</enabled>
				<checksumpolicy>warn</checksumpolicy>
			</releases>
			<snapshots>
				<updatepolicy>never</updatepolicy>
				<checksumpolicy>warn</checksumpolicy>
			</snapshots>
		</pluginrepository>
		<pluginrepository>
			<id>atlassian-plugin-sdk</id>
			<url>file://${env.ATLAS_HOME}/repository</url>
			<releases>
				<enabled>true</enabled>
				<checksumpolicy>warn</checksumpolicy>
			</releases>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</pluginrepository>
	</pluginrepositories>
	<properties>
		<downloadsources>true</downloadsources>
		<downloadjavadocs>true</downloadjavadocs>
	</properties>
</profile>  

Maven projekt az Eclipse-ben

Az Atlassian HelloWorld projekt a leírás alapján Java projektként kerül be az Eclipse-be, de néhány apró módosítással Maven projekté alakíthatjuk. Az átalakítás után egyszerűbb lesz a frissítés, a kommitálás és a szerverindítás. A HelloWorld projektet átalakíthatjuk a Jobb klikk --- Configure --- "Convert to Maven project" kiválasztásával vagy eleve Maven projektként importáljuk. Az így módosított projekt hibát fog jelezni (Plugin execution not covered by lifecycle configuration), amit a lenti pom.xml módosítással orvosolhatunk.

<?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>hu.lacimol.tutorial</groupid>
	<artifactid>jiradevtutor</artifactid>
	<version>1.0-SNAPSHOT</version>
	<organization>
		<name>Molnár László</name>
		<url>http://jiradev.blog.hu</url>
	</organization>
	<name>JiraDevTutor</name>
	<description>This is the hu.lacimol.tutorial:helloworld plugin for Atlassian JIRA.</description>
	<packaging>atlassian-plugin</packaging>
	<!-- További elemek: dependencies -->
	<build>
		<!-- További elemek: plugins -->
		<pluginmanagement>
			<plugins>
				<!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself. -->
				<plugin>
					<groupid>org.eclipse.m2e</groupid>
					<artifactid>lifecycle-mapping</artifactid>
					<version>1.0.0</version>
					<configuration>
						<lifecyclemappingmetadata>
							<pluginexecutions>
								<pluginexecution>
									<pluginexecutionfilter>
										<groupid>com.atlassian.maven.plugins</groupid>
										<artifactid>maven-jira-plugin</artifactid>
										<versionrange>[4.1.6,)</versionrange>
										<goals>
											<goal>filter-plugin-descriptor</goal>
											<goal>filter-test-plugin-descriptor</goal>
											<goal>copy-bundled-dependencies</goal>
											<goal>copy-test-bundled-dependencies</goal>
											<goal>compress-resources</goal>
											<goal>generate-manifest</goal>
											<goal>generate-test-manifest</goal>
											<goal>generate-rest-docs</goal>
										</goals>
									</pluginexecutionfilter>
									<action>
										<ignore></ignore>
									</action>
								</pluginexecution>
							</pluginexecutions>
						</lifecyclemappingmetadata>
					</configuration>
				</plugin>
			</plugins>
		</pluginmanagement>
	</build>
	<!-- További elemek: properties -->
</project>

A módosítás után már nem jelez hibát a fordító. Ezután hozzunk létre a "Run Configurations" menü alatt egy indító parancsot. Itt a Maven fül alatt állítsuk be a "com.atlassian.maven.plugins:maven-amps-dispatcher-plugin:4.1.6:run" parancsot a Goals résznél, majd növeljük a JRE fül alatt a Maven fordításhoz szükséges memóriát: -Xmx768M -XX:MaxPermSize=256M. Ezután a lenti konfigurációval kiválthatjuk a parancssorból futtatható "atlas-run"-t.

eclipse_run_conf.png

A fenti módosítások letölthetők SVN-en keresztül és becsomagolva a blog GoogleCode oldaláról a JiraDevTutor projektből. A fenti kód fordításához telepíteni kell az SDK-t és be kell állítani az abban telepített Maven-t az Eclipse-ben (Run Configurations --- Maven Build --- Maven runtime).

Címkék: plugin programozás maven

15.
április

Jira plugin fejlesztés 1.

kezdeti lépések |  lacimol

HelloWorld példa

Az Atlassian fejlesztői honlapján részletes leírást találhatunk arról, hogy hogyan kezdjünk neki a plugin fejlesztésnek. Ha már van telepített Atlassian Plugin SDK-nk, akkor ugorhatunk a HelloWorld plugin példára, de ha most kezdjük, akkor érdemes végigmenni a részletes leíráson.

Kezdeti lépések:

  1. Telepítés és beállítások
  2. Tesztek
  3. Konfigurációs fájlok

A fejlesztéshez telepítenünk kell a JDK-t, az SDK-t és ajánlatos az Eclipse IDE-t is. A fenti telepítési leírás Linuxra készült, de mivel én Windows alatt fogok fejleszteni, így lesznek apró eltérések. Az SDK telepítése közben végig szükség lesz internetkapcsolatra, mert az alap telepítő csomag mellett még kb 120 MB-ot le fog tölteni. A teljes projekt 3.8 (SDK + projekt) + 1.2 GB (maven repository) helyet igényel és legalább 2 GB RAM-ot. A fenti 3 lépés egyenként 1-2 órát vesz igénybe (internetkapcsolattól és géptől függően), de a következő lista segíthet az esetlegesen felmerülő hibákon túljutni és a lépésekhez szükséges időt csökkenteni:

  • a Maven-t az Eclipsehez a megadott helyett itt érdemes keresni: http://download.jboss.org/jbosstools/updates/m2eclipse-wtp
  • a JIRA build és Tomcat indítás eleinte 4 percet, később 2 percet vesz igénybe, ezt kihagyjatjuk a FastDev-vel (a DevelpoerTool segítségével JIRA-ból újrafordíthatjuk a változásokat, ami csak 20-40 mp-et igényel)
  • ha a FastDev nem fordítja be automatikusan a változásokat (több percig dolgozik), akkor érdemes kikapcsolni a CLI-t (<useFastdevCli>false</useFastdevCli> - https://answers.atlassian.com/questions/108577/fastdev-in-atlassian-sdk-4-1)
  • a tesztek futtatásánál (pl: atlas-run -DskipTests=true) a paraméterben átadott "=" jelet a Windows 7 elválasztóként kezeli, így ehhez az érintett 2 db *.bat fájlban ezt kezelni kell (atlas-run és atlas-integration-test).
  • ha a megadott 2990-es porton nem nyitható meg a JIRA vagy más portot látunk a logban, akkor gyanakodjunk, hogy egy másik ablakban fut egy másik példány (érdemes a telepítési leírásban a cmd-nek megfelelően egy atlas-run indítási parancsot is létrehoznunk az Eclipse-ben)

Az eredmény

A HelloWorld projekt létrehozása (atlas-create-jira-plugin) és módosítása (atlas-create-jira-plugin-module) után a lenti eredményt kapjuk (Web Section és Web Item):

insatalled_helloworld_plugin.png

További Plugin Module-ok: Component Import, Component, Component Tab Panel, Custom Field, Custom Field Searcher, Downloadable Plugin Resource, Gadget Plugin Module, Issue Tab Panel, Keyboard Shortcut, JQL Function, Licensing API Support, Module Type, Project Tab Panel, REST Plugin Module, RPC Endpoint Plugin, Report, Search Request View, Servlet Context Listener, Servlet Context Parameter, Servlet Filter, Servlet, Template Context Item, User Format, Version Tab Panel, Web Item, Web Panel, Web Panel Renderer, Web Resource, Web Resource Transformer, Web Section, Webwork Plugin, Workflow Condition, Workflow Post Function, Workflow Validator

A projekt főbb fájljai

A fenti demo projekt a lenti fájlokból épül fel (+ a teszt fájlok és lib-ek). A kód egy egyszerű interfészből, egy getName()-et implementáló osztályból, egy nyelvi részeket tartalmazó properties-ből és a menüpontot megvalósító xml-ből áll (A projekt egyik meghatározó fájlja a pom.xml, de azt később - a Maven-nel együtt - nézzem meg).

atlassian-plugin.xml

<?xml version="1.0" encoding="UTF-8"?>

${project.description}${project.version}images/pluginIcon.pngimages/pluginLogo.png
  com.atlassian.auiplugin:ajshelloworldcom.atlassian.tutorial.helloworld.MyPluginComponentThe mySection PluginThe Client Sites Plugin
    deleteMe
  Molnár László: Java developer / Scrum Master
    http://www.lacimol.hu
  

MyPluginComponent.java

package com.atlassian.tutorial.helloworld;

public interface MyPluginComponent {
    String getName();
}

MyPluginComponentImpl.java

package com.atlassian.tutorial.helloworld;

import com.atlassian.sal.api.ApplicationProperties;

public class MyPluginComponentImpl implements MyPluginComponent {
    
	private final ApplicationProperties applicationProperties;

    public MyPluginComponentImpl(ApplicationProperties applicationProperties) {
        this.applicationProperties = applicationProperties;
    }

    public String getName() {
        
		if(null != applicationProperties) {
            return "myComponent:" + applicationProperties.getDisplayName();
        }
        
        return "myComponent";
    }
}

helloworld.properties

#put any key/value pairs here
my.plugin.name=MyPlugin
my-section.label=mySection
my-section.name=mySection
my-section.description=The mySection Plugin

client-sites.label=Client Sites
client-sites.name=Client Sites
client-sites.description=The Client Sites Plugin

lacimol.label=Molnár László
lacimol.name=lacimol.hu
lacimol.description=Molnár László: Java developer / Scrum Master

Konfigurációs fájlok

A Atlassion fejlesztői leírás végén a konfigurációs fájlok paraméterezhetőségéről, a pom.xml beállításairól, a kódolási konvenciókról, a további plugin modulokról és fejlesztői eszközökről olvashatunk. Ezek inkább lexikális leírásként és dokumentációként olvasandók.

A fentiek alapján látható, hogy viszonylag egyszerűen (főként paraméterezéssel) létrehozható egy egyszerű plugin a JIRA-hoz. A következő bejegyzésben a Maven és a pom.xml működését nézem meg.

Címkék: jira plugin programozás atlassian