Utilisation des valeurs d'une ligne d'un objet Service dans une action

Request description

Bonjour,

J’ai créé une action en row sur un objet Service mais j’ai l’impression que les valeurs de l’instance d’objet ne sont pas bonnes (elle semblent être celles de la dernière ligne remontée lors du searchService).

image

image

Il n’y a aucune persistance même si on est encore sur la liste ? Ou j’ai mal fait quelque chose ?

Merci d’avance pour votre retour
Emmanuelle

[Platform]
Status=OK
Version=5.3.61
BuiltOn=2025-01-16 09:26

Bonjour,

Petit up sur ce ticket, au cas où il soit passé à la trappe.
Merci d’avance !

Emmanuelle

Ce n’était pas zappé, juste pas encore eu le temps d’investiguer.

De quel type d’objet service parle-t-on exactement ? Et quelle est sa configuration ?

D’accord désolée, ça peut attendre la semaine prochaine si besoin (je ne travaille pas le jeudi vendredi)

Ma première question était simplement “est-ce que c’est sensé marcher”, pour une action en row sur un objet service ? Car si oui je vais investiguer un peu plus de mon côté.

Mon objet est paramétré en “service”

Et dans le searchService, j’appelle une API de Service Now et je retourne les résultats dans une liste.

Déjà il existe un type d’objet service pour ServiceNow, cf. ServiceNow integration

Ensuite, les actions des objets service appellent uniquement le hook actionService qui doit donc être implémenté si on souhaite que ça fasse quelque chose => à voir si ce serait pertinent de fallbacker sur une action “locale” si ce hook ne fait rien (i.e. retourne null)

Par exemple, pour les objets service Simplicité, qui est le seul type d’objet service qui implémente ce hook par défaut, l’implémentation consiste à appeler l’action en question sur l’instance remote.

En revanche, pour les objets service ServiceNow le hook n’est donc pas implémenté par défaut

Je vais regarder pour ce qui est du fait de passer par le initAction et de savoir quel record est sélectionné à ce point si on y passe. Le sujet des actions sur les objets service reste un sujet ouvert à ce stade, c’est l’occasion de pousser ça un peu plus loin.

Oui je t’avais demandé la doc pour les objets ServiceNow mais je n’ai malheureusement pas les accès nécessaires pour le paramétrer (des histoires administratives). Donc je passe par l’API qui nous est mise à disponibilité.

J’ai vu des éléments de réponse sur actionService dans ce ticket. Je vais voir si je peux commencer avec ça.

Je suis dispo pour beta tester si jamais ce fonctionnement évolue, en tout cas.

Dans la réponse du post cité je dis:

si le hook actionService n’est pas implémenté, ça fait comme pour tout objet, ça appelle la méthode paramétrée dans l’action

A la relecture du code j’ai un petit doute sur ce point, mais je vais vérifier…

Je comprends que dans ton cas tu as implémenté un objet service de base appelant une API custom qui interagit avec ServiceNow car tu n’as pas accès directement aux APIs ServiceNow.

Peux tu indiquer ce que tu as implémenté exactement comme hooks car le comportement d’un objet service dépend de ce qui est implémenté et je voudrais me mettre dans une configuration de test la plus proche possible de ton cas

J’ai un peu honte de te coller tout mon code à dépiauter, d’autant qu’il n’est pas forcément très joli, tu me dis si je peux faire quelque chose pour simplifier …

PS : dans le count, j’ai mis en dur pour l’instant car je peine à accéder à l’API aggregate de ServiceNow, mais à terme ça sera un vrai count.

package com.simplicite.objects.RCIB;

import java.util.*;

import com.simplicite.util.*;
import com.simplicite.util.exceptions.*;
import com.simplicite.util.tools.*;
import com.simplicite.commons.RCIB.RciSnow;
import org.json.JSONObject;
import org.json.JSONArray;
import java.text.SimpleDateFormat;


/**
 * Business object RciSnowApplication
 */
public class RciSnowApplication extends com.simplicite.util.ObjectService {
	private static final long serialVersionUID = 1L;
	
