Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- package com.eisenvault.repo.webscript;
- import java.io.Serializable;
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.Comparator;
- import java.util.Date;
- import java.util.HashMap;
- import java.util.HashSet;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
- import java.util.stream.Collectors;
- import org.alfresco.error.AlfrescoRuntimeException;
- import org.alfresco.model.ContentModel;
- import org.alfresco.repo.model.Repository;
- import org.alfresco.repo.security.authentication.AuthenticationUtil;
- import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
- import org.alfresco.service.cmr.audit.AuditQueryParameters;
- import org.alfresco.service.cmr.audit.AuditService;
- import org.alfresco.service.cmr.audit.AuditService.AuditQueryCallback;
- import org.alfresco.service.cmr.repository.NodeRef;
- import org.alfresco.service.cmr.repository.NodeService;
- import org.alfresco.service.namespace.QName;
- import org.apache.commons.lang.StringUtils;
- import org.apache.commons.logging.Log;
- import org.apache.commons.logging.LogFactory;
- import org.springframework.extensions.webscripts.Cache;
- import org.springframework.extensions.webscripts.DeclarativeWebScript;
- import org.springframework.extensions.webscripts.Status;
- import org.springframework.extensions.webscripts.WebScriptRequest;
- public class NodeAuditTrailWebScript extends DeclarativeWebScript {
- private static Log logger = LogFactory.getLog(NodeAuditTrailWebScript.class);
- /** Audit related */
- private static final String EV_AUDIT_APPLICATION_NAME = "alfresco-access";
- private static final String EV_AUDIT_APPLICATION_NAME_ROOT_PATH = "alfresco-access";
- private static final String EV_AUDIT_NODE_PATH = "/alfresco-access/transaction/node";
- private static final String EV_AUDIT_ACTION_PATH = "/alfresco-access/transaction/action";
- private static final String EV_AUDIT_SUB_ACTIONS_PATH = "/alfresco-access/transaction/sub-actions";
- private static final String EV_AUDIT_USER_PATH = "/alfresco-access/transaction/user";
- private static final String EV_AUDIT_DOC_PATH = "/alfresco-access/transaction/path";
- private static final String EV_AUDIT_TYPE_PATH = "/alfresco-access/transaction/type";
- private static final String EV_AUDIT_VERSION_LABEL_PATH = "/alfresco-access/transaction/cm:versionLabel";
- private static final String EV_AUDIT_COPY_FROM_PATH = "/alfresco-access/transaction/copy/from/path";
- private static final String EV_AUDIT_MOVE_FROM_PATH = "/alfresco-access/transaction/move/from/path";
- private static final String EV_AUDIT_PROP_FROM_NAME_PATH = "/alfresco-access/transaction/properties/from/name";
- private static final String EV_AUDIT_PROP_TO_NAME_PATH = "/alfresco-access/transaction/properties/to/name";
- private static final String EV_AUDIT_PROP_FROM_PATH = "/alfresco-access/transaction/properties/from";
- private static final String EV_AUDIT_PROP_TO_PATH = "/alfresco-access/transaction/properties/to";
- private static final String EV_AUDIT_PROP_ADD_PATH = "/alfresco-access/transaction/properties/add";
- private static final String EV_AUDIT_PROP_DELETE_PATH = "/alfresco-access/transaction/properties/delete";
- private static final String EV_AUDIT_ASPECTS_ADD_PATH = "/alfresco-access/transaction/aspects/add";
- private static final String EV_AUDIT_ASPECTS_DELETE_PATH = "/alfresco-access/transaction/aspects/delete";
- /** Audit actions */
- private static final String EV_AUDIT_ACTION_UPDATE_NODE_PROP = "updateNodeProperties";
- private static final String EV_AUDIT_ACTION_READ = "READ";
- private static final String EV_AUDIT_ACTION_READ_CONTENT = "readContent";
- private static final String EV_AUDIT_ACTION_ADD_NODE_ASPECT = "addNodeAspect";
- private static final String EV_AUDIT_ACTION_DELETE_NODE_ASPECT = "deleteNodeAspect";
- /** Audit actions show */
- private static final String EV_AUDIT_ACTION_UPDATE_NODE_PROP_SHOW = "PROPERTIES UPDATED";
- private static final String EV_AUDIT_ACTION_READ_SHOW = "READ";
- private static final String EV_AUDIT_ACTION_READ_CONTENT_SHOW = "READ CONTENT";
- private static final String EV_AUDIT_ACTION_DOWNLOAD_SHOW = "VIEW IN BROWSER/DOWNLOAD";
- private static final String EV_AUDIT_ACTION_ADD_NODE_ASPECT_SHOW = "ASPECT ADDED";
- private static final String EV_AUDIT_ACTION_DELETE_NODE_ASPECT_SHOW = "ASPECT DELETED";
- /** Services required by class */
- private NodeService nodeService;
- private AuditService auditService;
- private Repository repository;
- /** Services setter method */
- public void setNodeService(NodeService nodeService) {
- this.nodeService = nodeService;
- }
- public void setAuditService(AuditService auditService) {
- this.auditService = auditService;
- }
- public void setRepository(Repository repository) {
- this.repository = repository;
- }
- @Override
- protected Map<String, Object> executeImpl(WebScriptRequest req,
- Status status, Cache cache) {
- Map<String, Object> model = new HashMap<String, Object>();
- try {
- String nodeRefString = req.getParameter("nodeRef");
- if (StringUtils.isBlank(nodeRefString))
- throw new Exception("'nodeRef' missing while processing request...");
- nodeRefString = nodeRefString.replace(":/", "");
- NodeRef nodeRef = repository.findNodeRef("node", nodeRefString.split("/"));
- List<TemplateAuditInfo> auditTrailList = getAuditTrail(nodeRef);
- List<Map<String, Object>> auditList = new ArrayList<Map<String, Object>>(
- auditTrailList.size());
- for (TemplateAuditInfo auditInfo : auditTrailList) {
- if (logger.isDebugEnabled()) {
- logger.debug("\n\n" + auditInfo.toString() + "\n\n");
- }
- Map<String, Object> auditInfoMap = new HashMap<String, Object>();
- auditInfoMap.put("userName", auditInfo.getUserIdentifier());
- auditInfoMap.put("applicationName", auditInfo.getAuditApplication());
- auditInfoMap.put("applicationMethod", auditInfo.getAuditMethod());
- auditInfoMap.put("date", auditInfo.getDate());
- auditInfoMap.put("propertiesUpdate", auditInfo.getUpdatedPropertiesList());
- if(auditInfo.getAuditMethod().equals(EV_AUDIT_ACTION_UPDATE_NODE_PROP_SHOW) && auditInfo.getUpdatedPropertiesList().size() == 0){
- continue;
- }
- auditList.add(auditInfoMap);
- }
- Map<QName, Serializable> props = nodeService.getProperties(nodeRef);
- String fileName = (String) props.get(ContentModel.PROP_NAME);
- model.put("data", auditList);
- model.put("nodeRef", nodeRef.getId());
- model.put("fileName", fileName);
- model.put("count", auditList.size());
- model.put("returnStatus", Boolean.TRUE);
- model.put("statusMessage", "Successfully retrieved audit trail for nodeRef[" + nodeRef + "]");
- } catch (Exception e) {
- logger.warn(e.getMessage());
- model.put("returnStatus", Boolean.FALSE);
- model.put("statusMessage", e.getMessage());
- }
- return model;
- }
- private List<TemplateAuditInfo> getAuditTrail(final NodeRef nodeRef) {
- final List<TemplateAuditInfo> result = new ArrayList<TemplateAuditInfo>();
- final AuditQueryCallback callback = new AuditQueryCallback() {
- public boolean valuesRequired() {
- return true;
- }
- public boolean handleAuditEntryError(Long entryId, String errorMsg,
- Throwable error) {
- throw new AlfrescoRuntimeException(
- "Failed to retrieve audit data.", error);
- }
- public boolean handleAuditEntry(Long entryId,
- String applicationName, String userName, long time,
- Map<String, Serializable> values) {
- TemplateAuditInfo auditInfo = new TemplateAuditInfo(
- applicationName, userName, time, values);
- result.add(auditInfo);
- return true;
- }
- };
- AuthenticationUtil.runAs(new RunAsWork<Object>() {
- public Object doWork() throws Exception {
- AuditQueryParameters pathParams = new AuditQueryParameters();
- pathParams.setApplicationName(EV_AUDIT_APPLICATION_NAME);
- pathParams.addSearchKey(EV_AUDIT_NODE_PATH, nodeRef);
- auditService.auditQuery(callback, pathParams, 1000);
- return null;
- }
- }, AuthenticationUtil.getAdminUserName());
- // sort audit entries by time of generation
- Collections.sort(result, new Comparator<TemplateAuditInfo>() {
- public int compare(TemplateAuditInfo o1, TemplateAuditInfo o2) {
- return o1.getDate().compareTo(o2.getDate());
- }
- });
- return result;
- }
- public class TemplateAuditInfo {
- private String applicationName;
- private String userName;
- private long time;
- private Map<String, Serializable> values;
- public TemplateAuditInfo(String applicationName, String userName,
- long time, Map<String, Serializable> values) {
- this.applicationName = applicationName;
- this.userName = userName;
- this.time = time;
- this.values = values;
- }
- public String getAuditApplication() {
- return this.applicationName;
- }
- public String getUserIdentifier() {
- return this.userName;
- }
- public Date getDate() {
- return new Date(time);
- }
- public String getAuditMethod() {
- if (this.values.get(EV_AUDIT_ACTION_PATH).equals(EV_AUDIT_ACTION_UPDATE_NODE_PROP)){
- return EV_AUDIT_ACTION_UPDATE_NODE_PROP_SHOW;
- } else if (this.values.get(EV_AUDIT_ACTION_PATH).equals(EV_AUDIT_ACTION_READ_CONTENT)) {
- return EV_AUDIT_ACTION_READ_CONTENT_SHOW;
- } else if (this.values.get(EV_AUDIT_ACTION_PATH).equals(EV_AUDIT_ACTION_READ)) {
- // list = [png, pdf, jpg, jpeg]
- // return "READ CONTENT OR VIEW IN BROWSER/DOWNLOAD" in case of list else return VIEW IN BROWSER/DOWNLOAD
- // as there is no difference between READ & READ CONTENT in case of list
- String docPath = (String) this.values.get(EV_AUDIT_DOC_PATH);
- String[] docPathArr = docPath.split("/");
- String docName = docPathArr[docPathArr.length - 1];
- String docNameArr[] = docName.split("\\.");
- String docExt = docNameArr[docNameArr.length - 1];
- logger.debug("\n\n\n\n The " + docName + " extension is " + docExt + " \n\n\n\n");
- if("pdf".equals(docExt) || "png".equals(docExt) || "jpg".equals(docExt) || "jpeg".equals(docExt) || "dwg".equals(docExt))
- return EV_AUDIT_ACTION_READ_SHOW + " OR " + EV_AUDIT_ACTION_DOWNLOAD_SHOW;
- return EV_AUDIT_ACTION_DOWNLOAD_SHOW;
- } else if (this.values.get(EV_AUDIT_ACTION_PATH).equals(EV_AUDIT_ACTION_ADD_NODE_ASPECT)) {
- return EV_AUDIT_ACTION_ADD_NODE_ASPECT_SHOW;
- }else if (this.values.get(EV_AUDIT_ACTION_PATH).equals(EV_AUDIT_ACTION_DELETE_NODE_ASPECT)) {
- return EV_AUDIT_ACTION_DELETE_NODE_ASPECT_SHOW;
- }else
- return this.values.get(EV_AUDIT_ACTION_PATH).toString();
- }
- public Map<String, Serializable> getValues() {
- return this.values;
- }
- public List<String> getUpdatedPropertiesList() {
- String objectType;
- // This list will be returned containing the string declaring properties updated from-->to
- List<String> propertiesUpdatedFromAfter = new ArrayList<String>();
- // get the value that are updated
- if (this.values.get(EV_AUDIT_PROP_FROM_PATH) != null) {
- // Get the map of properties that are updated
- Map<QName, Serializable> beforeProperties = (Map<QName, Serializable>) this.values.get(EV_AUDIT_PROP_FROM_PATH);
- Map<QName, Serializable> afterProperties = (Map<QName, Serializable>) this.values.get(EV_AUDIT_PROP_TO_PATH);
- logger.debug(" \n\n$$$$$$$$$$$$$$$$\n\n Printing out keys of the beforeProperties map " + beforeProperties.keySet() + " \n\n$$$$$$$$$$$$$$$$\n\n");
- logger.debug(" \n\n$$$$$$$$$$$$$$$$\n\n Printing out values of the beforeProperties map " + beforeProperties.values() + " \n\n$$$$$$$$$$$$$$$$\n\n");
- logger.debug(" \n\n$$$$$$$$$$$$$$$$\n\n Printing out values of the afterProperties map " + afterProperties.values() + " \n\n$$$$$$$$$$$$$$$$\n\n");
- for(QName qname : beforeProperties.keySet()){
- // need to check the type of values returned
- objectType = getObjectType(beforeProperties.get(qname));
- // Creating object based on the return value of objectType
- if("String".equals(objectType)){
- String propBefore = (String) beforeProperties.get(qname);
- String propAfter = (String) afterProperties.get(qname);
- logger.debug("\n\n$$$$$$$$$$$$$$$\n\n . Property '" + qname.getLocalName() + "' updated from: '" + propBefore
- + "' to: '" + propAfter + "' \n\n$$$$$$$$$$$$$$$$\n\n");
- propertiesUpdatedFromAfter.add(". Property '" + qname.getLocalName() + "' updated from: '" + propBefore
- + "' to: '" + propAfter + "'");
- }else if("Map".equals(objectType)){
- Map propBefore = (Map) beforeProperties.get(qname);
- Map propAfter = (Map) afterProperties.get(qname);
- if(ContentModel.PROP_TITLE.equals(qname) || ContentModel.PROP_DESCRIPTION.equals(qname)){
- // propBefore.values() and propAfter.values() gives us a list
- // converting them to comma separated values
- logger.debug(" \n\n$$$$$$$$$$$$$$$$\n\n . Property " + qname.getLocalName() + " updated from: " + String.join(",", propBefore.values())
- + " to: " + String.join(",", propAfter.values()) + " \n\n$$$$$$$$$$$$$$$$\n\n");
- propertiesUpdatedFromAfter.add(". Property '" + qname.getLocalName() + "' updated from: '" + String.join(",", propBefore.values())
- + "' to: '" + String.join(",", propAfter.values()) + "'");
- }
- }else if("List".equals(objectType)){
- List propBefore = (List) beforeProperties.get(qname);
- List propAfter = (List) afterProperties.get(qname);
- if(qname.equals(ContentModel.PROP_TAGS)){
- // propAdd contains a list of noderefs for tags; we need to get the values from those noderefs
- // propBefore and propAfter can come with size 0 if the List object sent to getTaggablePropValue is null
- propBefore = getTaggablePropValue(propBefore);
- propAfter = getTaggablePropValue(propAfter);
- logger.debug(" \n\n$$$$$$$$$$$$$$$$\n\n . Property " + qname.getLocalName() + " updated from: " + String.join(",", propBefore)
- + " to: " + String.join(",", propAfter) + " \n\n$$$$$$$$$$$$$$$$\n\n");
- propertiesUpdatedFromAfter.add(". Property '" + qname.getLocalName() + "' updated from: '" + propBefore.stream()
- .collect(Collectors.joining(",")) + " to: " + String.join(",", propAfter) + "'");
- }
- }else if("Date".equals(objectType)){
- Date propBefore = (Date) beforeProperties.get(qname);
- Date propAfter = (Date) afterProperties.get(qname);
- if(ContentModel.PROP_MODIFIED.equals(qname)){
- // don't add this to list as modified value is of no use
- }else{
- logger.debug(" \n\n$$$$$$$$$$$$$$$$\n\n . Property " + qname.getLocalName() + " updated from: " + propBefore
- + " to: " + propAfter + " \n\n$$$$$$$$$$$$$$$$\n\n");
- propertiesUpdatedFromAfter.add(". Property '" + qname.getLocalName() + "' updated from: '" + propBefore
- + " to: " + propAfter + "'");
- }
- }else if("Long".equals(objectType)){
- Long propBefore = (Long) beforeProperties.get(qname);
- Long propAfter = (Long) afterProperties.get(qname);
- logger.debug(" \n\n$$$$$$$$$$$$$$$$\n\n . Property " + qname.getLocalName() + " updated from: " + propBefore
- + " to: " + propAfter + " \n\n$$$$$$$$$$$$$$$$\n\n");
- propertiesUpdatedFromAfter.add(". Property '" + qname.getLocalName() + "' updated from: '" + propBefore
- + " to: " + propAfter + "'");
- }
- }
- }
- // get the value that are added
- if (this.values.get(EV_AUDIT_PROP_ADD_PATH) != null){
- // Get the map of properties that are added
- Map<QName, Serializable> addedProperties = (Map<QName, Serializable>) this.values.get(EV_AUDIT_PROP_ADD_PATH);
- logger.debug("\n\n**************\n\n Printing out keys of the addedProperties map " + addedProperties.keySet() + " \n\n**************\n\n");
- logger.debug("\n\n**************\n\n Printing out values of the addedProperties map " + addedProperties.values() + " \n\n**************\n\n");
- for(QName qname : addedProperties.keySet()){
- // need to check the type of values returned
- objectType = getObjectType(addedProperties.get(qname));
- if("String".equals(objectType)){
- String propAdd = (String) addedProperties.get(qname);
- logger.debug("\n\n**************\n\n . Property " + qname.getLocalName() + " added with value " + propAdd
- + " \n\n**************\n\n ");
- propertiesUpdatedFromAfter.add(". Property '" + qname.getLocalName() + "' added with value '" + propAdd + "'");
- }else if("Map".equals(objectType)){
- Map propAdd = (Map) addedProperties.get(qname);
- if(ContentModel.PROP_TITLE.equals(qname) || ContentModel.PROP_DESCRIPTION.equals(qname)){
- logger.debug("\n\n**************\n\n . Property " + qname.getLocalName() + " added with value " + String.join(",", propAdd.values())
- + " \n\n**************\n\n ");
- propertiesUpdatedFromAfter.add(". Property '" + qname.getLocalName() + "' added with value '" + String.join(",", propAdd.values()) + "'");
- }
- }else if("List".equals(objectType)){
- List propAdd = (List) addedProperties.get(qname);
- if(qname.equals(ContentModel.PROP_TAGS)){
- // propAdd contains a list of noderefs for tags; we need to get the values from those noderefs
- // propAdd can come with size 0 if the List object sent to getTaggablePropValue is null
- propAdd = getTaggablePropValue(propAdd);
- logger.debug("\n\n**************\n\n . Property " + qname.getLocalName() + " added with value " + String.join(",", propAdd)
- + " \n\n**************\n\n ");
- propertiesUpdatedFromAfter.add(". Property '" + qname.getLocalName() + "' added with value '" + String.join(",", propAdd) + "'");
- }
- }else if("Date".equals(objectType)){
- Date propAdd = (Date) addedProperties.get(qname);
- logger.debug("\n\n**************\n\n . Property " + qname.getLocalName() + " added with value " + propAdd
- + "\n\n**************\n\n ");
- propertiesUpdatedFromAfter.add(". Property '" + qname.getLocalName() + "' added with value '" + propAdd + "'");
- }else if("Long".equals(objectType)){
- Long propAdd = (Long) addedProperties.get(qname);
- logger.debug("\n\n**************\n\n . Property " + qname.getLocalName() + " added with value " + propAdd
- + " \n\n**************\n\n ");
- propertiesUpdatedFromAfter.add(". Property '" + qname.getLocalName() + "' added with value '" + propAdd + "'");
- }
- }
- }
- // Get the aspects that are added
- if(this.values.get(EV_AUDIT_ASPECTS_ADD_PATH) != null){
- Set<QName> aspectsAddList = (Set<QName>)this.values.get(EV_AUDIT_ASPECTS_ADD_PATH);
- logger.debug(" \n\n^^^^^^^^^^^^^\n\n Printing out values of the aspectsAddList list " + aspectsAddList + " \n\n^^^^^^^^^^^^^\n\n");
- Set<String> aspects = new HashSet<>();
- for(QName qName : aspectsAddList){
- logger.debug("Getting localname for qname " + qName.getLocalName());
- aspects.add(qName.getLocalName());
- }
- logger.debug("\n\n^^^^^^^^^^^^^\n\n . Aspect " + String.join(",", aspects) + " added \n\n^^^^^^^^^^^^^\n\n");
- propertiesUpdatedFromAfter.add(". Aspect(s) '" + String.join(",", aspects) + "' added");
- // Trying with java 8 but not working :(
- /*String citiesCommaSeparated = aspectsAddList.stream().map(i -> i.getLocalName()).collect(Collectors.joining(","));
- logger.debug("************** \n\n\n\n Aspect " + citiesCommaSeparated + " added \n\n\n\n **************");
- propertiesUpdatedFromAfter.add("Aspect " + citiesCommaSeparated + " added");*/
- }
- // Get the aspects that are deleted
- if(this.values.get(EV_AUDIT_ASPECTS_DELETE_PATH) != null){
- Set<QName> aspectsDeleteList = (Set<QName>)this.values.get(EV_AUDIT_ASPECTS_DELETE_PATH);
- logger.debug("\n\n&&&&&&&&&&&&\n\n Printing out values of the aspectsDeleteList list " + aspectsDeleteList + " \n\n&&&&&&&&&&&&\n\n");
- Set<String> aspects = new HashSet<>();
- for(QName qName : aspectsDeleteList){
- logger.debug("Getting localname for qname " + qName);
- aspects.add(qName.getLocalName());
- }
- logger.debug("\n\n&&&&&&&&&&&&\n\n . Aspect " + String.join(",", aspects) + " removed \n\n&&&&&&&&&&&&\n\n");
- propertiesUpdatedFromAfter.add(". Aspect(s) '" + String.join(",", aspects) + "' removed");
- }
- // Get the move path from
- if(this.values.get(EV_AUDIT_MOVE_FROM_PATH) != null){
- String moveFromPath = (String)this.values.get(EV_AUDIT_MOVE_FROM_PATH);
- String moveToPath = (String)this.values.get(EV_AUDIT_DOC_PATH);
- moveFromPath = moveFromPath.replace(("/app:company_home/st:sites"),"");
- moveToPath = moveToPath.replace(("/app:company_home/st:sites"),"");
- logger.debug("\n\n#############\n\n . Moved from " + moveFromPath + " to " + moveToPath + " \n\n#############\n\n");
- propertiesUpdatedFromAfter.add(". Moved from '" + moveFromPath + "' to '" + moveToPath + "'");
- }
- // Get the copy path from
- if(this.values.get(EV_AUDIT_COPY_FROM_PATH) != null){
- String copyFromPath = (String)this.values.get(EV_AUDIT_COPY_FROM_PATH);
- String copyToPath = (String)this.values.get(EV_AUDIT_DOC_PATH);
- copyFromPath = copyFromPath.replace(("/app:company_home/st:sites"),"");
- copyToPath = copyToPath.replace(("/app:company_home/st:sites"),"");
- logger.debug("\n\n#############\n\n . Copied from " + copyFromPath + " to " + copyToPath + " \n\n#############\n\n");
- propertiesUpdatedFromAfter.add(". Copied from '" + copyFromPath + "' to '" + copyToPath + "'");
- }
- return propertiesUpdatedFromAfter;
- }
- /**
- *
- * @param taggablePropertyValue
- * @return String:tagNames.toString()
- *
- * Returns tag names from their respective nodeRef
- */
- public List<String> getTaggablePropValue(List<NodeRef> tagsNodeRef) {
- List<String> tagsNames = new ArrayList<>();
- if(tagsNodeRef == null){
- return tagsNames;
- }
- for (int i = 0; i < tagsNodeRef.size(); i++) {
- tagsNames.add((String) nodeService.getProperty(tagsNodeRef.get(i), ContentModel.PROP_NAME));
- }
- return tagsNames;
- }
- /**
- *
- * @param object
- * @return String:objectType
- *
- * Get the object type for the values map values
- */
- public String getObjectType(Serializable object){
- String objectType = null;
- if(object instanceof String){
- objectType = "String";
- }else if(object instanceof Map){
- objectType = "Map";
- }else if(object instanceof List){
- objectType = "List";
- }else if(object instanceof Date){
- objectType = "Date";
- }else if(object instanceof Long){
- objectType = "Long";
- }
- logger.debug("\t\t Got " +objectType+ " type \t\t");
- return objectType;
- }
- public String toString() {
- return "TemplateAuditInfo [applicationName=" + applicationName
- + ",userName= " + userName + ",time= " + time + ",values= "
- + values + "]";
- }
- }
- }
Add Comment
Please, Sign In to add comment