カスタムレポートで使用するデータは、プロセスレポートで使用可能なデータである必要があります。 データを確実に使用可能にするには、cron ジョブをスケジュールするか、プロセスレポート UI の「同期」オプションを使用します。
カスタムレポートを作成するには、QueryBuilder の REST インターフェイスを使用するか、または QueryBuilder API で OSGi サービスを作成します。
カスタムレポートを作成するための一般的な手順
カスタムレポートを追加する前に、次のテンプレート手順を実行してください。
-
-
必要なクエリーをカプセル化した URL リクエストは、該当するクエリー結果オブジェクトを返す必要があります。 クエリーを作成するには、QueryBuilder の REST インターフェイスを使用し、QueryBuilder API で OSGi サービスを作成します。動的または静的なクエリーを作成できます。
-
結果を表示するカスタムユーザーインターフェイスを作成します。 スタンドアロンのユーザーインターフェイスを作成するか、または結果を既存のプロセスレポート UI と統合します。
QueryBuilder の REST インターフェイスの使用
CRX QueryBuilder REST インターフェイスは Java API および REST API 経由で Asset Share Query Builder の機能を公開します。 CRX QueryBuilder REST インターフェイスの使用方法を確認してから、次の手順を実行してください。
-
http://[server]:[port]/lc/bin/querybuilder.json にアクセスします。
-
プロセスレポートのストレージノードの構造およびプロパティに基づいてクエリーを作成します。
オプションのパラメーターでオフセット、制限、アクセス数、プロパティを指定できます。 静的レポートの場合は引数をハードコードし、動的レポートの場合はパラメーターを UI から取得します。
すべてのプロセス名を取得するためのクエリーは次のように記述します。
http://[Server]:[Port]/lc/bin/querybuilder.json?exact=false&p.hits=selective&p.properties=pmProcessTitle&path=%2fcontent%2freporting%2fpm&property=pmNodeType&property.operation=equals&property.value=ProcessType&type=sling%3aFolder
注意:どのクエリーでも、path パラメーターで crx 保存場所を指定し、URL 標準に従って文字をエスケープします。
Query Builder API を使用したサービスの作成
QueryBuilder API を使用してサービスを作成するための前提条件は、CQ OSGI バンドルを作成してデプロイすること、および Query Builder API を使用することです。
-
適切なアノテーションを使用して OSGi サービスを作成します。 QueryBuilder にアクセスするには、次のように記述します。
@Reference(referenceInterface = QueryBuilder.class)
private QueryBuilder queryBuilder; -
predicate グループを作成します。 predicate グループを作成するコードは次のとおりです。
PredicateGroup predicateGroup = new PredicateGroup();
predicateGroup.setAllRequired(true); -
新しく作成した predicateGroup に predicate を追加します。 役に立つ predicate 構造としては、JcrBoolPropertyPredicateEvaluator、JcrPropertyPredicateEvaluator、RangePropertyPredicateEvaluator、DateRangePredicateEvaluator、TypePredicateEvaluator などがあります。
静的レポートの場合は predicate をハードコードし、動的レポートの場合はリクエストから predicate を取得します。
プロセスのすべてのインスタンスを取得するサンプルコードは次のとおりです。
Predicate predicate; //Add the path Constraint predicate = new Predicate(PathPredicateEvaluator.PATH); predicate.set(PathPredicateEvaluator.PATH, "/content/reporting/pm"); // should point to the crx path being used to store data predicate.set(PathPredicateEvaluator.EXACT, "false"); predicateGroup.add(predicate); //type nt:unstructured predicate = new Predicate(TypePredicateEvaluator.TYPE); predicate.set(TypePredicateEvaluator.TYPE, "nt:unstructured"); predicateGroup.add(predicate); //NodeType: Process Instance predicate = new Predicate(JcrPropertyPredicateEvaluator.PROPERTY); predicate.set(JcrPropertyPredicateEvaluator.PROPERTY, "pmNodeType"); predicate.set(JcrPropertyPredicateEvaluator.OPERATION, JcrPropertyPredicateEvaluator.OP_EQUALS); predicate.set(JcrPropertyPredicateEvaluator.VALUE, "ProcessInstance"); predicateGroup.add(predicate); //processName predicate = new Predicate(JcrPropertyPredicateEvaluator.PROPERTY); predicate.set(JcrPropertyPredicateEvaluator.PROPERTY, "pmProcessName"); predicate.set(JcrPropertyPredicateEvaluator.OPERATION, JcrPropertyPredicateEvaluator.OP_EQUALS); predicate.set(JcrPropertyPredicateEvaluator.VALUE, processName); //processName variable stores the name of the process whose instances need to be searched predicateGroup.add(predicate);
-
predicateGroup を使用してクエリーを定義します。
Query query = queryBuilder.createQuery(predicateGroup, session);
-
クエリーの結果を取得します。
query.setStart(offset); // hardcode or fetch from request if(hits == -1) // hardcode or fetch from request hits = 0; query.setHitsPerPage(hits); SearchResult searchResult = query.getResult();
-
結果を反復し、必要な形式に変換します。 結果を CSV 形式で送信するためのコードは次のとおりです。
Iterator<Node> iter = searchResult.getNodes(); while(iter.hasNext()) { Node node = iter.next(); row = new StringBuilder(); for (String property : includeProperties) { // the properties of the node which needs to be returned, or one can return all the properties too. try { row.append(node.getProperties(property).nextProperty().getString() + COMMA_SEPARATOR); } catch (NoSuchElementException e) { //Adding separator for no value row.append(COMMA_SEPARATOR); } catch (RepositoryException e) { e.printStackTrace(); } } row.deleteCharAt(row.lastIndexOf(COMMA_SEPARATOR)); row.append(NEW_LINE); out.write(row.toString().getBytes());
-
org.apache.felix maven-bundle-plugin を使用して、サーブレットの OSGi バンドルを作成します。
-
CRX サーバーでバンドルをデプロイします。
サービスの例
次のサンプルコードは、月末、四半期末、年末の時点での状態が RUNNING および COMPLETE であるプロセスのインスタンスをカウントするサービスです。
package custom.reporting.service; import java.text.DateFormatSymbols; import java.util.Calendar; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.SortedSet; import java.util.TreeSet; import javax.jcr.Node; import javax.jcr.Session; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.Service; import com.day.cq.search.Predicate; import com.day.cq.search.PredicateGroup; import com.day.cq.search.Query; import com.day.cq.search.QueryBuilder; import com.day.cq.search.eval.JcrPropertyPredicateEvaluator; import com.day.cq.search.eval.PathPredicateEvaluator; import com.day.cq.search.eval.TypePredicateEvaluator; import com.day.cq.search.result.SearchResult; @Component(metatype = true, immediate = true, label = "PeriodicProcessVolume", description = "Service for supporting cutom reports pluggable to Process Reporting.") @Service(value = PeriodicProcessVolume.class) public class PeriodicProcessVolume { private static String[] monthNameList = new DateFormatSymbols().getMonths(); private static String[] quaterNameList = { "I", "II", "III", "IV" }; private final Map<Integer, Map<Integer, Long[]>> monthly = new HashMap<Integer, Map<Integer, Long[]>>(); private final Map<Integer, Map<Integer, Long[]>> quaterly = new HashMap<Integer, Map<Integer, Long[]>>(); private final Map<Integer, Long[]> yearly = new HashMap<Integer, Long[]>(); @Reference(referenceInterface = QueryBuilder.class) private QueryBuilder queryBuilder; private void addConstraints(PredicateGroup predicateGroup, String processName) { Predicate predicate; //Add the path Constraint predicate = new Predicate(PathPredicateEvaluator.PATH); predicate.set(PathPredicateEvaluator.PATH, "/content/reporting/pm"); predicate.set(PathPredicateEvaluator.EXACT, "false"); predicateGroup.add(predicate); //type nt:unstructured predicate = new Predicate(TypePredicateEvaluator.TYPE); predicate.set(TypePredicateEvaluator.TYPE, "nt:unstructured"); predicateGroup.add(predicate); //NodeType: Process Instance predicate = new Predicate(JcrPropertyPredicateEvaluator.PROPERTY); predicate.set(JcrPropertyPredicateEvaluator.PROPERTY, "pmNodeType"); predicate.set(JcrPropertyPredicateEvaluator.OPERATION, JcrPropertyPredicateEvaluator.OP_EQUALS); predicate.set(JcrPropertyPredicateEvaluator.VALUE, "ProcessInstance"); predicateGroup.add(predicate); //processName if (processName != null) { predicate = new Predicate(JcrPropertyPredicateEvaluator.PROPERTY); predicate.set(JcrPropertyPredicateEvaluator.PROPERTY, "pmProcessName"); predicate.set(JcrPropertyPredicateEvaluator.OPERATION, JcrPropertyPredicateEvaluator.OP_EQUALS); predicate.set(JcrPropertyPredicateEvaluator.VALUE, processName); predicateGroup.add(predicate); } } private Long[] setFrequency(Long[] frequency, int index) { if (frequency == null) { frequency = new Long[2]; frequency[0] = 0L; frequency[1] = 0L; } frequency[index] = frequency[index] + 1L; return frequency; } public void populateValues(Session session, String processName) { PredicateGroup predicateGroup = new PredicateGroup(); predicateGroup.setAllRequired(true); try { addConstraints(predicateGroup, processName); long batchSize = 10000L; long start = 0l; while (true) { Query query = queryBuilder.createQuery(predicateGroup, session); query.setStart(start); query.setHitsPerPage(batchSize); SearchResult searchResult = query.getResult(); Iterator<Node> itr = searchResult.getNodes(); long length = 0; while (itr.hasNext()) { length++; Node n = itr.next(); Calendar calender = n.getProperty("pmCreateTime").getDate(); String status = n.getProperty("pmStatus").getString(); int index = 0; if ("COMPLETE".equals(status)) { index = 1; } else if ("RUNNING".equals(status)) { index = 0; } else { continue; } int month = calender.get(Calendar.MONTH); int year = calender.get(Calendar.YEAR); int quater; if (month < 3) { quater = 1; } else if (month < 6) { quater = 2; } else if (month < 9) { quater = 3; } else { quater = 4; } Long frequency[]; Map<Integer, Long[]> yearMonthMap = this.monthly.get(year); if (yearMonthMap == null) { yearMonthMap = new HashMap<Integer, Long[]>(); } frequency = yearMonthMap.get(month); frequency = setFrequency(frequency, index); yearMonthMap.put(month, frequency); this.monthly.put(year, yearMonthMap); Map<Integer, Long[]> yearQuaterMap = this.quaterly.get(year); if (yearQuaterMap == null) { yearQuaterMap = new HashMap<Integer, Long[]>(); } frequency = yearQuaterMap.get(quater); frequency = setFrequency(frequency, index); yearQuaterMap.put(quater, frequency); this.quaterly.put(year, yearQuaterMap); frequency = this.yearly.get(year); frequency = setFrequency(frequency, index); this.yearly.put(year, frequency); } if (length < batchSize) { break; } else { start = start + batchSize; } } } catch (Exception e) { e.printStackTrace(); } } public Map<String, Long[]> getMonthly() { Map<String, Long[]> result = new LinkedHashMap<String, Long[]>(); SortedSet<Integer> years = new TreeSet<Integer>(monthly.keySet()); for (Integer year : years) { Map<Integer, Long[]> yearMonthMap = monthly.get(year); SortedSet<Integer> months = new TreeSet<Integer>(yearMonthMap.keySet()); for (Integer month : months) { String str = monthNameList[month] + " " + year; result.put(str, yearMonthMap.get(month)); } } return result; } public Map<String, Long[]> getQuaterly() { Map<String, Long[]> result = new LinkedHashMap<String, Long[]>(); SortedSet<Integer> years = new TreeSet<Integer>(quaterly.keySet()); for (Integer year : years) { Map<Integer, Long[]> quaterMonthMap = quaterly.get(year); SortedSet<Integer> quaters = new TreeSet<Integer>(quaterMonthMap.keySet()); for (Integer quater : quaters) { String str = quaterNameList[quater - 1] + " " + year; result.put(str, quaterMonthMap.get(quater)); } } return result; } public Map<Integer, Long[]> getYearly() { return yearly; } }
このサービス上にビルドする pom.xml ファイルのサンプルを次に示します。
<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> <!-- ====================================================================== --> <!-- P R O J E C T D E S C R I P T I O N --> <!-- ====================================================================== --> <groupId>com.custom</groupId> <artifactId>sample-report-core</artifactId> <packaging>bundle</packaging> <name>PR Sample Report</name> <description>Bundle providing support for a custom report pluggable to process reporting.</description> <version>1</version> <!-- ====================================================================== --> <!-- B U I L D D E F I N I T I O N --> <!-- ====================================================================== --> <build> <plugins> <plugin> <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <version>2.3.7</version> <extensions>true</extensions> <configuration> <instructions> <Bundle-Category>sample-report</Bundle-Category> <Export-Package> custom.reporting.service.*; </Export-Package> </instructions> </configuration> </plugin> <plugin> <groupId>org.apache.felix</groupId> <artifactId>maven-scr-plugin</artifactId> <version>1.11.0</version> <executions> <execution> <id>generate-scr-scrdescriptor</id> <goals> <goal>scr</goal> </goals> <configuration> <!-- Private service properties for all services. --> <properties> <service.vendor>Sample Report</service.vendor> </properties> </configuration> </execution> </executions> </plugin> </plugins> </build> <!-- ====================================================================== --> <!-- D E P E N D E N C I E S --> <!-- ====================================================================== --> <dependencies> <dependency> <groupId>com.day.cq</groupId> <artifactId>cq-search</artifactId> <version>5.6.4</version> </dependency> <dependency> <groupId>javax.jcr</groupId> <artifactId>jcr</artifactId> <version>2.0</version> </dependency> <dependency> <groupId>org.apache.felix</groupId> <artifactId>org.apache.felix.scr.annotations</artifactId> <version>1.9.0</version> </dependency> </dependencies> </project>
別の UI の作成
結果を表示するための UI を別途作成するための前提条件は、Sling の基礎知識があること、CRX ノードを作成すること、適切なアクセス権限を付与することです。
-
/apps ノードで CRX ノードを作成し、適切なアクセス権限を付与します。 (PERM_PROCESS_REPORTING_USER)
-
/content ノードでレンダラーを定義します。
-
手順 1 で作成したノードに JSP または HTML ファイルを追加します。 CSS ファイルも追加できます。
-
QueryBuilder REST API またはサービスの Ajax 呼び出しを開始するための JavaScript コードを追加します。 さらに、適切な引数を追加します。
-
Ajax 呼び出しに適切なサクセスハンドラーを追加し、結果を解析して表示します。 結果を複数の形式(json、csv、ユーザー定義)で解析し、表形式またはその他の形式で表示します。
-
(オプション)適切なエラーハンドラーを Ajax 呼び出しに追加します。
OSGi サービスと QueryBuilder API の両方を使用する JSP サンプルコードは次のとおりです。
<%@taglib prefix="sling" uri="http://sling.apache.org/taglibs/sling/1.0"%> <%request.setAttribute("silentAuthor", new Boolean(true));%> <%@include file="/libs/foundation/global.jsp"%> <%@ page import="java.util.Map, java.util.Set, com.adobe.idp.dsc.registry.service.ServiceRegistry, javax.jcr.Session, org.apache.sling.api.resource.ResourceResolver, custom.reporting.service.PeriodicProcessVolume"%> <% response.setContentType("text/html"); response.setCharacterEncoding("utf-8"); %><!DOCTYPE HTML> <html> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="/lc/apps/sample-report-process-reporting/custom-reports/periodicProcessVolume/style.css"> <title>REPORT Monthly / Qaterly / Yearly</title> <script type="text/javascript"> <% slingResponse.setCharacterEncoding("utf-8"); ResourceResolver resolver = slingRequest.getResourceResolver(); String processName = slingRequest.getParameter("processName"); Session session = resolver.adaptTo(Session.class); custom.reporting.service.PeriodicProcessVolume periodicProcessVolume = sling.getService(custom.reporting.service.PeriodicProcessVolume.class); periodicProcessVolume.populateValues(session, processName); if (processName == null) { processName = "All"; } %> var lineSeprator = "<td class='seprator'>----------------</td>"; var tableEnder = "<tr>" + lineSeprator + lineSeprator + lineSeprator + "</tr>"; var tableColHeader = "<td class='colHead colNum'>Running</td>"; tableColHeader += "<td class='colHead colNum'>Complete</td></tr>"; tableColHeader += tableEnder; var monthly = "<table><tr><td class='colHead colStr'>Month</td>"; monthly += tableColHeader; <% Map<String, Long[]> monthlyMap = periodicProcessVolume.getMonthly(); Set<String> monthKeys = monthlyMap.keySet(); for (String key: monthKeys) { Long[] frequencies = monthlyMap.get(key); %> monthly += "<tr><td class='colStr'> <%= key %> </td>"; monthly += "<td class='colNum'> <%= frequencies[0] %> </td>"; monthly += "<td class='colNum'> <%= frequencies[1] %> </td></tr>"; <% } %> monthly += tableEnder; var quaterly = "<table><tr><td class='colHead colStr'>Quater</td>"; quaterly += tableColHeader; <% Map<String, Long[]> quaterMap = periodicProcessVolume.getQuaterly(); Set<String> quaterKeys = quaterMap.keySet(); for (String key: quaterKeys) { Long[] frequencies = quaterMap.get(key); %> quaterly += "<tr><td class='colStr'> <%= key %> </td>"; quaterly += "<td class='colNum'> <%= frequencies[0] %> </td>"; quaterly += "<td class='colNum'> <%= frequencies[1] %> </td></tr>"; <% } %> quaterly += tableEnder; var yearly = "<table><tr><td class='colHead colStr'>Year</td>"; yearly += tableColHeader; <% Map<Integer, Long[]> yearMap = periodicProcessVolume.getYearly(); Set<Integer> yearKeys = yearMap.keySet(); for (Integer key: yearKeys) { Long[] frequencies = yearMap.get(key); %> yearly += "<tr><td class='colStr'> <%= key %> </td>"; yearly += "<td class='colNum'> <%= frequencies[0] %> </td>"; yearly += "<td class='colNum'> <%= frequencies[1] %> </td></tr>"; <% } %> yearly += tableEnder; function reloadFrame(value) { if (value === '-1') { window.location = "/lc/content/process-reporting-runtime/custom-reports/periodicProcessVolume.html"; } else { window.location = "/lc/content/process-reporting-runtime/custom-reports/periodicProcessVolume.html?processName=" + value; } } function populateTable(selection) { if (selection === 0) { document.getElementById('tableHeading').innerHTML = 'Monthly'; document.getElementById('volumeTable').innerHTML = monthly; } else if (selection === 1) { document.getElementById('tableHeading').innerHTML = 'Quaterly'; document.getElementById('volumeTable').innerHTML = quaterly; } else { document.getElementById('tableHeading').innerHTML = 'Yearly'; document.getElementById('volumeTable').innerHTML = yearly; } } function fetchProcesses() { var xmlhttp = new XMLHttpRequest(), request = ''; xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState === 4 && xmlhttp.status === 200) { var responseText, response, items, hits = [], responseSize = 0, processName, selectedIndex = 0, comboBox; responseText = xmlhttp.responseText; if (responseText !== undefined && responseText !== null) { response = JSON.parse(responseText); responseSize = response.results; hits = response.hits; } items = "<option value='-1'>All</option>"; for(var i = 0; i < responseSize; i++) { processName = hits[i].pmProcessTitle; if (processName === '<%= processName %>') { selectedIndex = i + 1; } items += "<option value='" + processName + "'>" + processName + "</option>" } comboBox = document.getElementById('processSelection'); comboBox.innerHTML = items; comboBox.selectedIndex = selectedIndex; } }; request = "/lc/bin/querybuilder.json?"; request += "exact=false&"; request += "p.hits=selective&"; request += "p.properties=pmProcessTitle&"; request += "path=%2fcontent%2freporting%2fpm&"; request += "property=pmNodeType&"; request += "property.operation=equals&"; request += "property.value=ProcessType&"; request += "type=sling%3aFolder"; xmlhttp.open("POST", request, true); xmlhttp.setRequestHeader("Content-type","application/json"); xmlhttp.send(); } </script> </head> <body onLoad="fetchProcesses();populateTable(0);"> Process: <select id="processSelection" onchange="reloadFrame(this.value);"></select>     Period Interval: <select name="periodSelection" onchange="populateTable(this.selectedIndex);"> <option value="1">Monthly</option> <option value="2">Quaterly</option> <option value="3">Yearly</option> </select> <br> <br> <br> <br> <div class="inline"> Process:   <b><%= processName %></b>     Period:   </div> <b> <div id="tableHeading" class="inline"> </div> </b> <br><br> <div id="volumeTable"> </div> </body> </html>
既存のプロセスレポート UI へのレポート UI の統合
結果を表示するための UI を別途作成するための前提条件は、Sling の基礎知識があること、CRX ノードを作成すること、適切なアクセス権限を付与することです。
-
「別の UI を作成」セクションの説明に従って別の UI を作成します。
-
プラグ可能なレポートについてはその都度 /content/process-reporting-runtime/custom-reports ノードで子ノード nt:unstructured を作成します。
- id - レポートの固有の ID 番号を指定します。
- name - レポートの名前を指定します。 この名前が UI に表示されます。
- link - 別の UI のレンダラーへの相対リンクを指定します。 このリンクは手順 1 で作成したものです。
- description - レポートの説明を 1 行で指定します。 description フィールドは空欄のままでも構いません。
- icon - レポートを表す画像を指定します。 icon フィールドは空欄のままでも構いません。
-
レポート UI がプロセスレポート UI に統合されます。 UI を統合すると、更新後に次のようになります。
サンプルパッケージ
sample-report-pkg-1.zip パッケージを読み込み、記事で説明しているカスタムレポートおよび UI を Process Management UI と統合します。
ダウンロード