	private static final String COUNTRY_FIELDNAME = "rciCreateAppCountries";
	private static final String IRN_FIELDNAME = "rciSearchIrn";
	private static final String DIVISION_FIELDNAME = "rciAppDivision";
	private static final String ORG_FIELDNAME = "rciAppOrgdomId";
	private static final String CORP_FIELDNAME = "rciAppCorp";
	private static final String ORG_DOMNAME_FIELDNAME = "rciAppOrgdomId.rciOrgDomName";
	private static final String DOMLEAD = "DOMLEAD";


	protected static String DIVISION_LOCAL = "LOCAL";
	
	@Override
	public void preSearch() {

		if (getInstanceName().equals("home_ajax_RciSnowApplication_17"))
		{
			ObjectDB user = getGrant().getTmpObject("RciUser");
			user.resetFilters();
			
			if (user.select(Integer.toString(getGrant().getUserId())))
			{
				String cty = user.getFieldValue("rciUsrCtyId.isoCtyCode2");
				
				//Bidouille pour gérer le cas spécific des nomenclatures CORP
				if (cty.equals("CP"))
					cty = "CORP";
					
				String managedBy = "RCI " + cty;
				setFieldFilter("rciSnaManagedBy",managedBy);
			}
			
		}
		
		super.preSearch();
	}
	
	@Override
	public long countService() {		
		return 2000;
	}

	@Override
	public List<String[]> searchService(boolean pagine) {

		List<String[]> snowApps = new ArrayList<>();

		try
		{
			String apiFilters = "";
			String appExistsFilter = "";
			
			for (ObjectField f : getFields())
			{
				if (f.isFiltered())
				{
					if (f.getName().equals("rciSnaAppExists"))
					{
						appExistsFilter = f.getFilter();
					}
					else
					{

						apiFilters = apiFilters + RciSnow.earToCmdbFilter(f.getName(), HTTPTool.encode(f.getFilter())) + "^";
					}
				}
			}
			
			if (apiFilters.length() > 0)
				apiFilters = apiFilters.substring(0, apiFilters.length() - 1);
			
			String limit = "", page = "";
			
			if (appExistsFilter.isEmpty())
			{
				limit = Integer.toString(getMaxRows());
				page = Integer.toString(getCurrentPage());
				setLimit(true);
			}
			else
			{
				limit = "2000";
				page = "0";
				setLimit(false);
			}
			
			String res = RciSnow.reqSnowAppWithLimit(null, limit, page, apiFilters, getGrant());
			
			if (res != null)
			{
				JSONObject resJSON = new JSONObject(res);
				
				if (resJSON != null && resJSON.getJSONArray("result") != null && resJSON.getJSONArray("result").length() > 0)
				{

					JSONArray appsJSON = resJSON.getJSONArray("result");
					
					String key = "";
				    String snaField = "";
				    String snowField = "";
					
					//for (int j = 0 ; j < appsJSON.length() ; j++)
					for (int j = 0 ; j < appsJSON.length() ; j++)
					{
						JSONObject appJSON = appsJSON.getJSONObject(j);

						String[] snowApp = new String[getFields().size()];
						
						String irn = appJSON.getString("name");
						snowApp[getFieldIndex("rciSnaIdentifier")] = irn;
						
						Boolean appExists = appExists(irn);
						snowApp[getFieldIndex("rciSnaAppExists")] = Boolean.toString(appExists);
						
						if (appExistsFilter.isEmpty() || ((appExistsFilter.equals("1") && appExists) || (appExistsFilter.equals("0") && !appExists)))
						{
							for (Map.Entry<String, String> entry : RciSnow.CMDB_EAR_FIELDS.entrySet()) {
											
							    key = entry.getKey();
							    snaField = "rciSna" + key;
							    snowField = entry.getValue();
							    
							    if (getField(snaField, false) != null)
							    	snowApp[getFieldIndex(snaField)] = RciSnow.cmdbToEar(snowField, appJSON);
							}
							
							snowApps.add(snowApp);

						}

					}
				}
			}
			
			
		}
		catch (Exception e) {
			AppLog.error(e, getGrant());
		}

		return snowApps;
	}
	
