Csf Usage Guide

  • November 2019
  • PDF

This document was uploaded by user and they confirmed that they have the permission to share it. If you are author or own the copyright of this book, please report to us by using this DMCA report form. Report DMCA


Overview

Download & View Csf Usage Guide as PDF for free.

More details

  • Words: 2,526
  • Pages: 15
2

Configure the Environment

2.1 Set up the Environment The common service framework uses one property file, arch.properties, to configure the basic configurations. Developers need to create a new system variable to point to the property file. 2.1.1

Windows: 1) Create Directory for common service: i.e. D:/CommonService D:/CommonService/log D:/CommonService/log/MessageHandling D:/CommonService/log/Notification D:/CommonService/log/DataAccess 2) Extract all jars from Install/lib to the D:/CommonService folder: activation.jar classes12.jar stc.jms.stcjms.jar CommonServices.jar egate.jar fscontext.jar jms.jar mail.jar xdb.jar SNMP4J-agent.jar SNMP4J.jar 3) Copy arch.properties file to D:\CommonService 4) Go to My Computer -> Properties -> Advanced -> Environment Variables

Note: 1. Be sure to use “/” instead of the regular “\” for path. 2. No environment variable should contain “/u”, it will cause failure at reading property.

2.2 Create Database Tables The common service framework allows developers and business developers to dynamically change the message information, notification contacts…etc via database tables. Developers can use the scripts to create the tables and populate basic data.



EAICS_Create_User.sql – create a new user on the database instance for common services.



MessageHandling.sql – create all tables for message handling service.



Notification.sql – create all tables for notification service.



Parameterization.sql – create all tables for parameterization service.



MessageHandlingDefaultData.sql – populates message handling tables with a basic set of data.



NotificationDefaultData.sql – populates notification tables with a basic set of data and creates common tables.



ParameterizationDefaultData.sql – populates sample parameterization data entries.



Resubmit.sql – create all tables for resubmit service.



EHW.sql – create all tables and sequence objects for EHW gui

Note: Developers should NOT rely solely on the basic data set. Developers should acquire a full understanding of the data structure and customize the data according to the business requirements. (See section 3).

2.3 Set up JNDI Bindings to DataSource The common services utilize JNDI bindings to allow dynamic usage of the datasource. Following steps will set up your data source JNDI bindings. 2.3.1 Windows 1. Move the batch file, bindingCreate.bat, from install/script to the common service dir, eg. D:/CommonService 2. Edit the batch file, example: set JAVA_PATH=D:\JavaCAPS51\edesigner\jdk\bin set CLASSPATH=.\lib\activation.jar;.\lib\classes12.jar;.\lib\com.stc.jms.stcjms.jar;.\lib\egate .jar;.\lib\fscontext.jar;.\lib\jms.jar;.\lib\mail.jar;.\lib\xdb.jar set FILE_CONTEXT=. set SOURCE=archdb set SID=orcl set HOST=localhost set PORT=1521 set USER_NAME=admin set PASSWORD=admin

3. Execute the batch file. A .bindings file is created under the common service folder. The .bindings file looks like the following and you should find information in the highlighted area: #This file is used by the JNDI FSContext. #Tue Aug 07 15:43:02 EDT 2007 user/archdb/RefAddr/2/Encoding=String user/archdb/RefAddr/0/Type=Source jdbc/archdb/RefAddr/4/Content=1521 jdbc/archdb/RefAddr/3/Encoding=String jdbc/archdb/RefAddr/2/Content=orcl

jdbc/archdb/ClassName=oracle.jdbc.pool.OracleConnectionPoolDataSource user/archdb/RefAddr/1/Type=UserName jdbc/archdb/RefAddr/0/Type=driverType jdbc/archdb/RefAddr/1/Content=localhost user/archdb/RefAddr/0/Encoding=String user/archdb/RefAddr/2/Type=Password jdbc/archdb/RefAddr/1/Encoding=String jdbc/archdb/RefAddr/1/Type=serverName user/archdb/ClassName=com.accenture.eai.cs.DataAccess.DataAccessUser jdbc/archdb/RefAddr/2/Type=databaseName jdbc/archdb/FactoryName=oracle.jdbc.pool.OracleDataSourceFactory user/archdb/RefAddr/2/Content=05EFBAC0269A <- encrypted password* jdbc/archdb/RefAddr/4/Encoding=String jdbc/archdb/RefAddr/3/Type=networkProtocol jdbc/archdb/RefAddr/3/Content=tcp user/archdb/RefAddr/1/Encoding=String jdbc/archdb/RefAddr/0/Content=thin jdbc/archdb/RefAddr/4/Type=portNumber jdbc/archdb/RefAddr/2/Encoding=String jdbc/archdb/RefAddr/0/Encoding=String user/archdb/FactoryName=com.accenture.eai.cs.DataAccess.DataAccessUserFactory user/archdb/RefAddr/1/Content=admin

