All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.ow2.bonita.facade.impl.RuntimeAPIImpl Maven / Gradle / Ivy

/**
 * Copyright (C) 2006  Bull S. A. S.
 * Bull, Rue Jean Jaures, B.P.68, 78340, Les Clayes-sous-Bois
 * This library is free software; you can redistribute it and/or modify it under the terms
 * of the GNU Lesser General Public License as published by the Free Software Foundation
 * version 2.1 of the License.
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth
 * Floor, Boston, MA  02110-1301, USA.
 * 
 * Modified by Matthieu Chaffotte - BonitaSoft S.A.
 **/
package org.ow2.bonita.facade.impl;

import java.rmi.RemoteException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.ow2.bonita.facade.def.majorElement.ActivityDefinition;
import org.ow2.bonita.facade.def.majorElement.DataFieldDefinition;
import org.ow2.bonita.facade.def.majorElement.ProcessDefinition;
import org.ow2.bonita.facade.def.majorElement.impl.ProcessDefinitionImpl;
import org.ow2.bonita.facade.exception.ActivityNotFoundException;
import org.ow2.bonita.facade.exception.BonitaInternalException;
import org.ow2.bonita.facade.exception.IllegalTaskStateException;
import org.ow2.bonita.facade.exception.InstanceNotFoundException;
import org.ow2.bonita.facade.exception.ProcessNotFoundException;
import org.ow2.bonita.facade.exception.TaskNotFoundException;
import org.ow2.bonita.facade.exception.UncancellableInstanceException;
import org.ow2.bonita.facade.exception.UndeletableInstanceException;
import org.ow2.bonita.facade.exception.VariableNotFoundException;
import org.ow2.bonita.facade.internal.InternalRuntimeAPI;
import org.ow2.bonita.facade.runtime.ActivityInstance;
import org.ow2.bonita.facade.runtime.InstanceState;
import org.ow2.bonita.facade.runtime.ProcessInstance;
import org.ow2.bonita.facade.runtime.impl.CommentImpl;
import org.ow2.bonita.facade.runtime.impl.ProcessInstanceImpl;
import org.ow2.bonita.facade.uuid.ActivityInstanceUUID;
import org.ow2.bonita.facade.uuid.ProcessDefinitionUUID;
import org.ow2.bonita.facade.uuid.ProcessInstanceUUID;
import org.ow2.bonita.pvm.job.Timer;
import org.ow2.bonita.runtime.InternalExecution;
import org.ow2.bonita.runtime.InternalInstance;
import org.ow2.bonita.runtime.TaskManager;
import org.ow2.bonita.services.Archiver;
import org.ow2.bonita.services.LargeDataRepository;
import org.ow2.bonita.services.Querier;
import org.ow2.bonita.services.Recorder;
import org.ow2.bonita.services.Repository;
import org.ow2.bonita.util.AccessorUtil;
import org.ow2.bonita.util.BonitaRuntimeException;
import org.ow2.bonita.util.EnvTool;
import org.ow2.bonita.util.ExceptionManager;
import org.ow2.bonita.util.GroovyException;
import org.ow2.bonita.util.GroovyUtil;
import org.ow2.bonita.util.Misc;
import org.ow2.bonita.util.ProcessUtil;

/**
 * @author Marc Blachon, Guillaume Porcher, Charles Souillard, Miguel Valdes, Pierre Vigneras
 */

public class RuntimeAPIImpl implements InternalRuntimeAPI {
  private static final Logger LOG = Logger.getLogger(ManagementAPIImpl.class.getName());

  protected RuntimeAPIImpl() {
  }

  /**
   * Create an instance of the specified process and return the processUUID
   */
  public ProcessInstanceUUID instantiateProcess(final ProcessDefinitionUUID processUUID) throws ProcessNotFoundException {
    try {
      return instantiateProcess(processUUID, null, null);
    } catch (final VariableNotFoundException e) {
      //must never occur
      throw new BonitaRuntimeException(e);
    }
  }