	public boolean appExists(String name)
	{
		ObjectDB app = getGrant().getTmpObject("RciApplication");

		app.resetFilters();
		app.setFieldFilter("rciAppIdentifier",name);
		app.setFieldFilter("rciAppIsBackup",false);
		app.setFieldFilter("rciFieldIsArchived",false);

		return app.getCount() > 0;

	}

Et le initAction avec le log qui me remonte le mauvais IRN

	@Override
	public void initAction(Action action) {
		String lang = getGrant().getLang();
	
		if (action.getName().equals("createApp") || action.getName().equals("searchIRN"))
		{
			AppLog.info("IRN " + getFieldValue("rciSnaIdentifier"), getGrant());
			ObjectField f = action.getConfirmField(lang, COUNTRY_FIELDNAME);
			ObjectField org = action.getConfirmField(lang, ORG_FIELDNAME);
			ObjectField orgdom = action.getConfirmField(lang,ORG_DOMNAME_FIELDNAME);
			
			if (orgdom == null)
				action.addConfirmField(lang, getGrant().getTmpObject("RciApplication").getField(ORG_DOMNAME_FIELDNAME));
			
			ObjectDB sub = getGrant().getTmpObject("RciSubsidiary");
			sub.resetFilters();
			sub.getField("rciSubScope").setOrder(1);
			
			List<String[]> subList = sub.search();

			
			f.getList().getItems().clear();
			for (int i = 0 ; i < subList.size() ; i++)
			{	
				sub.setValues(subList.get(i));
				f.getList().putItem(sub.getFieldValue("rciSubCouId"), sub.getFieldValue("rciSubScope"), true);
			}

			org.setRequired(false);
			orgdom.setRequired(false);
		}
		
		if (action.getName().equals("searchIRN"))
		{

			ObjectField org = action.getConfirmField(lang, ORG_FIELDNAME), irn = action.getConfirmField(lang, IRN_FIELDNAME);
			ObjectField orgdom = action.getConfirmField(lang, ORG_DOMNAME_FIELDNAME);
			
			org.setRequired(false);
			orgdom.setRequired(false);
			irn.setRequired(true);
			irn.setDefaultValue("IRN-");
		}
		
		super.initAction(action);
	}

OK je vais faire des tests mais je n’aurai pas le temps de le faire aujourd’hui, ni demain je pense.

Le pattern usuel avec les objets service quand l’API utilisée ne permet pas de faire juste un “count” c’est de faire le “search” dans le countService et de conserver le résultat (via un setCurrentList() ou en conservant la réponse brute dans un param d’objet) pour le réutiliser dans le searchService

NB: les API natives de ServiceNow permettent de faire un “count” en trichant un peu = en faisant un search paginé à 1 (de mémoire 0 ne marche pas) et en lisant le header X-Total-Count, la vraie pagination étant utilisée au niveau du “search”

Merci beaucoup David pour tous ces éléments, j’ai déjà de quoi avancer pour aujourd’hui et je reprendrai lundi donc pas de souci. (C’est pour une fonctionnalité qui devrait être disponible idéalement pour la fin du mois, ce n’est pas urgent.)

J’avais tenté le X-Total-Count sans succès mais je vais m’acharner un peu si c’est une bonne piste.

Voilà comment sont implémentés les count et search dans la classe ServiceNowAPITool:

	/**
	 * Run a count-only search query
	 * @param query Search query
	 * @return Search count
	 */
	public long count(String query) throws Exception
	{
		String table = m_config.getString("table");
		String url = m_config.getString("url") + DATA_PATH.replaceAll("\\$1", table) +
			"?sysparm_limit=1&sysparm_fields=sys_id" +
			(!Tool.isEmpty(query) ? "&sysparm_query=" + HTTPTool.encode(query) : "");

		HttpResponse<String> r = Unirest.get(url).basicAuth(m_config.getString("username"), m_config.getString("password")).asString();
		if (r.getStatus()!=200)
			throw new SearchException("Count failed, status: " + r.getStatus() + " (" + r.getStatusText() + ")");

		return Tool.parseLong(r.getHeaders().get("X-Total-Count").get(0), 0);
	}