2.4 Configure the property file There are various sections in the property file. This section highlights the properties that need to configure according to the EAI machine. ##################################################################### #Message Handling Framework Properties ##################################################################### # API Properties for JMS_Host, JMS_port, and JMS queue name MH_EVENT_TOPIC=MH_Queue MH_HOST_NAME=localhost MH_PORT_NUMBER=18007

# Database logical name for message handling MH_DB=archdb

MH_MSG_TOKEN=$ LOGGING_FLAG_FILE

MH_RECEIVER_TIMEOUT=10 NOTIF_RECEIVER_TIMEOUT=10

# Followings are tables for message handling # DO NOT CHANGE IF USE THE DEFAULT SQL SCRIPT # Logging level

MH_LOG_LEVEL_TABLE=mh_logging_level MH_SEVERITY_COLUMN=msg_severity MH_CLSTYPE_COLUMN=cls_type MH_LOG_FLAG_COLUMN=log_level_flag

# Message code MH_MSGCODE_TABLE=mh_msgcode_table MH_MSGCODE_MSGCODE_COLUMN=mh_msgcode_msgcode MH_MSG_COLUMN=mh_msg

# Logging distribution and transport codes MH_DIST_TRANS_CODE_TABLE=mh_dist_trans_code_table MH_DIST_SEVERITY_COLUMN=msg_severity MH_DIST_MSGCODE_COLUMN=msg_code MH_DIST_DISTCODE_COLUMN=dist_code MH_DIST_TRANSCODE_COLUMN=trans_code

# Message Class Type MH_CLSTYPE_CODE_TABLE=mh_clstype_code_table MH_CLSTYPE_CLSTYPECODE_COLUMN=cls_type_code MH_CLSTYPE_CLSTYPE_COLUMN=cls_type

# Message severity MH_SEVERITY_CODE_TABLE=mh_severity_code_table MH_SEVERITY_SEVCODE_COLUMN=mh_severity_sevcode MH_SEVERITY_SEVERITY_COLUMN=mh_severity_severity

##################################################################### # Notification Framework Properties ##################################################################### # CHANGE THE FOLLOWING PROPERTY BASED ON YOUR ENVIRONMENT SETTING NOTIF_HOST_NAME=localhost NOTIF_PORT_NUMBER=18007 NOTIF_QUEUE_NAME=NotifQueue NOTIF_ERROR_LOG_FILE=D:/Mammoth Visteon Assets/Common Service Framework/log/Notification/notification.log NOTIF_BACKUP_FILENAME=D:/Mammoth Visteon Assets/Common Service Framework/log/Notification/notificationQueueLog

# Log file path for Notification failure NOTIF_DEF_LOG_FILE=D:/Mammoth Visteon Assets/Common Service Framework/log/Notification/notificationError.log

# Database logical name for message handling NOTIF_FW_DB=archdb

# SMTP Server mail.smtp.host=vistsmtp.visteon.com

# Default email contact when notification faiures [email protected] NOTIF_EMAIL_DEF_SUBJECT=Notification Failure [email protected] NOTIF_EMAIL_DEF_TO_NAME1=David Wang NOTIF_EMAIL_DEF_TO_ADDRESS2 NOTIF_EMAIL_DEF_TO_NAME2

# Followings are tables for message handling # DO NOT CHANGE IF USE THE DEFAULT SQL SCRIPT NOTIF_TRANS_CODE_TABLE=notif_trans_code NOTIF_TRANS_INT_COLUMN=int_trans_code NOTIF_TRANS_CODE_COLUMN=char_trans_code NOTIF_DISTR_CODE_TABLE=notif_distr_code NOTIF_DISTR_INT_COLUMN=int_distr_code NOTIF_DISTR_CODE_COLUMN=char_distr_code

