001: /*
002: * SalomeTMF is a Test Management Framework
003: * Copyright (C) 2005 France Telecom R&D
004: *
005: * This library is free software; you can redistribute it and/or
006: * modify it under the terms of the GNU Lesser General Public
007: * License as published by the Free Software Foundation; either
008: * version 2 of the License, or (at your option) any later version.
009: *
010: * This library is distributed in the hope that it will be useful,
011: * but WITHOUT ANY WARRANTY; without even the implied warranty of
012: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013: * Lesser General Public License for more details.
014: *
015: * You should have received a copy of the GNU Lesser General Public
016: * License along with this library; if not, write to the Free Software
017: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
018: *
019: * @author Fayçal SOUGRATI, Vincent Pautret, Marche Mikael
020: *
021: * Contact: mikael.marche@rd.francetelecom.com
022: */
023:
024: package org.objectweb.salome_tmf.ihm.tools;
025:
026: import java.awt.Frame;
027: import java.awt.Image;
028: import java.awt.MediaTracker;
029: import java.awt.Toolkit;
030: import java.io.BufferedInputStream;
031: import java.io.BufferedOutputStream;
032: import java.io.ByteArrayOutputStream;
033: import java.io.File;
034: import java.io.FileOutputStream;
035: import java.io.IOException;
036: import java.io.InputStream;
037: import java.net.MalformedURLException;
038: import java.net.URL;
039: import java.util.ArrayList;
040: import java.util.HashSet;
041: import java.util.Hashtable;
042: import java.util.Iterator;
043: import java.util.Set;
044: import java.util.StringTokenizer;
045: import java.util.Vector;
046:
047: import javax.swing.ImageIcon;
048: import javax.swing.JDialog;
049: import javax.swing.JFrame;
050: import javax.swing.JOptionPane;
051:
052: import org.objectweb.salome_tmf.api.Api;
053: import org.objectweb.salome_tmf.api.ApiConstants;
054: import org.objectweb.salome_tmf.api.Util;
055: import org.objectweb.salome_tmf.api.data.DataUpToDateException;
056: import org.objectweb.salome_tmf.api.sql.LockException;
057: import org.objectweb.salome_tmf.data.AutomaticTest;
058: import org.objectweb.salome_tmf.data.Campaign;
059: import org.objectweb.salome_tmf.data.DataConstants;
060: import org.objectweb.salome_tmf.data.DataSet;
061: import org.objectweb.salome_tmf.data.Environment;
062: import org.objectweb.salome_tmf.data.Execution;
063: import org.objectweb.salome_tmf.data.ExecutionResult;
064: import org.objectweb.salome_tmf.data.ExecutionTestResult;
065: import org.objectweb.salome_tmf.data.Family;
066: import org.objectweb.salome_tmf.data.ManualTest;
067: import org.objectweb.salome_tmf.data.Parameter;
068: import org.objectweb.salome_tmf.data.Test;
069: import org.objectweb.salome_tmf.data.TestList;
070: import org.objectweb.salome_tmf.data.User;
071: import org.objectweb.salome_tmf.ihm.IHMConstants;
072: import org.objectweb.salome_tmf.ihm.languages.Language;
073: import org.objectweb.salome_tmf.ihm.main.SalomeLocksPanel;
074: import org.objectweb.salome_tmf.ihm.main.SalomeTMFContext;
075: import org.objectweb.salome_tmf.ihm.main.datawrapper.DataModel;
076:
077: /**
078: * Classe regroupant des m?thodes utilitaires
079: */
080: public class Tools implements ApiConstants, IHMConstants {
081:
082: /**
083: * Méthode permettant de récupérer des icones qui peuvent ?tre utilis?es
084: * dans l'interface.
085: * @param path le chemin complet permettant d'acc?der au fichier image.
086: * @param description la chaine de caractères pouvant être associée à l'icone
087: * @return un objet <code>ImageIcon</code>
088: */
089: public static ImageIcon createAppletImageIcon(String path,
090: String description) {
091: int MAX_IMAGE_SIZE = 10000;
092: int count = 0;
093:
094: BufferedInputStream imgStream = new BufferedInputStream(
095: Tools.class.getResourceAsStream(path));
096: if (imgStream != null) {
097: byte buf[] = new byte[MAX_IMAGE_SIZE];
098: try {
099: count = imgStream.read(buf);
100: } catch (IOException ieo) {
101: System.err
102: .println(Language
103: .getInstance()
104: .getText(
105: "Impossible_de_lire_le_flux_depuis_le_fichier_:_")
106: + path);
107: }
108:
109: try {
110: imgStream.close();
111: } catch (IOException ieo) {
112: System.err.println(Language.getInstance().getText(
113: "Impossible_de_fermer_le_fichier_")
114: + path);
115: }
116:
117: if (count <= 0) {
118: System.err.println(Language.getInstance().getText(
119: "Le_fichier_est_vide_:_")
120: + path);
121: return null;
122: }
123:
124: return new ImageIcon(Toolkit.getDefaultToolkit()
125: .createImage(buf), description);
126: } else {
127: System.err.println(Language.getInstance().getText(
128: "Impossible_de_trouver_le_fichier_:_")
129: + path);
130: return null;
131: }
132: } // Fin de la m?thode createAppletImageIcon/2
133:
134: /**
135: * M?thode permettant de r?cup?rer des icones qui peuvent ?tre utilis?es
136: * dans l'interface.
137: * @param path le chemin complet permettant d'acc?der au fichier image.
138: * @param description la cha?ne de caract?res pouvant ?tre associ?e ? l'icone
139: * @return un objet <code>ImageIcon</code>
140: */
141: public static Image createImage(String path, String description) {
142: int IMAGE_BUF = 2048;
143: int count = 0;
144: int pos = 0;
145: Image pImage = null;
146: ByteArrayOutputStream pBufer = new ByteArrayOutputStream(
147: IMAGE_BUF);
148: byte buf[] = new byte[IMAGE_BUF];
149:
150: BufferedInputStream imgStream = new BufferedInputStream(
151: Tools.class.getResourceAsStream(path));
152: if (imgStream != null) {
153: try {
154: while ((count = imgStream.read(buf, 0, IMAGE_BUF)) != -1) {
155: pBufer.write(buf, 0, count);
156: pos += count;
157: }
158: if (pos != 0) {
159: imgStream.close();
160: pImage = Toolkit.getDefaultToolkit().createImage(
161: pBufer.toByteArray());
162: pBufer.close();
163: }
164: } catch (IOException ieo) {
165: System.err
166: .println(Language
167: .getInstance()
168: .getText(
169: "Impossible_de_lire_le_flux_depuis_le_fichier_:_")
170: + path);
171: }
172: } else {
173: System.err.println(Language.getInstance().getText(
174: "Impossible_de_trouver_le_fichier_:_")
175: + path);
176: }
177: return pImage;
178: } // Fin de la m?thode createAppletImageIcon/2
179:
180: /**
181: *
182: * @param name
183: * @param familyList
184: * @return
185: */
186: public static Family familyInList(String name, ArrayList familyList) {
187: for (int i = 0; i < familyList.size(); i++) {
188: if (((Family) familyList.get(i)).getNameFromModel().equals(
189: name)) {
190: return (Family) familyList.get(i);
191: }
192: }
193: return null;
194: } // Fin de la m?thode familyInList/2
195:
196: /**
197: *
198: * @param name
199: * @param testListList
200: * @return
201: */
202: public static TestList testListInList(String name,
203: ArrayList testListList) {
204: for (int i = 0; i < testListList.size(); i++) {
205: if (((TestList) testListList.get(i)).getNameFromModel()
206: .equals(name)) {
207: return (TestList) testListList.get(i);
208: }
209: }
210: return null;
211: } // Fin de la m?thode familyInList/2
212:
213: /**
214: *
215: * @param name
216: * @param testList
217: * @return
218: */
219: public static Test testInList(Test test, ArrayList testList) {
220: for (int i = 0; i < testList.size(); i++) {
221: if (((Test) testList.get(i)).getNameFromModel().equals(
222: test.getNameFromModel())
223: && ((Test) testList.get(i)).getTestListFromModel()
224: .getNameFromModel().equals(
225: test.getTestListFromModel()
226: .getNameFromModel())
227: && ((Test) testList.get(i)).getTestListFromModel()
228: .getFamilyFromModel().getNameFromModel()
229: .equals(
230: (test.getTestListFromModel()
231: .getFamilyFromModel()
232: .getNameFromModel()))) {
233: return (Test) testList.get(i);
234: }
235: }
236: return null;
237: } // Fin de la m?thode familyInList/2
238:
239: /**
240: * Transforme le texte pass? en param?tre en un texte au format html d?coup?
241: * en ligne dont la longueur est pass?e en param?tre
242: * @param text le texte
243: * @param lineSize la longueur des lignes
244: * @return un texte au format html
245: */
246: public static String createHtmlString(String text, int lineSize) {
247: String begin = "<html>";
248: String end = "</html>";
249: String beginCut = "<p>";
250: String endCut = "</p>";
251:
252: String resul = begin;
253: int i = 0;
254: int j = lineSize;
255: while (j <= text.length()) {
256: resul = resul + beginCut + text.substring(i, j) + endCut;
257: i = j;
258: j = j + lineSize;
259: }
260: if (i != text.length()) {
261: resul = resul + beginCut + text.substring(i, text.length())
262: + endCut;
263: }
264: return resul + end;
265: } // Fin de la m?thode createHtmlString/2
266:
267: public static String createStringWithReturn(String text,
268: int lineSize) {
269:
270: text.replaceAll("[\n]", "");
271:
272: String resul = "";
273: int i = 0;
274: int j = lineSize;
275: while (j <= text.length()) {
276: resul = resul + "\n" + text.substring(i, j);
277: i = j;
278: j = j + lineSize;
279: }
280: if (i != text.length()) {
281: resul = resul + "\n" + text.substring(i, text.length());
282: }
283:
284: return resul;
285:
286: }
287:
288: /**
289: * Retourne l'icone associ?e au type pass? au param?tre. On retourne <code>null</code>
290: * si le type n'est pas SUCCESS, FAIL ou UNKNOW.
291: * @param type
292: * @return
293: */
294: public static ImageIcon getActionStatusIcon(String type) {
295: if (type == null)
296: return null;
297: if (type.equals(SUCCESS)) {
298: return Tools
299: .createAppletImageIcon(PATH_TO_SUCCESS_ICON, "");
300: } else if (type.equals(FAIL)) {
301: return Tools.createAppletImageIcon(PATH_TO_FAIL_ICON, "");
302: } else if (type.equals(UNKNOWN)) {
303: return Tools.createAppletImageIcon(PATH_TO_UNKNOW_ICON, "");
304: }
305: return null;
306: } // Fin de la m?thode getActionStatusIcon/1
307:
308: /**
309: *
310: * @param url
311: * @return
312: */
313: public static String[] chooseProjectAndUser(URL url) {
314: String[] result = { "", "" };
315: String urlString = url.toString();
316: String[] tab = urlString.split("[?=]");
317: Util.log("[Tools.chooseProjectAndUser()] tab.length = "
318: + tab.length);
319: for (int i = 0; i < tab.length; i++)
320: Util.log("[Tools.chooseProjectAndUser()] tab[" + i + "] = "
321: + tab[i]);
322: if (tab.length == 3) {
323: int idConnection = Integer.parseInt(tab[2]);
324: Util
325: .log("[Tools.chooseProjectAndUser()] set ID Connection = "
326: + idConnection);
327: result[0] = Api.getConnectionProject(idConnection);
328: Util
329: .log("[Tools.chooseProjectAndUser()] Connection with project = "
330: + result[0]);
331: result[1] = Api.getConnectionUser(idConnection);
332: Util
333: .log("[Tools.chooseProjectAndUser()] Connection with user = "
334: + result[1]);
335: }
336: return result;
337: }
338:
339: /**
340: *
341: * @param descr
342: * @return
343: */
344: public static ArrayList getParametersInDescription(String descr) {
345: ArrayList result = new ArrayList();
346: StringTokenizer tokenizer = new StringTokenizer(descr);
347: while (tokenizer.hasMoreTokens()) {
348: String element = tokenizer.nextToken();
349: if (element.startsWith("$")
350: && DataModel.getCurrentTest()
351: .hasUsedParameterNameFromModel(
352: element.substring(1, (element
353: .length() - 1)))
354: && element.endsWith("$")) {
355: result
356: .add(element.substring(1,
357: (element.length() - 1)));
358: }
359: }
360: return result;
361:
362: }
363:
364: public static ArrayList getParametersInDescription(String descr,
365: Hashtable paramTable) {
366: HashSet paramSet = new HashSet(paramTable.values()); //ADD - Hashtable2HashSet
367: ArrayList result = new ArrayList();
368: for (Iterator iter = paramSet.iterator(); iter.hasNext();) {
369: Parameter param = (Parameter) iter.next();
370: if (containsString(descr, param.getNameFromModel()))
371: result.add(param);
372: }
373: return result;
374: }
375:
376: public static boolean containsString(String descr, String paramName) {
377: char[] tabOfChar = descr.toCharArray();
378: char[] paramCharTab = paramName.toCharArray();
379: int i = 0;
380: boolean find = false;
381: while (i < tabOfChar.length) {
382: System.out.print(tabOfChar[i]);
383: if (tabOfChar[i] == '$') {
384: i++;
385: int oldValue = i;
386: int j = 0;
387: while (j < paramCharTab.length && i < tabOfChar.length
388: && paramCharTab[j] == tabOfChar[i]) {
389: j++;
390: i++;
391: }
392: if (j == paramCharTab.length && i < tabOfChar.length
393: && tabOfChar[i] == '$') {
394: find = true;
395: break;
396: } else {
397: i = oldValue - 1;
398: System.out.println();
399: }
400: }
401: i++;
402: }
403: System.out.println();
404: return find;
405: }
406:
407: /**
408: *
409: * @param description
410: * @param dataSet
411: * @param env
412: * @return
413: */
414: //public static String getInstantiedDescription(String description, DataSet dataSet) {
415: public static String getInstantiedDescription(String description,
416: Execution pExec) {
417: String result = description;
418: DataSet dataSet = pExec.getDataSetFromModel();
419: Environment ptrEnv = pExec.getEnvironmentFromModel();
420: if (dataSet != null) {
421: Set dataSetKeysSet = dataSet
422: .getParametersHashMapFromModel().keySet();
423: for (Iterator iter = dataSetKeysSet.iterator(); iter
424: .hasNext();) {
425: String paramName = (String) iter.next();
426: Parameter element = DataModel.getCurrentProject()
427: .getParameterFromModel(paramName);
428: String value = dataSet
429: .getParameterValueFromModel(element
430: .getNameFromModel());
431: if (value
432: .startsWith(DataConstants.PARAM_VALUE_FROM_ENV)) {
433: value = ptrEnv.getParameterValue(paramName);
434: if (value == null) {
435: value = "";
436: }
437: }
438: try {
439: result = result
440: .replaceAll("[$]"
441: + element.getNameFromModel()
442: + "[$]", value);
443: } catch (Exception e) {
444: }
445: }
446: }
447: return result;
448: } // Fin de la m?thode getInstantiedDescription/3
449:
450: /**
451: *
452: * @param description
453: * @param paramName
454: * @param test
455: * @return
456: */
457: public static String clearStringOfParameter(String description,
458: String paramName) {
459: String result = description;
460: result = result.replaceAll("[$]" + paramName + "[$]", "");
461: return result;
462: } // Fin de la m?thode clearStringOfParameter/3
463:
464: /**
465: *
466: * @param tsfVector
467: * @param tsf
468: * @return
469: */
470: /*public static boolean containsTSF(Vector tsfVector, TestSuiteFamily tsf) {
471: for (int i = 0; i < tsfVector.size(); i++) {
472: TestSuiteFamily tsfOfVector = (TestSuiteFamily)tsfVector.get(i);
473: if (tsfOfVector.getFamilyName().equals(tsf.getFamilyName()) &&
474: tsfOfVector.getTestName().equals(tsf.getTestName()) &&
475: tsfOfVector.getSuiteName().equals(tsf.getSuiteName())) {
476: return true;
477: }
478: }
479: return false;
480: }*/
481:
482: public static Image loadImages(JFrame frm, String imageFile) {
483: try {
484: MediaTracker mTrack = new MediaTracker(frm); // load les image avant de les afficher
485: Image image = createImage(PATH_TO_SALOME_INTRO_ICON, "");
486: mTrack.addImage(image, 0);
487: mTrack.waitForAll();
488: return image;
489: } catch (Exception e) {
490: System.out.println(" getimages : " + e);
491: }
492: return null;
493: }
494:
495: public static URL getURL(String file) throws MalformedURLException {
496: URL documentBase = new URL("file:///"
497: + System.getProperty("user.dir") + "/");
498: return new URL(documentBase, file);
499: }
500:
501: /**
502: * Méthodes qui retourne une liste de liste contenant des tests soit manuels
503: * soit automatiques. L'ordre est préservé par rapport à la liste donnée en
504: * paramètre.
505: * @param list une liste de tests quelconques
506: * @param une liste de liste de tests, regroupés selon leur type (manuel ou
507: * autmatique)
508: */
509: public static ArrayList getListOfTestManualOrAutomatic(
510: ArrayList list, ExecutionResult pExecutionResult,
511: Vector pAllTestToExecute, boolean continueExec,
512: boolean onlyNotExec, boolean onlyAssigned, boolean forModif) {
513: ArrayList result = new ArrayList();
514: ArrayList tempList = new ArrayList();
515: boolean manual = false;
516: if (list.get(0) instanceof ManualTest) {
517: manual = true;
518: } else {
519: manual = false;
520: }
521: for (int i = 0; i < list.size(); i++) {
522: boolean add = true;
523: if (pExecutionResult != null) {
524: Test pTest = (Test) list.get(i);
525: ExecutionTestResult pExecutionTestResult = pExecutionResult
526: .getExecutionTestResultFromModel(pTest);
527: if (pExecutionTestResult != null) {
528: String status = pExecutionTestResult
529: .getStatusFromModel();
530:
531: //System.out.println("Previous status for "+pTest.getNameFromModel()+" is : " + status);
532: if (status != null && !status.equals("")
533: && continueExec && onlyNotExec) {
534: add = false;
535: //System.out.println("Not add test "+ pTest.getNameFromModel()+" with : " + status);
536: } else if (onlyAssigned) {
537: Campaign pCamp = pExecutionResult
538: .getExecution().getCampagneFromModel();
539: User currentUser = DataModel.getCurrentUser();
540: if (currentUser.getIdBdd() != pCamp
541: .getAssignedUserID(pTest)) {
542: add = false;
543: //System.out.println("Not add test "+pTest.getNameFromModel()+", not assigned to user : " + currentUser.getLoginFromModel());
544: }
545: } else {
546: //System.out.println("Add test "+ pTest.getNameFromModel()+" with : " + status);
547: }
548: }
549: if (forModif && pTest instanceof AutomaticTest) { //Pas possible de Modifier des résultats automatique
550: add = false;
551: }
552: }
553: if (add) {
554: pAllTestToExecute.add(list.get(i));
555: if (list.get(i) instanceof ManualTest) {
556: if (!manual) {
557: if (tempList.size() > 0) {
558: result.add(tempList);
559: tempList = new ArrayList();
560: }
561: }
562: manual = true;
563: tempList.add(list.get(i));
564: } else if (list.get(i) instanceof AutomaticTest) {
565: if (manual) {
566: if (tempList.size() > 0) {
567: result.add(tempList);
568: tempList = new ArrayList();
569: }
570: }
571: manual = false;
572: tempList.add(list.get(i));
573: }
574: }
575: }
576: if (tempList.size() != 0) {
577: result.add(tempList);
578: }
579: return result;
580: }
581:
582: /**
583: *
584: * @param testList
585: */
586: public static void initExecutionResultMap(ArrayList testList,
587: ExecutionResult execResult, Campaign c) {
588: for (int i = 0; i < testList.size(); i++) {
589: execResult.initTestResultStatusInModel((Test) testList
590: .get(i), "", i, c);
591: }
592: } // Fin de la m?thode initMap/1
593:
594: /**
595: *
596: * @param str
597: * @return
598: */
599: public static String speedpurge(String str) {
600: String res_str;
601: byte[] str_b = str.getBytes();
602: int taille = str_b.length;
603: byte[] res = new byte[taille];
604: int j = 0;
605: for (int i = 0; i < taille; i++) {
606: if (str_b[i] != 92) {
607: res[j] = str_b[i];
608: j++;
609: } else {
610: res[j] = 47;
611: j++;
612: }
613:
614: }
615: res_str = new String(res, 0, j);
616: return res_str;
617:
618: }
619:
620: /**
621: * M?thode d'affichage d'un message sous forme de fen?tre lorsqu'une
622: * exception est lev?e
623: * @param message un message ? afficher
624: */
625: public static void ihmExceptionView(Exception exception) {
626: if (Api.isDEBUG()) {
627: exception.printStackTrace();
628: }
629: if (exception instanceof DataUpToDateException) {
630: SalomeTMFContext.getInstance().showMessage(
631: Language.getInstance().getText("Update_data"),
632: Language.getInstance().getText("Erreur_!"),
633: JOptionPane.ERROR_MESSAGE);
634: } else if (exception instanceof LockException) {
635: LockException lockException = (LockException) exception;
636: if (lockException.code == LockException.DOLOCK) {
637: int admin_id_bdd = -1;
638: try {
639: admin_id_bdd = DataModel.getCurrentProject()
640: .getAdministratorWrapperFromDB().getIdBDD();
641: } catch (Exception e) {
642: e.printStackTrace();
643: }
644: if (admin_id_bdd == DataModel.getCurrentUser()
645: .getIdBdd()) {
646: Object[] options = {
647: Language.getInstance().getText("Oui"),
648: Language.getInstance().getText("Non") };
649: int choice = -1;
650: //int actionCase = -1;
651: choice = JOptionPane.showOptionDialog(new Frame(),
652: Language.getInstance().getText(
653: "Lock_Problem_User_Is_Admin"),
654: Language.getInstance().getText(
655: "Attention_!"),
656: JOptionPane.YES_NO_OPTION,
657: JOptionPane.QUESTION_MESSAGE, null,
658: options, options[1]);
659: if (choice == JOptionPane.YES_OPTION) {
660: JDialog dialog = new JDialog(SalomeTMFContext
661: .getInstance().getSalomeFrame(), true);
662: SalomeLocksPanel locksPanel = new SalomeLocksPanel(
663: DataModel.getCurrentProject()
664: .getIdBdd());
665: locksPanel.loadDataFromDB();
666: dialog.getContentPane().add(locksPanel);
667: dialog.setTitle("Salomé locks");
668: dialog.pack();
669: dialog.setLocation(400, 300);
670: dialog.setVisible(true);
671: }
672: } else {
673: JOptionPane.showMessageDialog(new Frame(), Language
674: .getInstance().getText(
675: "Lock_Problem_User_IsNot_Admin"),
676: Language.getInstance().getText(
677: "Information_!"),
678: JOptionPane.INFORMATION_MESSAGE);
679: }
680: }
681: } else {
682: JOptionPane.showMessageDialog(new Frame(), Language
683: .getInstance().getText("Problème_inconnu_:_\n")
684: + exception.toString(), Language.getInstance()
685: .getText("Information_!"),
686: JOptionPane.INFORMATION_MESSAGE);
687: }
688: }
689:
690: public static void RefreshView(String name, String type) {
691: JOptionPane
692: .showMessageDialog(
693: new Frame(),
694: Language.getInstance().getText(
695: "Les_données_de_")
696: + name
697: + "["
698: + type
699: + Language
700: .getInstance()
701: .getText(
702: "]_ne_sont_pas_à_jour,_veuillez_faire_un_rafraichir_avant_toute_autre_commande\n"),
703: Language.getInstance().getText("Information_!"),
704: JOptionPane.INFORMATION_MESSAGE);
705: }
706:
707: public static void writeFile(InputStream input, String path) {
708: try {
709: // directory creation if it does not exist
710: File destFile = new File(path);
711: if (!destFile.getParentFile().exists())
712: destFile.getParentFile().mkdir();
713:
714: // Flux d'?criture sur le fichier
715: FileOutputStream fos = new FileOutputStream(path);
716: BufferedOutputStream bos = new BufferedOutputStream(fos);
717: BufferedInputStream bis = new BufferedInputStream(input);
718:
719: // Copie du flux dans le fichier
720: int car = bis.read();
721: while (car > -1) {
722: bos.write(car);
723: car = bis.read();
724: }
725: // Fermeture des flux de donn?es
726: bos.flush();
727: bos.close();
728: bis.close();
729: } catch (Exception e) {
730: e.printStackTrace();
731: }
732: }
733: } // Fin de la classe Tools
|