	/**
	 * Run a search query
	 * @param query Search query
	 * @param fields Fields to return (null means all fields)
	 * @return Search result as JSON array
	 */
	public JSONArray search(String query, String fields, long limit, long offset) throws Exception
	{
		String table = m_config.getString("table");
		String url = m_config.getString("url") + DATA_PATH.replaceAll("\\$1", table) +
			"?sysparm_no_count=true&sysparm_exclude_reference_link=true" +
			(m_config.optBoolean("display") ? "&sysparm_display_value=true" : "") +
			(limit>0 ? "&sysparm_limit=" + limit : "") +
			(offset>=0 ? "&sysparm_offset=" + offset : "") +
			(!Tool.isEmpty(fields) ? "&sysparm_fields=" + fields : "") +
			(!Tool.isEmpty(query) ? "&sysparm_query=" + HTTPTool.encode(query) : "");

		HttpResponse<String> r = Unirest.get(url).basicAuth(m_config.getString("username"), m_config.getString("password")).asString();
		if (r.getStatus()!=200)
			throw new GetException("Search failed, status: " + r.getStatus() + " (" + r.getStatusText() + ")");

		return new JSONObject(r.getBody()).getJSONArray("result");
	}

La triche consiste à ne demander au count qu’un seul record et un seul attribut (sys_id) pour limiter au max le volume de la réponse et de se baser sur le header X-Total-Count

La mise en forme de ces résultats bruts pour l’objet service ServiceNow se faisant dans la classe ObjectServiceServiceNow

1 Like

Parfait pour le count ça fonctionne sans impacter les performances, merci !

Je reprends le sujet, j’ai configuré un objet service basique:

Auquel j’ai associé l’action suivante:

Et voici le code de cet objet:

package com.simplicite.objects.Application;

import java.util.ArrayList;
import java.util.List;

import com.simplicite.util.Action;
import com.simplicite.util.AppLog;
import com.simplicite.util.Message;
import com.simplicite.util.ObjectService;
import com.simplicite.util.annotations.BusinessObjectAction;

public class AppTestService extends ObjectService {
	private static final long serialVersionUID = 1L;

	private static final int NBROWS = 10;

	@Override
	public long countService() {
		return NBROWS;
	}

	private String[] getRow(String rowId) {
		return new String[] { rowId, "Code " + rowId };
	}

	@Override
	public List<String[]> searchService(boolean pagine) {
		List<String[]> rows = new ArrayList<>();
		for (int i = 1; i <= NBROWS; i++)
			rows.add(getRow(String.valueOf(i)));
		return rows;
	}

	@Override
	public boolean selectService(String rowId, boolean copy) {
		setValues(getRow(rowId));
		return true;
	}

	@Override
	public void initAction(Action action) {
		AppLog.info("Action " + action.getName() + " invoked for row ID " + getRowId() + " (" + getFieldValue("appApsCode") + ")", getGrant());
	}

	@BusinessObjectAction
	public String testAction(Action action) {
		return Message.formatSimpleInfo("Action result for row ID " + getRowId() + " (" + getFieldValue("appApsCode") + ")");
	}
}

A l’exécution ça fait ce qu’il faut

1 Like

Merci David pour cet exemple, déjà je n’avais pas positionné le rowId, ni surchargé le selectService.
Est-ce que dans ce dernier, je dois réappeler l’API ? Sinon je ne vois pas comment faire le setValues correctement ?

Si tu as affaire à des données qui sont susceptibles d’avoir changé entre le moment où la liste est affichée et le moment où tu ouvre le formulaire il vaut mieux faire un appel API dans le selectService pour récupérer le record à jour.

C’est que ce font nos objets service ServiceNow via la méthode suivante de ServiceNowTool:

	/**
	 * Run a select
	 * @param rowId Record row ID
	 * @param fields Fields to return (null means all fields)
	 * @return Select result as JSON object
	 */
	public JSONObject select(String rowId, String fields) throws Exception
	{
		if (Tool.isEmpty(rowId))
			throw new GetException("Empty row ID (sys_id)");

		String table = m_config.getString("table");
		String url = m_config.getString("url") + DATA_PATH.replaceAll("\\$1", table) + "/" + rowId +
			"?sysparm_no_count=true&sysparm_exclude_reference_link=true" +
			(m_config.optBoolean("display") ? "&sysparm_display_value=true" : "") +
			(!Tool.isEmpty(fields) ? "&sysparm_fields=" + fields : "");

		HttpResponse<String> r = Unirest.get(url).basicAuth(m_config.getString("username"), m_config.getString("password")).asString();

		if (r.getStatus()!=200)
			throw new GetException("Select failed, status: " + r.getStatus() + " (" + r.getStatusText() + ")");

		return new JSONObject(r.getBody()).getJSONObject("result");
	}

Ce que je ne comprends pas bien si tu utilise des APIs qui semblent être des APIs standard ServiceNow c’est pourquoi tu n’utilise pas simplement un objet service ServiceNow plutôt que de refaire spécifiquement la même chose

Sinon il y a toujours la possibilité de réutiliser les données issues du searchService pour valoriser le record courant dans le selectService.

Mais en général pour optimiser les flux (et selon les capacités de l’API en face), on ne récupère pas forcément tous les attributs affichés en formulaire lors d’un search pour l’affichage de la liste.

J’ai eu l’impression en regardant la configuration serviceNow, qu’il fallait un User Mdp à mettre dans le filter de l’objet, or nous passons par une API gateway avec une api Key.

C’est ça qui m’échappe, dans ton exemple tu génères les valeurs au moment du select à partir du rowId, mais de mon côté j’essaie de récupérer celles de ma ligne en liste et je n’y arrive pas.
Enfin, le initAction me renvoie les bonnes valeurs.
Mais quand je passe dans l’action, je me retrouve avec celles de la dernière ligne …

J’ai eu l’impression en regardant la configuration serviceNow, qu’il fallait un User Mdp à mettre dans le filter de l’objet, or nous passons par une API gateway avec une api Key.

En l’état les credentials sont un username+password (utilisé dans un header basic auth HTTP) car ça correspond au mode d’ident/authent ServiceNow qu’on a rencontré à ce jour mais on peut faire évoluer l’objet service pour qu’il sache faire autre chose en termes d’ident/authent.

Quelle est la manière dont ton API key est passée à l’appel dans ton cas ? Header HTTP ? Paramètre URL ? autre ?

Sinon s’agissant de mon test, je suis en v6 à jour (6.2.0). je ne pense pas que ça soit radicalement différent en 5.3 si ce n’est qu’il y a l’appel aux éventuelles implémentation Rhino qui compliquent les choses. je vais tester.

Rester en v5 ad vitam aerternam n’est vraiment pas une bonne chose . Comme je le rappelle régulièrement la maintenance long terme est uniquement destinée aux applications en prod qui n’évoluent plus du tout.

Quand on est encore ne phase projet active il faut impérativement suivre le mouvement, d’autant que l’effort pour passer de v5 à v6 n’est en général pas très élevé, en tout cas c’est inférieur à l’effort nécessaire à refaire en v5 des choses qui ont été ajoutées/améliorées en v6

Typiquement l’évolution sur l’ident/authent des objets ServiceNow dont je parle plus haut n’a pas normalement vocation à être backportée en v5.

PI, en v6 on vient aussi d’ajouter une implémentation par défaut du selectService qui va chercher le row ID dans la current list (pour des cas simple ça suffira), ça ne sera pas backporté en v5.

Bref fondamentalement c’est quoi qui bloque pour passer en v6 dans to cas ?

Je passe l’apiKey en header en effet.
Et oui on prévoit de passer en V6 très bientôt.

J’ai besoin de savoir quel header et quel format de la valeur de ce header ?