NOTIF_DISTR_MATRIX_TABLE=notif_distr_matrix NOTIF_MATRIX_TRANS_COLUMN=char_trans_code NOTIF_MATRIX_DISTR_COLUMN=char_distr_code NOTIF_MATRIX_DESTINATION=notif_destination

NOTIF_DATABASES_TABLE=notif_databases NOTIF_DB_DESTINATION=notif_destination NOTIF_DB_NAME=db_name NOTIF_DB_TABLE=db_table NOTIF_DB_COLUMN=db_column

NOTIF_EMAIL_INFO_TABLE=notif_email_group_info NOTIF_EMAIL_INFO_GROUP_NAME=group_name NOTIF_EMAIL_INFO_FROM_ADDRESS=from_address NOTIF_EMAIL_INFO_FROM_NAME=from_name NOTIF_EMAIL_INFO_SUBJECT=subject

NOTIF_EMAIL_TABLE=notif_email_groups NOTIF_EMAIL_GROUP_NAME=group_name NOTIF_EMAIL_TO_ADDRESS=to_address NOTIF_EMAIL_TO_NAME=to_name

NOTIF_DB_TRANS_CODE=DB NOTIF_FF_TRANS_CODE=FF NOTIF_EM_TRANS_CODE=EM NOTIF_PG_TRANS_CODE=PG NOTIF_SNMP_TRANS_CODE=SNMP

NOTIF_ARCH_EXCEPTIONID=exceptionid NOTIF_ARCH_MESSAGECODE=msg_code NOTIF_ARCH_TIMELOGGED=timelogged NOTIF_ARCH_SEVERITY=severity NOTIF_ARCH_CLASSTYPE=classtype NOTIF_ARCH_PROJECTPATH=projectpath NOTIF_ARCH_COLLABORATION=collaboration NOTIF_ARCH_DESCRIPTION=description NOTIF_ARCH_PAYLOAD=payload NOTIF_ARCH_RESUB_STATUS=resubstatus NOTIF_ARCH_LAST_MODIFIED_BY=lastmodifiedby NOTIF_ARCH_LAST_MODIFIED_DATE=lastmodifieddate

NOTIF_PROPERTY_TABLE=message_property NOTIF_PROPERTY_EXCEPTIONID=exceptionid NOTIF_PROPERTY_KEY=key NOTIF_PROPERTY_VALUE=value

NOTIF_PROPERTY_TABLE=message_property NOTIF_PROPERTY_EXCEPTIONID=exceptionid NOTIF_PROPERTY_KEY=key NOTIF_PROPERTY_VALUE=value

# ExceptionID Sequenece Name NOTIF_EID_SEQUENCE_NAME=exceptionid_seq

##################################################################### # Resubmit Framework Properties ##################################################################### RESUB_MES_TABLE=resubmit_message RESUB_MES_MESSAGECODE=msg_code RESUB_MES_COLLABORATION=collaboration RESUB_MES_DESTINATION=destination_id RESUB_MES_TYPE=resub_type

RESUB_DEST_TABLE=resubmit_destinations RESUB_DEST_ID=destination_id RESUB_DEST_NAME=destination_name RESUB_DEST_HOST=hostname RESUB_DEST_PORT=port RESUB_DEST_TYPE=destination_type

RESUB_ERROR_LOG_FILE=D:/Mammoth Visteon Assets/Common Service Framework/log/ResubmitHandler/resubmit.log

# Default Resubmit Destination ID Name RESUB_DEFAULT_DEST_ID=Dest1

##################################################################### # Data Access Framework Properties ##################################################################### #CHANGE THE FOLLOWING PROPERTY BASED ON YOUR ENVIRONMENT SETTING DATA_ACCESS_LOG = D:/Mammoth Visteon Assets/Common Service Framework/log/DataAccess/data_acess.log #REMOTE Database FILE_CONTEXT = D:/Mammoth Visteon Assets/Common Service Framework #LOCAL Database #FILE_CONTEXT = D:/Mammoth Visteon Assets/Common Service Framework ##################################################################### # Code Decode Properties ##################################################################### CODEDECODE_DATASOURCE_NAME=archdb

##################################################################### # Parametization Properties ##################################################################### PARAM_DATABASE_NAME=archdb

