--- /dev/null
+package fi.vtt.simantics.procore.internal;\r
+\r
+import java.io.File;\r
+import java.io.IOException;\r
+import java.lang.management.ManagementFactory;\r
+import java.lang.reflect.Method;\r
+import java.util.HashMap;\r
+import java.util.Map;\r
+import java.util.TreeSet;\r
+import java.util.UUID;\r
+\r
+import org.eclipse.core.runtime.Platform;\r
+import org.osgi.framework.Bundle;\r
+import org.simantics.databoard.binding.mutable.Variant;\r
+import org.simantics.databoard.parser.repository.DataValueRepository;\r
+import org.simantics.db.DirectStatements;\r
+import org.simantics.db.Resource;\r
+import org.simantics.db.Session;\r
+import org.simantics.db.Statement;\r
+import org.simantics.db.WriteGraph;\r
+import org.simantics.db.common.request.WriteRequest;\r
+import org.simantics.db.common.request.WriteResultRequest;\r
+import org.simantics.db.common.utils.Logger;\r
+import org.simantics.db.common.utils.NameUtils;\r
+import org.simantics.db.exception.DatabaseException;\r
+import org.simantics.db.exception.ServiceException;\r
+import org.simantics.db.exception.ValidationException;\r
+import org.simantics.db.impl.Activator;\r
+import org.simantics.db.impl.ClusterI;\r
+import org.simantics.db.impl.ResourceImpl;\r
+import org.simantics.db.impl.graph.ReadGraphImpl;\r
+import org.simantics.db.impl.graph.WriteLogger;\r
+import org.simantics.db.impl.query.QueryProcessor;\r
+import org.simantics.db.impl.query.QuerySupport;\r
+import org.simantics.db.procore.cluster.ClusterBig;\r
+import org.simantics.db.procore.cluster.ClusterImpl;\r
+import org.simantics.db.procore.cluster.ClusterSmall;\r
+import org.simantics.db.service.DebugSupport;\r
+import org.simantics.db.service.QueryControl;\r
+import org.simantics.db.service.XSupport;\r
+import org.simantics.layer0.Layer0;\r
+import org.simantics.scl.runtime.function.Function2;\r
+import org.simantics.scl.runtime.function.Function3;\r
+import org.simantics.scl.runtime.function.FunctionImpl2;\r
+import org.simantics.scl.runtime.function.FunctionImpl3;\r
+import org.simantics.utils.Development;\r
+import org.simantics.utils.FileUtils;\r
+\r
+import gnu.trove.map.hash.TIntIntHashMap;\r
+import gnu.trove.procedure.TIntIntProcedure;\r
+import gnu.trove.procedure.TIntShortProcedure;\r
+import gnu.trove.set.hash.TIntHashSet;\r
+\r
+public class DebugSupportImpl implements DebugSupport {\r
+\r
+ final private Map<String, Function2<WriteGraph, String, Object>> getCommands = new HashMap<String, Function2<WriteGraph, String, Object>>();\r
+ final private Map<String, Function3<WriteGraph, File, String, String>> listCommands = new HashMap<String, Function3<WriteGraph, File, String, String>>();\r
+ final private Map<String, Function2<WriteGraph, String, String>> execCommands = new HashMap<String, Function2<WriteGraph, String, String>>();\r
+ final private Map<String, Function2<WriteGraph, String, String>> printCommands = new HashMap<String, Function2<WriteGraph, String, String>>();\r
+\r
+ private static SessionImplSocket getSession(WriteGraph graph) {\r
+ return (SessionImplSocket)graph.getSession();\r
+ }\r
+\r
+ DebugSupportImpl() {\r
+\r
+ getCommands.put("listeners", new FunctionImpl2<WriteGraph, String, Object>() {\r
+\r
+ @Override\r
+ public Object apply(WriteGraph graph, String args) {\r
+ try {\r
+ return getSession(graph).queryProvider2.getListenerReport();\r
+ } catch (IOException e) {\r
+ Logger.defaultLogError(e);\r
+ return e.getMessage();\r
+ }\r
+ }\r
+\r
+ });\r
+\r
+ listCommands.put("counters", new FunctionImpl3<WriteGraph, File, String, String>() {\r
+\r
+ @Override\r
+ public String apply(WriteGraph graph, File file, String args) {\r
+ try {\r
+ return ReadGraphImpl.listCounters(file);\r
+ } catch (IOException e) {\r
+ Logger.defaultLogError(e);\r
+ return e.getMessage();\r
+ }\r
+ }\r
+\r
+ });\r
+\r
+ listCommands.put("queries", new FunctionImpl3<WriteGraph, File, String, String>() {\r
+\r
+ @Override\r
+ public String apply(WriteGraph graph, File file, String args) {\r
+ try {\r
+ return getSession(graph).queryProvider2.reportQueries(file);\r
+ } catch (IOException e) {\r
+ Logger.defaultLogError(e);\r
+ return e.getMessage();\r
+ }\r
+ }\r
+\r
+ });\r
+\r
+ listCommands.put("queryActivity", new FunctionImpl3<WriteGraph, File, String, String>() {\r
+\r
+ @Override\r
+ public String apply(WriteGraph graph, File file, String args) {\r
+ try {\r
+ return getSession(graph).queryProvider2.reportQueryActivity(file);\r
+ } catch (IOException e) {\r
+ Logger.defaultLogError(e);\r
+ return e.getMessage();\r
+ }\r
+ }\r
+\r
+ });\r
+\r
+ listCommands.put("listeners", new FunctionImpl3<WriteGraph, File, String, String>() {\r
+\r
+ @Override\r
+ public String apply(WriteGraph graph, File file, String args) {\r
+ try {\r
+ return getSession(graph).queryProvider2.reportListeners(file);\r
+ } catch (IOException e) {\r
+ Logger.defaultLogError(e);\r
+ return e.getMessage();\r
+ }\r
+ }\r
+\r
+ });\r
+\r
+ listCommands.put("clusters", new FunctionImpl3<WriteGraph, File, String, String>() {\r
+\r
+ @Override\r
+ public String apply(WriteGraph graph, File file, String args) {\r
+ return reportClusters(getSession(graph), file);\r
+ }\r
+\r
+ });\r
+\r
+ listCommands.put("cluster", new FunctionImpl3<WriteGraph, File, String, String>() {\r
+\r
+ @Override\r
+ public String apply(WriteGraph graph, File file, String args) {\r
+ return reportCluster(graph, file, args);\r
+ }\r
+\r
+ });\r
+\r
+ listCommands.put("virtuals", new FunctionImpl3<WriteGraph, File, String, String>() {\r
+\r
+ @Override\r
+ public String apply(WriteGraph graph, File file, String args) {\r
+ return reportVirtuals(getSession(graph), file);\r
+ }\r
+\r
+ });\r
+\r
+ listCommands.put("heap", new FunctionImpl3<WriteGraph, File, String, String>() {\r
+\r
+ @Override\r
+ public String apply(WriteGraph graph, File file, String args) {\r
+\r
+ try {\r
+\r
+ file.delete();\r
+\r
+ Object bean = getBean();\r
+ if (bean == null)\r
+ return "Could not retrieve bean.";\r
+\r
+ Method m = bean.getClass().getMethod("dumpHeap", String.class, boolean.class);\r
+ if (args.length() > 0) {\r
+ m.invoke(bean, file.getParent() + "/" + args, true);\r
+ } else {\r
+ m.invoke(bean, file.getAbsolutePath(), true);\r
+ }\r
+\r
+ } catch (Throwable t) {\r
+ Logger.defaultLogError(t);\r
+ return "Unexpected exception " + t;\r
+ }\r
+\r
+ return "Wrote " + file.getAbsolutePath();\r
+\r
+ }\r
+\r
+ private Object getBean() {\r
+ Class<?> beanClass = getBeanClass();\r
+ if (beanClass == null)\r
+ return null;\r
+ try {\r
+ Object bean = ManagementFactory.newPlatformMXBeanProxy(\r
+ ManagementFactory.getPlatformMBeanServer(),\r
+ "com.sun.management:type=HotSpotDiagnostic",\r
+ beanClass);\r
+ return bean;\r
+ } catch (IOException e) {\r
+ return null;\r
+ }\r
+ }\r
+\r
+ private Class<?> getBeanClass() {\r
+ try {\r
+ Class<?> clazz = Class.forName("com.sun.management.HotSpotDiagnosticMXBean");\r
+ return clazz;\r
+ } catch (ClassNotFoundException e) {\r
+ return null;\r
+ }\r
+ }\r
+\r
+ });\r
+\r
+ execCommands.put("WriteLogger.read", new FunctionImpl2<WriteGraph, String, String>() {\r
+\r
+ @Override\r
+ public String apply(WriteGraph graph, String args) {\r
+ graph.getSession().async(new WriteRequest() {\r
+\r
+ @Override\r
+ public void perform(WriteGraph graph) throws DatabaseException {\r
+ try {\r
+ WriteLogger.read(graph);\r
+ } catch (Exception e) {\r
+ e.printStackTrace();\r
+ }\r
+ }\r
+\r
+ });\r
+ return "Started to read the write log.";\r
+ }\r
+\r
+ });\r
+\r
+ execCommands.put("ReadGraph.resetCounters", new FunctionImpl2<WriteGraph, String, String>() {\r
+\r
+ @Override\r
+ public String apply(WriteGraph graph, String args) {\r
+ graph.getSession().async(new WriteRequest() {\r
+\r
+ @Override\r
+ public void perform(WriteGraph graph) throws DatabaseException {\r
+ try {\r
+ ReadGraphImpl.resetCounters();\r
+ } catch (Exception e) {\r
+ e.printStackTrace();\r
+ }\r
+ }\r
+\r
+ });\r
+ return "Started to read the write log.";\r
+ }\r
+\r
+ });\r
+\r
+ execCommands.put("QueryControl.flush", new FunctionImpl2<WriteGraph, String, String>() {\r
+\r
+ @Override\r
+ public String apply(WriteGraph graph, String args) {\r
+ QueryControl qc = graph.getService(QueryControl.class);\r
+ qc.flush(graph);\r
+ return "Flushed queries.";\r
+ }\r
+\r
+ });\r
+\r
+ execCommands.put("DebugSupport.validateClusters", new FunctionImpl2<WriteGraph, String, String>() {\r
+\r
+ @Override\r
+ public String apply(WriteGraph graph, String args) {\r
+ return validateClusters(graph);\r
+ }\r
+\r
+ });\r
+\r
+ execCommands.put("DebugSupport.writeForMs", new FunctionImpl2<WriteGraph, String, String>() {\r
+\r
+ @Override\r
+ public String apply(WriteGraph graph, String args) {\r
+ Integer amount = Integer.parseInt(args);\r
+ return writeForMs(graph, amount);\r
+ }\r
+\r
+ });\r
+\r
+ printCommands.put("nextId", new FunctionImpl2<WriteGraph, String, String>() {\r
+\r
+ @Override\r
+ public String apply(WriteGraph graph, String args) {\r
+ try {\r
+ SessionImplSocket session = getSession(graph);\r
+ ClusterImpl cluster = session.clusterTable.getNewResourceCluster(session.clusterTranslator, session.graphSession, false);\r
+ long cid = cluster.getClusterId();\r
+ int rid = cluster.getNumberOfResources(session.clusterTranslator);\r
+ return cid + "#" + rid;\r
+ } catch (Throwable t) {\r
+ return UUID.randomUUID().toString();\r
+ }\r
+ }\r
+\r
+ });\r
+\r
+ printCommands.put("usedMemory", new FunctionImpl2<WriteGraph, String, String>() {\r
+\r
+ @Override\r
+ public String apply(WriteGraph graph, String args) {\r
+ try {\r
+ Runtime runtime = Runtime.getRuntime();\r
+ return "" + (runtime.totalMemory() - runtime.freeMemory());\r
+ } catch (Throwable t) {\r
+ return UUID.randomUUID().toString();\r
+ }\r
+ }\r
+\r
+ });\r
+\r
+ }\r
+\r
+ private String reportClusters(SessionImplSocket session, File file) {\r
+\r
+ try {\r
+ StringBuilder b = new StringBuilder();\r
+ long totalApproxSize = 0;\r
+ int loaded = 0;\r
+ for(ClusterI cluster : session.clusterTable.getClusters()) {\r
+ b.append("[" + cluster.getClusterKey() + "]: ");\r
+ if(cluster instanceof ClusterSmall) b.append("ClusterSmall[" + cluster.getClusterId() + "]");\r
+ if(cluster instanceof ClusterBig) b.append("ClusterBig[" + cluster.getClusterId() + "]");\r
+ if(cluster instanceof ClusterWriteOnly) b.append("ClusterWriteOnly[" + cluster.getClusterId() + "]");\r
+ if(cluster.isLoaded()) {\r
+ long approx = cluster.getUsedSpace();\r
+ b.append(" approx size = " + approx + " bytes.\n");\r
+ totalApproxSize += approx;\r
+ loaded++;\r
+ } else {\r
+ b.append(" is not loaded.\n");\r
+ }\r
+ }\r
+ b.append("#Total approx size is " + totalApproxSize + " bytes.\n");\r
+ b.append("#Amount of loaded clusters is " + loaded + ".\n");\r
+ FileUtils.writeFile(file, b.toString().getBytes());\r
+ } catch (IOException e) {\r
+ e.printStackTrace();\r
+ } catch (DatabaseException e) {\r
+ e.printStackTrace();\r
+ }\r
+\r
+ return "OK";\r
+\r
+ }\r
+\r
+ private String reportCluster(final WriteGraph graph, File file, String args) {\r
+\r
+ try {\r
+\r
+ final StringBuilder b = new StringBuilder();\r
+ final SessionImplSocket session = (SessionImplSocket)graph.getSession();\r
+ long clusterId = Long.parseLong(args);\r
+ b.append("cluster id: " + clusterId);\r
+ b.append("\n");\r
+ b.append("internal resources: ");\r
+ b.append("\n");\r
+ ClusterI cluster = session.clusterTable.getClusterByClusterId(clusterId);\r
+ for(int i=1;i<=cluster.getNumberOfResources(session.clusterTranslator);i++) {\r
+ Resource r = session.getResource(i, clusterId);\r
+ String def = NameUtils.getSafeName(graph, r);\r
+ b.append( i+": " + def);\r
+ b.append("\n");\r
+ }\r
+ if(cluster instanceof ClusterSmall) {\r
+ ClusterSmall clusterSmall = (ClusterSmall)cluster;\r
+ b.append("foreign resources: ");\r
+ b.append("\n");\r
+ final TIntIntHashMap clusterHistogram = new TIntIntHashMap();\r
+ clusterSmall.foreignTable.getResourceHashMap().forEachEntry(new TIntShortProcedure() {\r
+\r
+ @Override\r
+ public boolean execute(int index, short pos) {\r
+ try {\r
+ Resource r = session.getResource(index);\r
+ String def = NameUtils.getSafeName(graph, r, true);\r
+ int cluster = index >>> 12;\r
+ int key = index & 0xfff;\r
+ int exist = clusterHistogram.get(cluster);\r
+ clusterHistogram.put(cluster, exist+1);\r
+ b.append( cluster + "$" + key +": " + def);\r
+ b.append("\n");\r
+ } catch (ValidationException e) {\r
+ e.printStackTrace();\r
+ } catch (ServiceException e) {\r
+ e.printStackTrace();\r
+ }\r
+ return true;\r
+ }\r
+ });\r
+ b.append("foreign histogram: ");\r
+ b.append("\n");\r
+ clusterHistogram.forEachEntry(new TIntIntProcedure() {\r
+\r
+ @Override\r
+ public boolean execute(int cluster, int count) {\r
+ b.append( cluster +": " + count);\r
+ b.append("\n");\r
+ return true;\r
+ }\r
+ });\r
+ }\r
+\r
+ FileUtils.writeFile(file, b.toString().getBytes());\r
+ } catch (IOException e) {\r
+ e.printStackTrace();\r
+ } catch (DatabaseException e) {\r
+ e.printStackTrace();\r
+ }\r
+\r
+ return "OK";\r
+\r
+ }\r
+\r
+ private String reportVirtuals(SessionImplSocket session, File file) {\r
+\r
+ session.virtualGraphServerSupport.report(file);\r
+\r
+ return "OK";\r
+\r
+ }\r
+\r
+ public Object query(Session session, final String command) {\r
+ try {\r
+ return session.sync(new WriteResultRequest<Object>() {\r
+\r
+ @Override\r
+ public Object perform(WriteGraph graph) throws DatabaseException {\r
+ return query(graph, command);\r
+ }\r
+\r
+ });\r
+ } catch (DatabaseException e) {\r
+ Logger.defaultLogError(e);\r
+ return null;\r
+ }\r
+ }\r
+\r
+ @Override\r
+ public Object query(WriteGraph graph, String command) {\r
+\r
+ Bundle bundle = Platform.getBundle(Activator.BUNDLE_ID);\r
+ File base = Utils.getBaseFile(bundle);\r
+\r
+ command = command.trim();\r
+\r
+ try {\r
+\r
+ if("help".equals(command)) {\r
+\r
+ return\r
+ "Welcome to the Simantics session debugger.<br><br>" +\r
+ "This shell allows you to make following queries into the running Simantics database session:<br>" +\r
+ "<ul><li>Get commands, which return debug objects. Type 'help get' to obtain more information.</li>" +\r
+ "<li>List commands, which create debug listings into files. Type 'help list' to obtain more information.</li>" +\r
+ "<li>Print commands, which output information about session state. Type 'help print' to obtain more information.</li>" +\r
+ "<li>Set commands, which modify session state variables. Type 'help set' to obtain more information.</li>" +\r
+ "<li>Exec commands, which perform certain actions. Type 'help exec' to obtain more information.</li></ul>"\r
+ ;\r
+\r
+ } else if ("help get".equals(command)) {\r
+\r
+ StringBuilder b = new StringBuilder();\r
+ b.append("The following get commands are available.<br><ul>");\r
+ for(String key : getCommands.keySet())\r
+ b.append("<li>" + key + "</li>");\r
+\r
+ b.append("</ul>");\r
+\r
+ return b.toString();\r
+\r
+ } else if ("help list".equals(command)) {\r
+\r
+ StringBuilder b = new StringBuilder();\r
+ b.append("The following list commands are available.<br><ul>");\r
+ for(String key : listCommands.keySet())\r
+ b.append("<li>" + key + "</li>");\r
+\r
+ b.append("</ul>");\r
+\r
+ return b.toString();\r
+\r
+ } else if ("help exec".equals(command)) {\r
+\r
+ StringBuilder b = new StringBuilder();\r
+ b.append("The following exec commands are available.<br><ul>");\r
+ for(String key : execCommands.keySet())\r
+ b.append("<li>" + key + "</li>");\r
+\r
+ b.append("</ul>");\r
+\r
+ return b.toString();\r
+\r
+ } else if ("help print".equals(command)) {\r
+\r
+ StringBuilder b = new StringBuilder();\r
+ b.append("The following print commands are available.<br><ul>");\r
+ for(String key : printCommands.keySet())\r
+ b.append("<li>" + key + "</li>");\r
+\r
+ b.append("</ul>");\r
+\r
+ return b.toString();\r
+\r
+ } else if ("help set".equals(command)) {\r
+\r
+ StringBuilder b = new StringBuilder();\r
+ b.append("The following set commands are available.<br><ul>");\r
+\r
+ for(Map.Entry<String, Variant> e : Development.getProperties().entrySet()) {\r
+ b.append("<li>" + e.getKey() + " - " + e.getValue().getBinding().type() + "</li>");\r
+ }\r
+\r
+ b.append("</ul>");\r
+\r
+ return b.toString();\r
+\r
+ } else if(command.startsWith("get")) {\r
+\r
+ String remainder = command.substring(3).trim();\r
+\r
+ for(Map.Entry<String, Function2<WriteGraph, String, Object>> e : getCommands.entrySet()) {\r
+ String key = e.getKey();\r
+ if(remainder.startsWith(key)) {\r
+ String args = remainder.substring(key.length()).trim();\r
+ return e.getValue().apply(graph, args);\r
+ }\r
+ }\r
+\r
+ } else if(command.startsWith("list")) {\r
+\r
+ String remainder = command.substring(4).trim();\r
+\r
+ for(Map.Entry<String, Function3<WriteGraph, File, String, String>> e : listCommands.entrySet()) {\r
+ String key = e.getKey();\r
+ if(remainder.startsWith(key)) {\r
+ String args = remainder.substring(key.length()).trim();\r
+ File file = new File(base, key + ".list");\r
+ base.mkdirs();\r
+ e.getValue().apply(graph, file, args);\r
+ return "Wrote " + file.getAbsolutePath();\r
+ }\r
+ }\r
+\r
+ } else if(command.startsWith("set")) {\r
+\r
+ String remainder = command.substring(3).trim();\r
+ String[] keyAndValue = remainder.split("=");\r
+ if(keyAndValue.length == 2) {\r
+\r
+ Variant exist = Development.getProperties().get(keyAndValue[0]);\r
+ if(exist != null) {\r
+ Development.setProperty(keyAndValue[0], exist.getBinding().parseValue(keyAndValue[1], new DataValueRepository()), exist.getBinding());\r
+ return "Property " + keyAndValue[0] + " was set to '" + keyAndValue[1] + "'";\r
+ } else {\r
+ return query(graph, "help set");\r
+ }\r
+ } else {\r
+ return query(graph, "help set");\r
+ }\r
+\r
+ } else if(command.startsWith("print")) {\r
+\r
+ String remainder = command.substring(5).trim();\r
+\r
+ for(Map.Entry<String, Function2<WriteGraph, String, String>> e : printCommands.entrySet()) {\r
+ String key = e.getKey();\r
+ if(remainder.startsWith(key)) {\r
+ String args = remainder.substring(key.length()).trim();\r
+ return e.getValue().apply(graph, args);\r
+ }\r
+ }\r
+\r
+ } else if(command.startsWith("exec")) {\r
+\r
+ String remainder = command.substring(4).trim();\r
+\r
+ for(Map.Entry<String, Function2<WriteGraph, String, String>> e : execCommands.entrySet()) {\r
+ String key = e.getKey();\r
+ if(remainder.startsWith(key)) {\r
+ String args = remainder.substring(key.length()).trim();\r
+ return e.getValue().apply(graph, args);\r
+ }\r
+ }\r
+\r
+ }\r
+\r
+ return "Unknown command '" + command + "'";\r
+\r
+ } catch (Throwable t) {\r
+\r
+ t.printStackTrace();\r
+\r
+ return t.getMessage();\r
+\r
+ }\r
+\r
+ }\r
+ private void writeResouce(WriteGraph graph) throws DatabaseException {\r
+ Layer0 l0 = Layer0.getInstance(graph);\r
+ Resource r = graph.newResource();\r
+ graph.claim(r, l0.InstanceOf, l0.Entity);\r
+ XSupport xs = (XSupport)graph.getService(XSupport.class);\r
+ xs.flushCluster(r);\r
+ }\r
+ private void wait(int amount) {\r
+ long start = System.nanoTime();\r
+ long limit = amount * 1000000L;\r
+ while ((System.nanoTime() - start) < limit) {\r
+ try {\r
+ Thread.sleep(100);\r
+ } catch (InterruptedException e) {\r
+ e.printStackTrace();\r
+ }\r
+ }\r
+ }\r
+ String writeForMs(WriteGraph graph, int amount) {\r
+ try {\r
+ writeResouce(graph);\r
+ wait(amount);\r
+ writeResouce(graph);\r
+ } catch (DatabaseException e) {\r
+ e.printStackTrace();\r
+ }\r
+\r
+ return "Slept for " + amount + " ms in write transaction.";\r
+ }\r
+\r
+ String validateClusters(WriteGraph graph) {\r
+\r
+ ReadGraphImpl impl = (ReadGraphImpl)graph;\r
+ QueryProcessor processor = impl.processor;\r
+\r
+ QuerySupport qs = graph.getService(QuerySupport.class);\r
+\r
+ TIntHashSet done = new TIntHashSet();\r
+ TreeSet<Integer> fringe = new TreeSet<Integer>();\r
+\r
+ ResourceImpl root = (ResourceImpl)graph.getRootLibrary();\r
+\r
+ done.add(root.id);\r
+ fringe.add(root.id);\r
+\r
+ while(!fringe.isEmpty()) {\r
+ int r = fringe.first();\r
+ fringe.remove(r);\r
+ DirectStatements ds = qs.getStatements(impl, r, processor, true);\r
+ for(Statement stm : ds) {\r
+ ResourceImpl p = (ResourceImpl)stm.getPredicate();\r
+ ResourceImpl o = (ResourceImpl)stm.getObject();\r
+ if(done.add(p.id)) fringe.add(p.id);\r
+ if(done.add(o.id)) fringe.add(o.id);\r
+ }\r
+ qs.getValue(impl, r);\r
+ }\r
+\r
+ return "Validated " + done.size() + " resources.";\r
+\r
+ }\r
+\r
+}\r