  public ProcessInstanceUUID instantiateProcess(final ProcessDefinitionUUID processUUID,
    final Map variables, Map attachments) throws ProcessNotFoundException, VariableNotFoundException {
    FacadeUtil.checkArgsNotNull(processUUID);
    if (LOG.isLoggable(Level.FINE)) {
        LOG.fine("Starting a new instance of process : " + processUUID);
    }
    InternalExecution rootExecution = ProcessUtil.createProcessInstance(processUUID, variables, attachments, null);
    if (LOG.isLoggable(Level.FINE)) {
        LOG.fine("Started: " + rootExecution.getInstance());
      }
    rootExecution.signal();
    return rootExecution.getInstance().getUUID();
  }

  public void executeTask(ActivityInstanceUUID taskUUID, boolean assignTask)
      throws TaskNotFoundException, IllegalTaskStateException, RemoteException {
    startTask(taskUUID, assignTask);
    finishTask(taskUUID, assignTask);
  }
  
  public void cancelProcessInstance(final ProcessInstanceUUID instanceUUID) throws InstanceNotFoundException,
    UncancellableInstanceException {
    //if this instance is a child execution, throw an exception
    FacadeUtil.checkArgsNotNull(instanceUUID);

    final Repository repository = EnvTool.getRepository();
    final Querier allQueriers = EnvTool.getAllQueriers();

    final ProcessInstance processInst = allQueriers.getProcessInstance(instanceUUID);

    if (processInst == null) {
      throw new InstanceNotFoundException("bai_RAPII_1", instanceUUID);
    }

    final InternalInstance instance = repository.getInstance(instanceUUID);
    final ProcessInstanceUUID parentInstanceUUID = processInst.getParentInstanceUUID();

    //if this instance is a child execution, throw an exception
    if (instance == null
        || parentInstanceUUID != null
        || !instance.getInstanceState().equals(InstanceState.STARTED)) {
      throw new UncancellableInstanceException("bai_RAPII_2", instanceUUID, parentInstanceUUID, processInst.getInstanceState());
    }

    instance.cancel();

  }
  
  public void cancelProcessInstances(Collection instanceUUIDs) throws InstanceNotFoundException, UncancellableInstanceException {
    FacadeUtil.checkArgsNotNull(instanceUUIDs);
    for (ProcessInstanceUUID instanceUUID : instanceUUIDs) {
      cancelProcessInstance(instanceUUID);
    }
    
  }

  public void deleteProcessInstances(final Collection instanceUUIDs) throws InstanceNotFoundException,
    UndeletableInstanceException {
    if (instanceUUIDs == null) {
      return;
    }
    for (ProcessInstanceUUID instanceUUID : instanceUUIDs) {
      deleteProcessInstance(instanceUUID);
    }
  }
  
  public void deleteProcessInstance(final ProcessInstanceUUID instanceUUID) throws InstanceNotFoundException,
  UndeletableInstanceException {
    //if this instance is a child execution, throw an exception
    //if this instance has children, delete them
    FacadeUtil.checkArgsNotNull(instanceUUID);

    final Repository repository = EnvTool.getRepository();
    final Querier allQueriers = EnvTool.getAllQueriers();
    final Querier journal = EnvTool.getJournalQueriers();
    final Querier history = EnvTool.getHistoryQueriers();

    ProcessInstance processInst = journal.getProcessInstance(instanceUUID);

    boolean inHistory = false;
    final boolean inJournal = processInst != null;
    if (!inJournal) {
      processInst = history.getProcessInstance(instanceUUID);
      inHistory = processInst != null;
    }

    if (processInst == null) {
      throw new InstanceNotFoundException("bai_RAPII_3", instanceUUID);
    }
    final ProcessInstanceUUID parentInstanceUUID = processInst.getParentInstanceUUID();
    //check that the parent instance does not exist anymore, else, throw an exception
    if (parentInstanceUUID != null && allQueriers.getProcessInstance(parentInstanceUUID) != null) {
      throw new UndeletableInstanceException("bai_RAPII_4", instanceUUID, parentInstanceUUID);
    }

    EnvTool.getLargeDataRepository().deleteData(Misc.getAttachmentCategories(instanceUUID));
    
    if (inJournal) {
      final Set timers = repository.getInstanceTimers(instanceUUID);
      if (timers != null) {
        for (final Timer timer : timers) {
          repository.removeTimer(timer);
        }
      }

      repository.removeInstance(instanceUUID);
      final Recorder recorder = EnvTool.getRecorder();
      recorder.remove(processInst);
    } else if (inHistory) {
      final Archiver archiver = EnvTool.getArchiver();
      archiver.remove(processInst);
    }
    final Set children = processInst.getChildrenInstanceUUID();
    for (final ProcessInstanceUUID child : children) {
      deleteProcessInstance(child);
    }
  }