Note: 1) Make sure the directory exists before using the service. 2) All log files is set to rollover when reaches 10MB.

4

Invoking the Service This section shows some essential examples on how to invoke the common services APIs.

4.1

Message Handling/Notification:

1. Import project Mammoth_JAR project to your Java CAPS. 2. Import all the jars under the project to the JCDs that will use common services. 3. There are three different conditions to use the service: •

Scenario 1: When user will not send the payload to messgaHandling framework:

[0] import com.accenture.eai.cs.MessageHandling.*; … [1] MessageHandling mh = MessageHandling.getInstance(); [2] int msgCode = 0004; int severity = 4; int classType = 1; String eventType = “testEvent”; String collabName = collabContext.getCollaborationName(); String projectPath = collabContext.getProjectPath(); String[] msgParameter = {"TestDriver","testMessage","testStackTrace"}; [3] if (!mh.isInitialize()) { if (!mh.initialize()) { logger.error( "Failed to initialize message handler" ); } else { logger.info( "Messagehandler is initialized successfully" ); } } [4] mh.messageHandler(msgCode,severity,classType, collabName, projectPath, msgParameter); [0] import the library [1] Create a MessageHandling instance using the static method getInstance(). [2] Create all necessary parameters. [3] Initialize the logging level of message handling logs if it’s not already initialized. [4] Invoke the method MessageHandler by providing the necessary parameters. Based on the logging level defined in the Database, the message will either be dropped or sent to the distribution groups via writing to DB, flat file or Send emails.

10/15/2008

PRIVATE/PROPRIETARY Contains Private and/or Proprietary information. May not be disclosed outside of Accenture except by a written agreement. Copyright © 2007 Accenture

Page 8



Scenario 2: When user will send just the payload along with other parameters to messgaHandling framework:

[0] import com.accenture.eai.cs.MessageHandling.*; … [1] MessageHandling mh = MessageHandling.getInstance(); [2] int msgCode = 0004; int severity = 4; int classType = 1; String eventType = “testEvent”; String collabName = collabContext.getCollaborationName(); String projectPath = collabContext.getProjectPath(); String payload = message.getTextMessage(); String[] msgParameter = {"TestDriver","testMessage","testStackTrace", payload}; [3] if (!mh.isInitialize()) { if (!mh.initialize()) { logger.error( "Failed to initialize message handler" ); } else { logger.info( "Messagehandler is initialized successfully" ); } } [4] mh.messageHandler(msgCode,severity,classType, collabName, projectPath, msgParameter); [0] import the library [1] Create a MessageHandling instance using the static method getInstance(). [2] Get the payload in String variable, add it into the String array parameter [3] Create all necessary parameters. [4] Initialize the logging level of message handling logs if it’s not already initialized. [5] Invoke the method MessageHandler by providing the necessary parameters. Based on the logging level defined in the Database, the message will either be dropped or sent to the distribution groups via writing to DB, flat file or Send emails. •

Scenario 3: When user will send the payload and message property along with other parameters to messgaHandling framework:

[0] import com.accenture.eai.cs.MessageHandling.*; … [1] MessageHandling mh = MessageHandling.getInstance(); [2] int msgCode = 0004; 10/15/2008

PRIVATE/PROPRIETARY Contains Private and/or Proprietary information. May not be disclosed outside of Accenture except by a written agreement. Copyright © 2007 Accenture

Page 9

int severity = 4; int classType = 1; String eventType = “testEvent”; String collabName = collabContext.getCollaborationName(); String projectPath = collabContext.getProjectPath(); String payload = message.getTextMessage(); String[] msgParameter = {"TestDriver","testMessage","testStackTrace", payload}; HashMap hm = new HashMap(); hm.put(“UserProperty”, input.retrieveUserProperty(UserProperty)); [3] if (!mh.isInitialize()) { if (!mh.initialize()) { logger.error( "Failed to initialize message handler" ); } else { logger.info( "Messagehandler is initialized successfully" ); } } [4] mh.messageHandler(msgCode,severity,classType, collabName, projectPath, msgParameter, hm); [0] import the library [1] Create a MessageHandling instance using the static method getInstance(). [2] Get the payload in String variable, add it into the String array parameter [3] Create HashMap variable and put all user properties in it. [4] Create all necessary parameters. [5] Initialize the logging level of message handling logs if it’s not already initialized. [6] Invoke the method MessageHandler by providing the necessary parameters. Based on the logging level defined in the Database, the message will either be dropped or sent to the distribution groups via writing to DB, flat file or Send emails.

