001/* 002 * Copyright 2017 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.glpi; 017 018import java.io.ByteArrayOutputStream; 019import java.io.IOException; 020import java.io.InputStream; 021import java.net.URI; 022import java.net.URISyntaxException; 023import java.util.ArrayList; 024import java.util.Collections; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.concurrent.Callable; 029import java.util.concurrent.ExecutionException; 030import java.util.concurrent.TimeUnit; 031import java.util.stream.Collectors; 032 033import org.apache.avalon.framework.activity.Initializable; 034import org.apache.avalon.framework.component.Component; 035import org.apache.avalon.framework.service.ServiceException; 036import org.apache.avalon.framework.service.ServiceManager; 037import org.apache.avalon.framework.service.Serviceable; 038import org.apache.commons.io.IOUtils; 039import org.apache.commons.lang3.StringUtils; 040import org.apache.http.client.config.RequestConfig; 041import org.apache.http.client.methods.CloseableHttpResponse; 042import org.apache.http.client.methods.HttpGet; 043import org.apache.http.client.utils.URIBuilder; 044import org.apache.http.impl.client.CloseableHttpClient; 045import org.apache.http.impl.client.HttpClientBuilder; 046 047import org.ametys.core.user.UserIdentity; 048import org.ametys.core.util.JSONUtils; 049import org.ametys.runtime.config.Config; 050import org.ametys.runtime.i18n.I18nizableText; 051import org.ametys.runtime.plugin.component.AbstractLogEnabled; 052import org.ametys.runtime.plugin.component.PluginAware; 053 054import com.google.common.cache.Cache; 055import com.google.common.cache.CacheBuilder; 056import com.google.common.cache.CacheLoader; 057import com.google.common.cache.LoadingCache; 058 059/** 060 * Connection and information from GLPI webservice 061 */ 062public class TicketGlpiManager extends AbstractLogEnabled implements Component, Initializable, Serviceable, PluginAware 063{ 064 /** Avalon ROLE. */ 065 public static final String ROLE = TicketGlpiManager.class.getName(); 066 067 private static final String __GLPI_INIT_SESSION = "/initSession/"; 068 069 private static final String __GLPI_SEARCH_USERS = "/search/user/"; 070 071 private static final String __GLPI_SEARCH_TICKET = "/search/ticket/"; 072 073 private static final String __GLPI_KILL_SESSION = "/killSession/"; 074 075 /** Maximum cache size, in number of records. */ 076 protected long _maxCacheSize; 077 078 /** The cache TTL in minutes. */ 079 protected long _cacheTtl; 080 081 /** 082 * The user information cache. The key of the cache is the user identity it self 083 */ 084 protected LoadingCache<UserIdentity, Map<String, Object>> _cache; 085 086 /** 087 * The user information cache. The key of the cache is the user identity it self 088 */ 089 protected Cache<String, Integer> _cacheIdentities; 090 091 private JSONUtils _jsonUtils; 092 093 private Map<Integer, I18nizableText> _glpiStatus; 094 private Map<Integer, I18nizableText> _glpiType; 095 096 private String _pluginName; 097 098 public void setPluginInfo(String pluginName, String featureName, String id) 099 { 100 _pluginName = pluginName; 101 } 102 103 @Override 104 public void service(ServiceManager manager) throws ServiceException 105 { 106 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 107 } 108 109 @Override 110 public void initialize() 111 { 112 // Tickets cache 113 GlpiCacheLoader loader = new GlpiCacheLoader(); 114 115 Long maxCacheSizeConf = Config.getInstance().getValueAsLong("org.ametys.plugins.glpi.maxsize"); 116 Long maxCacheSize = (long) (maxCacheSizeConf != null ? maxCacheSizeConf.intValue() : 1000); 117 118 Long cacheTtlConf = Config.getInstance().getValueAsLong("org.ametys.plugins.glpi.ttl"); 119 Long cacheTtl = (long) (cacheTtlConf != null && cacheTtlConf.intValue() >= 0 ? cacheTtlConf.intValue() : 60); 120 121 CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder().expireAfterWrite(cacheTtl, TimeUnit.MINUTES); 122 123 if (maxCacheSize > 0) 124 { 125 cacheBuilder.maximumSize(maxCacheSize); 126 } 127 128 _cache = cacheBuilder.build(loader); 129 130 // Identities cache 131 maxCacheSizeConf = Config.getInstance().getValueAsLong("org.ametys.plugins.glpi.maxsize.identities"); 132 maxCacheSize = (long) (maxCacheSizeConf != null ? maxCacheSizeConf.intValue() : 5000); 133 134 cacheTtlConf = Config.getInstance().getValueAsLong("org.ametys.plugins.glpi.ttl.identities"); 135 cacheTtl = (long) (cacheTtlConf != null && cacheTtlConf.intValue() >= 0 ? cacheTtlConf.intValue() : 60); 136 137 CacheBuilder<Object, Object> cacheIdentitiesBuilder = CacheBuilder.newBuilder().expireAfterWrite(cacheTtl, TimeUnit.MINUTES); 138 139 if (maxCacheSize > 0) 140 { 141 cacheIdentitiesBuilder.maximumSize(maxCacheSize); 142 } 143 144 _cacheIdentities = cacheIdentitiesBuilder.build(); 145 } 146 147 /** 148 * Get the user collaboration information from the exchange server. 149 * 150 * @param userIdentity the user identity. 151 * @return the user collaboration information as a Map. 152 * @throws Exception If an error occurred 153 */ 154 protected Map<String, Object> loadUserInfo(UserIdentity userIdentity) throws Exception 155 { 156 Map<String, Object> userInfo = new HashMap<>(); 157 158 String glpiUrl = Config.getInstance().getValueAsString("org.ametys.plugins.glpi.url"); 159 String usertoken = Config.getInstance().getValueAsString("org.ametys.plugins.glpi.usertoken"); 160 String apptoken = Config.getInstance().getValueAsString("org.ametys.plugins.glpi.apptoken"); 161 162 if (glpiUrl == null || usertoken == null || apptoken == null) 163 { 164 if (getLogger().isWarnEnabled()) 165 { 166 getLogger().warn("Missing configuration: unable to contact the GLPI WebService Rest API, the configuration is incomplete."); 167 } 168 169 return userInfo; 170 } 171 172 RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(10000).setSocketTimeout(10000).build(); 173 174 try (CloseableHttpClient httpclient = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).useSystemProperties().build()) 175 { 176 String sessionToken = _getGlpiSessionToken(httpclient, glpiUrl, usertoken, apptoken); 177 if (sessionToken == null) 178 { 179 return userInfo; 180 } 181 182 try 183 { 184 Integer userId = _cacheIdentities.get(userIdentity.getLogin().toLowerCase(), new Callable<Integer>() 185 { 186 @Override 187 public Integer call() throws Exception 188 { 189 return getUserIdentity(httpclient, userIdentity, glpiUrl, sessionToken, apptoken); 190 } 191 }); 192 193 if (userId != null && userId != -1) 194 { 195 Map<String, Object> glpiOpenTickets = getGlpiTickets(httpclient, glpiUrl, sessionToken, apptoken, userId); 196 if (glpiOpenTickets != null && !glpiOpenTickets.isEmpty()) 197 { 198 userInfo.put("countOpenTickets", glpiOpenTickets.get("countOpenTickets")); 199 userInfo.put("openTickets", glpiOpenTickets.get("openTickets")); 200 } 201 } 202 else 203 { 204 getLogger().debug("GPLI identity not found for user {}", userIdentity); 205 } 206 } 207 finally 208 { 209 _killGlpiSessionToken(httpclient, glpiUrl, sessionToken, apptoken); 210 } 211 } 212 213 return userInfo; 214 } 215 216 private String _getGlpiSessionToken(CloseableHttpClient httpclient, String glpiUrl, String usertoken, String apptoken) throws IOException, URISyntaxException 217 { 218 URIBuilder builder = new URIBuilder(glpiUrl + __GLPI_INIT_SESSION).addParameter("user_token", usertoken).addParameter("app_token", apptoken); 219 220 if (getLogger().isDebugEnabled()) 221 { 222 getLogger().debug("Call GLPI webservice to init session : " + builder.build()); 223 } 224 225 Map<String, Object> jsonObject = _callWebServiceApi(httpclient, builder.build(), true); 226 if (jsonObject != null && jsonObject.containsKey("session_token")) 227 { 228 String token = (String) jsonObject.get("session_token"); 229 if (getLogger().isDebugEnabled()) 230 { 231 getLogger().debug("GPLI WS returned session token '" + token + "'"); 232 } 233 return token; 234 } 235 236 if (getLogger().isDebugEnabled()) 237 { 238 getLogger().debug("GPLI WS returned no session token for user token '" + usertoken + "'"); 239 } 240 241 return null; 242 } 243 244 /** 245 * Get the user identity and fill the user identities cache 246 * 247 * @param httpclient The http client to send a request to the webservice 248 * @param userIdentity The current user identity 249 * @param glpiUrl The GLPI Url 250 * @param sessionToken The session token 251 * @param apptoken The app token 252 * @return The user identity corresponding, or null if not found 253 * @throws Exception if an error occurred 254 */ 255 @SuppressWarnings("unchecked") 256 protected Integer getUserIdentity(CloseableHttpClient httpclient, UserIdentity userIdentity, String glpiUrl, String sessionToken, String apptoken) throws Exception 257 { 258 Long maxIdentitiesSize = Config.getInstance().getValueAsLong("org.ametys.plugins.glpi.maxsize.identities"); 259 URIBuilder builder = new URIBuilder(glpiUrl + __GLPI_SEARCH_USERS) 260 .addParameter("range", "0-" + (maxIdentitiesSize != null ? maxIdentitiesSize : 1000)) 261 .addParameter("forcedisplay[0]", "1") 262 .addParameter("forcedisplay[1]", "2") 263 .addParameter("criteria[0][field]", "8") 264 .addParameter("criteria[0][searchtype]", "contains") 265 .addParameter("criteria[0][value]", "1") 266 .addParameter("session_token", sessionToken) 267 .addParameter("app_token", apptoken); 268 Map<String, Object> jsonObject = _callWebServiceApi(httpclient, builder.build(), true); 269 if (jsonObject != null && jsonObject.containsKey("data")) 270 { 271 List<Map<String, Object>> data = (List<Map<String, Object>>) jsonObject.get("data"); 272 Map<String, Integer> userIdentities = data.stream().filter(user -> StringUtils.isNotEmpty((String) user.get("1")) && user.containsKey("2")).collect( 273 Collectors.toMap(user -> ((String) user.get("1")).toLowerCase(), user -> (Integer) user.get("2"))); 274 _cacheIdentities.putAll(userIdentities); 275 276 if (userIdentities.containsKey(userIdentity.getLogin().toLowerCase())) 277 { 278 return userIdentities.get(userIdentity.getLogin().toLowerCase()); 279 } 280 } 281 return -1; 282 } 283 284 /** 285 * Get the GLPI tickets 286 * @param httpclient the HTTP client 287 * @param glpiUrl The url of GLPI server 288 * @param sessionToken The session token 289 * @param apptoken The app token 290 * @param userId The user id 291 * @return The tickets 292 * @throws IOException if an error occurred 293 * @throws URISyntaxException if failed to build uri 294 */ 295 protected Map<String, Object> getGlpiTickets(CloseableHttpClient httpclient, String glpiUrl, String sessionToken, String apptoken, Integer userId) throws IOException, URISyntaxException 296 { 297 StringBuilder uri = new StringBuilder(glpiUrl + __GLPI_SEARCH_TICKET + "?"); 298 299 uri.append(getTicketSearchQuery(userId)) 300 .append("&session_token=").append(sessionToken) 301 .append("&app_token=").append(apptoken); 302 303 if (getLogger().isDebugEnabled()) 304 { 305 getLogger().debug("Call GLPI webservice to search tickets : " + uri.toString()); 306 } 307 308 Map<String, Object> jsonObject = _callWebServiceApi(httpclient, new URI(uri.toString()), true); 309 if (jsonObject != null) 310 { 311 Map<String, Object> glpiTicketsInfo = new HashMap<>(); 312 if (jsonObject.containsKey("totalcount")) 313 { 314 glpiTicketsInfo.put("countOpenTickets", jsonObject.get("totalcount")); 315 } 316 if (jsonObject.containsKey("data")) 317 { 318 Object dataObject = jsonObject.get("data"); 319 List<GlpiTicket> glpiTickets = new ArrayList<>(); 320 if (dataObject instanceof List) 321 { 322 @SuppressWarnings("unchecked") 323 List<Object> dataList = (List<Object>) dataObject; 324 for (Object object : dataList) 325 { 326 if (object instanceof Map) 327 { 328 @SuppressWarnings("unchecked") 329 Map<String, Object> ticketData = (Map<String, Object>) object; 330 glpiTickets.add(parseTicket(ticketData)); 331 } 332 } 333 glpiTicketsInfo.put("openTickets", glpiTickets); 334 } 335 } 336 return glpiTicketsInfo; 337 } 338 return null; 339 } 340 341 /** 342 * Parse data into {@link GlpiTicket} 343 * @param data the json data 344 * @return the {@link GlpiTicket} 345 */ 346 protected GlpiTicket parseTicket(Map<String, Object> data) 347 { 348 int ticketId = (int) data.get("2"); 349 String ticketTitle = (String) data.get("1"); 350 int status = (int) data.get("12"); 351 352 int type = data.containsKey("14") ? (int) data.get("14") : -1; 353 String category = (String) data.get("7"); 354 355 GlpiTicket ticket = new GlpiTicket(ticketId, ticketTitle, status, type, category); 356 return ticket; 357 } 358 359 /** 360 * Get the part of rest API url for tickets search 361 * @param userId The user id 362 * @return The search query to concat to rest API url 363 */ 364 protected String getTicketSearchQuery(Integer userId) 365 { 366 StringBuilder sb = new StringBuilder(); 367 368 // forcedisplay is used for data to return 369 370 // To get all available search options, call GLPI_URL/listSearchOptions/Ticket?session_token=... 371 372 sb.append("forcedisplay[0]=1") // title 373 .append("&forcedisplay[1]=12") // status 374 .append("&forcedisplay[2]=4") // user id 375 .append("&forcedisplay[3]=2") // ticket id 376 .append("&forcedisplay[4]=7") // category 377 .append("&forcedisplay[6]=14") // type 378 .append("&criteria[0][field]=12&criteria[0][searchtype]=0&criteria[0][value]=notold") // status=notold (unresolved) 379 .append("&criteria[1][link]=AND") 380 .append("&criteria[1][field]=4&criteria[1][searchtype]=equals&criteria[1][value]=" + userId) // current user is the creator 381 .append("&sort=4"); // sort 382 383 // Get tickets with unresolved status (notold) 384 return sb.toString(); 385 } 386 387 private void _killGlpiSessionToken(CloseableHttpClient httpclient, String glpiUrl, String sessionToken, String apptoken) throws IOException, URISyntaxException 388 { 389 URIBuilder builder = new URIBuilder(glpiUrl + __GLPI_KILL_SESSION).addParameter("session_token", sessionToken).addParameter("app_token", apptoken); 390 _callWebServiceApi(httpclient, builder.build(), false); 391 } 392 393 private Map<String, Object> _callWebServiceApi(CloseableHttpClient httpclient, URI uri, boolean getJsonObject) throws IOException 394 { 395 HttpGet get = new HttpGet(uri); 396 397 try (CloseableHttpResponse httpResponse = httpclient.execute(get)) 398 { 399 if (httpResponse.getStatusLine().getStatusCode() != 200) 400 { 401 String msg = null; 402 if (httpResponse.getEntity() != null) 403 { 404 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 405 try (InputStream is = httpResponse.getEntity().getContent()) 406 { 407 IOUtils.copy(is, bos); 408 } 409 410 msg = bos.toString("UTF-8"); 411 } 412 413 getLogger().error("An error occurred while contacting the GLPI Rest API (status code : " + httpResponse.getStatusLine().getStatusCode() + "). Response is : " + msg); 414 return null; 415 } 416 417 if (getJsonObject) 418 { 419 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 420 try (InputStream is = httpResponse.getEntity().getContent()) 421 { 422 IOUtils.copy(is, bos); 423 } 424 425 if (getLogger().isDebugEnabled()) 426 { 427 getLogger().debug("GLPI webservice at uri " + uri + " returned : " + bos.toString("UTF-8")); 428 } 429 430 return _jsonUtils.convertJsonToMap(bos.toString("UTF-8")); 431 } 432 } 433 434 return null; 435 } 436 437 /** 438 * Get the number of open tickets 439 * 440 * @param userIdentity The user identity 441 * @return the number of unread mail 442 * @throws ExecutionException If an error occured during the connection to 443 * the server 444 */ 445 public int getCountOpenTickets(UserIdentity userIdentity) throws ExecutionException 446 { 447 if (userIdentity == null) 448 { 449 throw new IllegalArgumentException("User is not connected"); 450 } 451 452 Map<String, Object> userInfo = _cache.get(userIdentity); 453 454 if (userInfo.get("countOpenTickets") != null) 455 { 456 return (Integer) userInfo.get("countOpenTickets"); 457 } 458 else 459 { 460 return -1; 461 //throw new IllegalArgumentException("Unable to get open tickets count. No user matches in GPLI with login " + userIdentity.getLogin()); 462 } 463 } 464 465 /** 466 * Get all information about open tickets 467 * 468 * @param userIdentity The user identity 469 * @return List of all information about open tickets 470 * @throws ExecutionException If an error occured during the connection to 471 * the server 472 */ 473 @SuppressWarnings("unchecked") 474 public List<GlpiTicket> getOpenTickets(UserIdentity userIdentity) throws ExecutionException 475 { 476 if (userIdentity == null) 477 { 478 throw new IllegalArgumentException("User is not connected"); 479 } 480 481 Map<String, Object> userInfo = _cache.get(userIdentity); 482 if (userInfo.get("openTickets") != null) 483 { 484 return (List<GlpiTicket>) userInfo.get("openTickets"); 485 } 486 else 487 { 488 return Collections.EMPTY_LIST; 489 //throw new IllegalArgumentException("Unable to get open tickets. No user matches in GPLI with login " + userIdentity.getLogin()); 490 } 491 } 492 493 /** 494 * Get the i18n label of a GLPI status 495 * @param status The GLPI status 496 * @return the label of status as {@link I18nizableText} 497 */ 498 public I18nizableText getGlpiStatusLabel(int status) 499 { 500 if (_glpiStatus == null) 501 { 502 _glpiStatus = new HashMap<>(); 503 _glpiStatus.put(1, new I18nizableText("plugin." + _pluginName, "PLUGINS_GLPI_INCOMMING_STATUS")); 504 _glpiStatus.put(2, new I18nizableText("plugin." + _pluginName, "PLUGINS_GLPI_ASSIGNED_STATUS")); 505 _glpiStatus.put(3, new I18nizableText("plugin." + _pluginName, "PLUGINS_GLPI_PLANNED_STATUS")); 506 _glpiStatus.put(4, new I18nizableText("plugin." + _pluginName, "PLUGINS_GLPI_WAITING_STATUS")); 507 _glpiStatus.put(5, new I18nizableText("plugin." + _pluginName, "PLUGINS_GLPI_SOLVED_STATUS")); 508 _glpiStatus.put(6, new I18nizableText("plugin." + _pluginName, "PLUGINS_GLPI_CLOSED_STATUS")); 509 510 } 511 return _glpiStatus.get(new Integer(status)); 512 } 513 514 /** 515 * Get the i18n label of a GLPI ticket type 516 * @param type The GLPI type 517 * @return the label of status as {@link I18nizableText} 518 */ 519 public I18nizableText getGlpiTypeLabel(int type) 520 { 521 if (_glpiType == null) 522 { 523 _glpiType = new HashMap<>(); 524 _glpiType.put(1, new I18nizableText("plugin." + _pluginName, "PLUGINS_GLPI_INCIDENT_TYPE")); 525 _glpiType.put(2, new I18nizableText("plugin." + _pluginName, "PLUGINS_GLPI_DEMAND_TYPE")); 526 527 } 528 return _glpiType.get(new Integer(type)); 529 } 530 531 /** 532 * The Glpi cache loader. 533 */ 534 protected class GlpiCacheLoader extends CacheLoader<UserIdentity, Map<String, Object>> 535 { 536 @Override 537 public Map<String, Object> load(UserIdentity userIdentity) throws Exception 538 { 539 return loadUserInfo(userIdentity); 540 } 541 } 542}