  public void deleteAllProcessInstances(final ProcessDefinitionUUID processUUID) throws ProcessNotFoundException,
  UndeletableInstanceException {
    FacadeUtil.checkArgsNotNull(processUUID);

    final Querier querier = EnvTool.getAllQueriers();
    final ProcessDefinition process = querier.getProcess(processUUID);
    if (process == null) {
      throw new ProcessNotFoundException("bai_RAPII_5", processUUID);
    }

    Set instances = querier.getProcessInstances(processUUID);

    for (final ProcessInstance instance : instances) {
      //deletes only parent instances
      if (instance.getParentInstanceUUID() == null) {
        try {
          deleteProcessInstance(instance.getUUID());
        } catch (final InstanceNotFoundException e) {
        	String message = ExceptionManager.getInstance().getFullMessage("bai_RAPII_6");
          throw new BonitaInternalException(message, e);
        }
      }
    }
    instances = querier.getProcessInstances(processUUID);
    if (instances != null && !instances.isEmpty()) {
      final ProcessInstance first = instances.iterator().next();
      throw new UndeletableInstanceException("bai_RAPII_7", first.getUUID(), first.getParentInstanceUUID());
    }
  }

  public void startTask(final ActivityInstanceUUID taskUUID, final boolean assignTask) throws TaskNotFoundException, IllegalTaskStateException  {
    FacadeUtil.checkArgsNotNull(taskUUID);
    TaskManager.start(taskUUID, assignTask);
  }

  public void finishTask(final ActivityInstanceUUID taskUUID, final boolean assignTask) throws TaskNotFoundException, IllegalTaskStateException {
    FacadeUtil.checkArgsNotNull(taskUUID);
    TaskManager.finish(taskUUID, assignTask);
  }

  public void suspendTask(final ActivityInstanceUUID taskUUID, final boolean assignTask) throws TaskNotFoundException,
  IllegalTaskStateException {
    FacadeUtil.checkArgsNotNull(taskUUID);
    TaskManager.suspend(taskUUID, assignTask);
  }

  public void resumeTask(final ActivityInstanceUUID taskUUID, final boolean taskAssign) throws TaskNotFoundException, IllegalTaskStateException {
    FacadeUtil.checkArgsNotNull(taskUUID);
    TaskManager.resume(taskUUID, taskAssign);
  }

  public void assignTask(final ActivityInstanceUUID taskUUID) throws TaskNotFoundException {
    FacadeUtil.checkArgsNotNull(taskUUID);
    TaskManager.assign(taskUUID);
  }

  public void assignTask(final ActivityInstanceUUID taskUUID, final String userId) throws TaskNotFoundException {
    FacadeUtil.checkArgsNotNull(taskUUID, userId);
    TaskManager.assign(taskUUID, userId);
  }

  public void assignTask(final ActivityInstanceUUID taskUUID, final java.util.Set candidates) throws TaskNotFoundException {
    FacadeUtil.checkArgsNotNull(taskUUID, candidates);
    TaskManager.assign(taskUUID, candidates);
  }

  public void unassignTask(final ActivityInstanceUUID taskUUID) throws TaskNotFoundException {
    FacadeUtil.checkArgsNotNull(taskUUID);
    TaskManager.unAssign(taskUUID);
  }