4.2

Code Decode: Before using code/decode, make sure the desired table is created with a primary key. The primary key column will be the code used for getting the decode value. A new binding will have to be created using the NewDataSource class if the table will reside on a different DB instance. (Refer to section 2.3 for creating datasource binding). For the following example, assume the following reference table, STATE_DECODE, is used:

10/15/2008

Abbr

TimeZone

State

TX

Central

Texas

MI

Eastern

Michigan

PRIVATE/PROPRIETARY Contains Private and/or Proprietary information. May not be disclosed outside of Accenture except by a written agreement. Copyright © 2007 Accenture

Page 10

* Primary key is Abbr. 1. Import project Mammoth_JARS project to your Java CAPS. 2. Import all the jars under the project to the JCDs that will use common services. 3. Using the service: [0] import com.accenture.eai.cs.CodeDecode.*; …. [1] String keyColumnName = “Abbreviation”; String tableName = “STATE_DECODE_TABLE”; if (!Decode.isInitialize( tableName )) { if (!Decode.initialize( keyColumnName, tableName )) { logger.error( "Failed to initialize Decode table: " + tableName ); } else { logger.info( "Decode table :" + tableName + " is initialized successfully" ); } } [2] HashMap stateInfo = Decode.getCodeRecord( keyColumnName,”TX”, tableName); [3] String timeZone = stateInfo.get(“TimeZone”).toString(); String stateFullName = stateInfo.get(”State”).toString(); [0]: import the CodeDecode package [1]: Initialize the desired decode table if it’s not already initialized [2]: retrieve the record by providing primaryKeyColumnName, primaryKeyValue, and tableName [3]: access the record (stored in a HashMap) by using get methods

4.3

Parameterization:

1. Import project Mammoth_JARS project to your Java CAPS. 2. Import all the jars under the project to the JCDs that will use common services. 3. Using the service: [0] import com.accenture.eai.cs.Parameterization.*; String key = otdBatchServiceInterface.getBatchServiceHeader().getKey(); [1] Parameterization pm = Parameterization.getInstance(); [2] String paramTable = “PARAM_PUBLISH_TABLE”; if (!pm.isInitialize( paramTable )) { if (!pm.initialize( paramTable )) { logger.error( "Failed to initialize parmeterization table: " + paramTable ); 10/15/2008

PRIVATE/PROPRIETARY Contains Private and/or Proprietary information. May not be disclosed outside of Accenture except by a written agreement. Copyright © 2007 Accenture

Page 11

} else { logger.info( "Parameteriztaion Table : " + paramTable + " is initialized successfully" ); } } [3] HashMap pb = pm.getParam(paramTable,key); [4] String hostname = pb.get(“HOSTNAME”).toString(); String username = pb.get(”USERNAME”).toString(); .... [0]: Import parameterization package [1]: Create parameterization object instance [2]: Initialize /load desired parameterization table data if it’s not already loaded [3]: Use getParam (Strin tableName, String key) to retrieve the record in a HashMap [4]: Access the HashMap to get individual parameters

4.4

Resubmit Handler:

1. Import project Mammoth_JAR project to your Java CAPS. 2. Import all the jars under the project to the JCDs that will use common services. 3. There are two methods of Resubmit framework to start the service: •

First for a single resubmit case

[0] import com.accenture.eai.cs.Resubmit.*; … [1] ResubmitHandler rh = ResubmitHandler.getInstance(); [2] int msgCode = 0004; long exceptionID = 368; String userID = “userID”; [3] rh.resubmitHandler (exceptionID, userID); [0] import the library [1] Create a ResubmitHandler instance using the static method getInstance (). [2] Create all necessary parameters. [3] Invoke the method resubmitHandler by providing the necessary parameters. Based on the jms destination configuration defined in the Database, the message will either be sent to the configured destination or to the default destination. • 10/15/2008

Second for a multiple resubmit case at once PRIVATE/PROPRIETARY Contains Private and/or Proprietary information. May not be disclosed outside of Accenture except by a written agreement. Copyright © 2007 Accenture

Page 12

[0] import com.accenture.eai.cs.Resubmit.*; … [1] ResubmitHandler rh = ResubmitHandler.getInstance(); [2] int msgCode = 0004; long [] exceptionID = {368,412,411,397}; String userID = “userID”; [3] rh.resubmitHandler (exceptionID, userID); [0] import the library [1] Create a ResubmitHandler instance using the static method getInstance (). [2] Create all necessary parameters. [3] Invoke the method resubmitHandler by providing the necessary parameters. Based on the jms destination configuration defined in the Database, the message will either be sent to the configured destination or to the default destination.

5

Best Practices

It is recommended that user access MessageHandling and Notification by using the only public method from MessageHandling class: MessageHandler. First, user should get the instance of the class by invoking the static method, getInstance (): MessageHandling mh = MessageHandling.getInstance (); User now can perform all required message handling by using messageHandler method and provide the appropriate parameters. The signature of messageHandler is: public boolean messageHandler (int msgCode, int msgSeverity, int clsType, String localCollabRuleName, String projectPath, String[] msgParameters) Parameter

Description

Type

Example

msgCode

The numeric code referring to a specific event message

int

“10101”

msgSeverity

The level of severity of the event at which it is thrown. Severity can be Trace, Informational, Warning, or Critical.

int

“4”

Class of team associated to the event. Class can be Architecture, Application, and Database etc.

int

localCollabRuleName

Collaboration rule in which the message was triggered.

String

“crTranslateInventoryOrder”

projectPath

Project path for the collaboration where the message was triggered.

String

“ModelMFT002/MonexMFTPub”

clsType

10/15/2008

In this case 4 would signify “Critical”. 1 In this case 1 would be “Architecture”

PRIVATE/PROPRIETARY Contains Private and/or Proprietary information. May not be disclosed outside of Accenture except by a written agreement. Copyright © 2007 Accenture

Page 13

msgParameters

Message values or value to describe the event

String[]

{“Starting collaboration rule ASMP_Trans”} {InterfaceName, DeveloperName, Timestamp}

Example of Usage: Public Class HelloWorld { Public void Smile() { MessageHandling mh = MessageHandling.getInstance (); String collabName = collabContext.getCollaborationName (); String projectPath = collabContext.getProjectPath (); String myCollab = {collabName}; [1] mh.messageHandler (0001, 1, 1, collabName, projectPath, myCollab); Try { // business logic } catch (DataAccessException e) { // logic handles the exception String[] msgParameters = {e.getClass ().getName(), e.getMessage()}; [2]mh.messageHandler(0004,3,1, collabName, projectPath, msgParameters); } catch (Exception e) { // logic handles the exception String[] msgParameters = {e.getClass().getName(), e.getMessage()}; [3]mh.messageHandler(0004,4,1, collabName, projectPath, msgParameters); } } [1]. A trace message [2]. A warning message [3]. A critical message

10/15/2008

PRIVATE/PROPRIETARY Contains Private and/or Proprietary information. May not be disclosed outside of Accenture except by a written agreement. Copyright © 2007 Accenture

Page 14

6

Troubleshooting

For general debugging, use server log produced by JCAPS. It is under: /logicalhost/is/domains/<domain name>/log/server.log Common Service logs are configured within the property file: For DataAccess: DATA_ACCESS_LOG = D:/CommonService/log/DataAccess/data_acess.log*

For Notification: 1. NOTIF_ERROR_LOG_FILE=D:/CommonService/log/Notification/notification.log* 2. NOTIF_BACKUP_FILENAME= D:/CommonService/log/Notification/notificationLog* 3. NOTIF_DEF_FILE=D:/CommonService/log/Notification/notificationDeliverError.log*

1. If messages failed to reach the Notification Queue, it will be logged in NOTIF_ERROR_LOG_FILE 2. Any notification that failed to reach the destination will be logged in NOTIF_BACKUP_FILENAME 3. Failure of notifications will be logged in NOTIF_DEF_FILE

10/15/2008

PRIVATE/PROPRIETARY Contains Private and/or Proprietary information. May not be disclosed outside of Accenture except by a written agreement. Copyright © 2007 Accenture

Page 15

Related Documents

Csf Usage Guide
November 2019 6
Usage-guide-sp.pdf
June 2020 5
Csf- Design
November 2019 12
Undo Usage
May 2020 7