  private void setProcessInstanceVariable(final ProcessInstance instance, final ProcessInstanceUUID instanceUUID,
      final String variableId, final Object variableValue) throws InstanceNotFoundException, VariableNotFoundException {

    if (instance == null) {
      throw new InstanceNotFoundException("bai_RAPII_20", instanceUUID);
    }
    /*
    final InternalExecution rootExecution = instance.getRootExecution();
    final Map globalVars = rootExecution.getScopeVariables();
    if (!globalVars.containsKey(variableId)) {
      throw new VariableNotFoundException("bai_RAPII_21", instanceUUID, variableId);
    }
    */
    
    if (!instance.getLastKnownVariableValues().containsKey(variableId)) {
        throw new VariableNotFoundException("bai_RAPII_21", instanceUUID, variableId);
      }
    final ProcessDefinitionUUID processDefinitionUUID = instance.getProcessDefinitionUUID();
    final ProcessDefinition ProcessDefinitionImpl = EnvTool.getAllQueriers().getProcess(processDefinitionUUID);
    Misc.badStateIfNull(ProcessDefinitionImpl, "can't find a process with uuid " + processDefinitionUUID);


    String dataTypeClassName = null;
    for (final DataFieldDefinition df : ProcessDefinitionImpl.getDataFields()) {
      if (df.getName().equals(variableId)) {
        dataTypeClassName = df.getDataTypeClassName();
      }
    }
    if (dataTypeClassName == null) {
      for (final DataFieldDefinition df : ProcessDefinitionImpl.getDataFields()) {
        if (df.getName().equals(variableId)) {
          dataTypeClassName = df.getDataTypeClassName();
        }
      }
    }
    String message = ExceptionManager.getInstance().getFullMessage("bai_RAPII_28", variableId);
    Misc.badStateIfNull(dataTypeClassName, message);
    //Object value = Misc.checkAssignmentCompatibility(variableId, variableValue, dataTypeClassName);
    //rootExecution.setVariable(variableId, value);
    
    Misc.checkAssignmentCompatibility(variableId, variableValue, dataTypeClassName);
  }

  public void setProcessInstanceVariable(final ProcessInstanceUUID instanceUUID,
    final String variableId, final Object variableValue) throws InstanceNotFoundException, VariableNotFoundException {
    //final InternalInstance instance = FacadeUtil.getInstance(instanceUUID);
	  ProcessInstance instance = EnvTool.getJournalQueriers().getProcessInstance(instanceUUID);
    setProcessInstanceVariable(instance, instanceUUID, variableId, variableValue);
    EnvTool.getRecorder().recordInstanceVariableUpdated(variableId, variableValue, instance.getUUID(), EnvTool.getUserId());
  }
  
  public void setProcessInstanceWebLabels(ProcessInstanceUUID instanceUUID, Collection labelIds) throws InstanceNotFoundException, RemoteException {
	  ProcessInstance instance = EnvTool.getAllQueriers().getProcessInstance(instanceUUID);
    
    if (instance == null) {
      throw new InstanceNotFoundException("bai_RAPII_20", instanceUUID);
    }
    ((ProcessInstanceImpl)instance).addWebLabels(labelIds);
  }
  
  public void setProcessInstanceWebLabels(Collection instanceUUIDs, Collection labelIds) throws InstanceNotFoundException, RemoteException {
    Set instances = EnvTool.getAllQueriers().getProcessInstances(instanceUUIDs);
    if (instances != null) {
      for (ProcessInstance instance : instances) {
    	  ((ProcessInstanceImpl)instance).addWebLabels(labelIds);
      }
    }
  }

  public void setActivityInstanceVariable(final ActivityInstanceUUID activityUUID,
    final String variableId, final Object variableValue) throws ActivityNotFoundException, VariableNotFoundException {
    final ActivityInstance activity = EnvTool.getAllQueriers().getActivityInstance(activityUUID);
    if (activity == null) {
      throw new ActivityNotFoundException("bai_RAPII_22", activityUUID);
    }
    final ProcessInstanceUUID instanceUUID = activity.getProcessInstanceUUID();
    final String activityId = activity.getActivityName();

    final Repository repository = EnvTool.getRepository();
    InternalExecution execution = repository.getExecutionOnActivity(instanceUUID, activityUUID);
    if (execution == null) {
      throw new ActivityNotFoundException("bai_RAPII_23", activityUUID);
    }
    if (execution.hasExecution(activityId)) {
      execution = (InternalExecution) execution.getExecution(activityId);
    }
    final Recorder recorder = EnvTool.getRecorder();
    ActivityInstance activityInstance = EnvTool.getJournalQueriers().getActivityInstance(activityUUID);
    if (!activityInstance.getLastKnownVariableValues().containsKey(variableId)) {
        throw new VariableNotFoundException("bai_RAPII_24", instanceUUID, activityId, variableId);
      }
    /*
    if (!execution.getScopeVariables().containsKey(variableId)) {
      throw new VariableNotFoundException("bai_RAPII_24", instanceUUID, activityId, variableId);
    }
*/
    final ProcessDefinitionUUID processDefinitionUUID = activity.getProcessDefinitionUUID();
    ActivityDefinition activityDefinition;
    try {
      activityDefinition = new QueryDefinitionAPIImpl().getProcessActivity(
          processDefinitionUUID, activityId, Querier.DEFAULT_KEY);
    } catch (final ProcessNotFoundException e) {
      throw new IllegalStateException(e);
    }

    String dataTypeClassName = null;
    for (final DataFieldDefinition df : activityDefinition.getDataFields()) {
      if (df.getName().equals(variableId)) {
        dataTypeClassName = df.getDataTypeClassName();
      }
    }
    String message = ExceptionManager.getInstance().getFullMessage("bai_RAPII_29", variableId);
    Misc.badStateIfNull(dataTypeClassName, message);
    Object value = Misc.checkAssignmentCompatibility(variableId, variableValue, dataTypeClassName);
    //execution.setVariable(variableId, variableValue);

    // local variable updated -> update only current activity
    recorder.recordActivityVariableUpdated(variableId, value, execution.getCurrentActivityInstanceUUID(), EnvTool.getUserId());
  }

  public void setVariable(final ActivityInstanceUUID activityUUID,
    final String variableId, final Object variableValue) throws ActivityNotFoundException,
    VariableNotFoundException {
    try {
      setActivityInstanceVariable(activityUUID, variableId, variableValue);
    } catch (final VariableNotFoundException e) {
      final ActivityInstance activity = EnvTool.getAllQueriers().getActivityInstance(activityUUID);
      if (activity == null) {
        throw new ActivityNotFoundException("bai_RAPII_25", activityUUID);
      }
      try {
        setProcessInstanceVariable(activity.getProcessInstanceUUID(), variableId, variableValue);
      } catch (final InstanceNotFoundException e1) {
        // If activity exists, the process instance must exist too.
        Misc.unreachableStatement();
      }
    }
  }

  // Checks if the value can be assigned to the variable, throws an exception otherwise.
 /* private Object checkAssignmentCompatibility(final String variableId, final Object variableValue, final String dataTypeClassName, final ProcessDefinitionUUID processUUID) {
    if (variableValue == null) {
      return null;
    }
    try {
      Class< ? > destTypeClass = Class.forName(dataTypeClassName);
      Class< ? > valueClass = variableValue.getClass();
      boolean assignmentOK = valueClass.isAssignableFrom(destTypeClass);
      if (assignmentOK) {
        return variableValue;
      } else {
        String message = ExceptionManager.getInstance().getFullMessage("bai_RAPII_26", variableValue,variableId );
        //try to convert it
        if ("java.lang".equals(valueClass.getPackage().getName()) && "java.lang".equals(destTypeClass.getPackage().getName())) {
          //we can convert it
          String varValueAsString = variableValue.toString();
          if (destTypeClass.equals(String.class.getName())) {
            return new String(varValueAsString);
          } else if (destTypeClass.equals(Boolean.class)) {
            if (varValueAsString.equals("true") || varValueAsString.equals("false")) {
              return Boolean.valueOf(varValueAsString);
            } else {
              throw new BonitaWrapperException(new TypeMismatchException(message));
            }
          } else if (destTypeClass.equals(Character.class.getName())) {
            if (varValueAsString != null && varValueAsString.length() == 1) { 
              return new Character(variableValue.toString().charAt(0));
            } else {
              throw new BonitaWrapperException(new TypeMismatchException(message));
            }
          } else {
            //Short, Long, Double, Float, Integer : all of them have a valueOf(String) methof
            Method valueOf;
            try {
              valueOf = destTypeClass.getMethod("valueOf", new Class< ? >[]{String.class});
              return valueOf.invoke(destTypeClass, new Object[]{varValueAsString});
            } catch (Exception e) {
              throw new BonitaWrapperException(new TypeMismatchException(message));
            }
          }
        } else {
        
          throw new BonitaWrapperException(new TypeMismatchException(message));
        }
        
        //throw new BonitaWrapperException(new TypeMismatchException(message));
      }
    } catch (ClassNotFoundException e) {
      throw new BonitaRuntimeException(e);
    }
  }*/

  public void addComment(final ProcessInstanceUUID instanceUUID, 
      final  ActivityInstanceUUID activityUUID, final String message, final String userId)
   throws InstanceNotFoundException, ActivityNotFoundException {
    final ProcessInstance instance =
      EnvTool.getAllQueriers().getProcessInstance(instanceUUID);
    if (instance == null) {
      throw new InstanceNotFoundException("bai_RAPII_27", instanceUUID);
    }
    if (activityUUID != null) {
      final ActivityInstance activity = EnvTool.getAllQueriers().getActivityInstance(activityUUID);
      if (activity == null) {
        throw new ActivityNotFoundException("bai_RAPII_28", activityUUID);
      }
    }
    CommentImpl comment = new CommentImpl(userId, message, activityUUID);
    ((ProcessInstanceImpl)instance).addComment(comment);
  }

  public void addProcessMetaData(ProcessDefinitionUUID uuid, String key,
      String value) throws ProcessNotFoundException, RemoteException {
    FacadeUtil.checkArgsNotNull(uuid, key, value);
    ProcessDefinition process = EnvTool.getAllQueriers().getProcess(uuid);
    if (process == null) {
      throw new ProcessNotFoundException("bai_RAPII_29", uuid);
    }
    ((ProcessDefinitionImpl)process).addAMetaData(key, value);
  }

  public void deleteProcessMetaData(ProcessDefinitionUUID uuid, String key)
      throws ProcessNotFoundException, RemoteException {
    FacadeUtil.checkArgsNotNull(uuid, key);
    ProcessDefinition process = EnvTool.getAllQueriers().getProcess(uuid);
    if (process == null) {
      throw new ProcessNotFoundException("bai_RAPII_29", uuid);
    }
    ((ProcessDefinitionImpl)process).deleteAMetaData(key);
  }

  public Object evaluateGroovyExpression(String expression, ProcessInstanceUUID instanceUUID)
      throws InstanceNotFoundException, GroovyException {
    Map processVariables = AccessorUtil.getQueryRuntimeAPI().getProcessInstanceVariables(instanceUUID);
    Map allVariables = new HashMap(processVariables);
    return GroovyUtil.evaluate(expression, allVariables, instanceUUID.getProcessDefinitionUUID());
  }

  public Object evaluateGroovyExpression(String expression, ActivityInstanceUUID activityUUID)
      throws InstanceNotFoundException, ActivityNotFoundException, GroovyException {
    Map activityVariables = AccessorUtil.getQueryRuntimeAPI().getActivityInstanceVariables(activityUUID);
    Map processVariables = AccessorUtil.getQueryRuntimeAPI().getProcessInstanceVariables(activityUUID.getProcessInstanceUUID());
    Map allVariables = new HashMap(activityVariables);
    allVariables.putAll(processVariables);
    return GroovyUtil.evaluate(expression, allVariables, activityUUID.getProcessInstanceUUID().getProcessDefinitionUUID());
  }
  
  public void setAttachment(ProcessInstanceUUID instanceUUID, String name, byte[] value) throws RemoteException {
    FacadeUtil.checkArgsNotNull(instanceUUID);
    LargeDataRepository largeDataRepository = EnvTool.getLargeDataRepository();
    largeDataRepository.storeData(Misc.getAttachmentCategories(instanceUUID), name, value, true);
  }
  
  public void setAttachments(ProcessInstanceUUID instanceUUID, Map attachments) throws RemoteException {
    FacadeUtil.checkArgsNotNull(instanceUUID);
    if (attachments != null) {
      LargeDataRepository largeDataRepository = EnvTool.getLargeDataRepository();
      for (Map.Entry attach : attachments.entrySet()) {
        largeDataRepository.storeData(Misc.getAttachmentCategories(instanceUUID), attach.getKey(), attach.getValue(), true);
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy