Package com.scooterframework.web.controller

Source Code of com.scooterframework.web.controller.BaseRequestProcessor

/*
*   This software is distributed under the terms of the FSF
*   Gnu Lesser General Public License (see lgpl.txt).
*
*   This program is distributed WITHOUT ANY WARRANTY. See the
*   GNU General Public License for more details.
*/
package com.scooterframework.web.controller;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.scooterframework.admin.ApplicationConfig;
import com.scooterframework.admin.Constants;
import com.scooterframework.admin.EnvConfig;
import com.scooterframework.admin.FilterManager;
import com.scooterframework.admin.FilterManagerFactory;
import com.scooterframework.autoloader.JavaCompiler;
import com.scooterframework.common.exception.ExecutionException;
import com.scooterframework.common.exception.MethodCreationException;
import com.scooterframework.common.logging.LogUtil;
import com.scooterframework.common.util.CurrentThreadCache;
import com.scooterframework.common.util.CurrentThreadCacheClient;
import com.scooterframework.common.util.StringUtil;
import com.scooterframework.common.util.WordUtil;
import com.scooterframework.orm.sqldataexpress.config.DatabaseConfig;
import com.scooterframework.web.route.NoRouteFoundException;

/**
* <p><strong>BaseRequestProcessor</strong> contains the processing logic that
* the {@link MainActionServlet} performs as it receives each servlet request
* from the container. You can customize the request processing behavior by
* subclassing this class and overriding the method(s) whose behavior you are
* interested in changing.</p>
*
* @author (Fei) John Chen
*/
public class BaseRequestProcessor {
    protected LogUtil log = LogUtil.getLogger(getClass().getName());

    private Map<String, ActionProperties> requestPropertiesMap = new HashMap<String, ActionProperties>();

    public static final String DEFAULT_CONTROLLER_CLASS = "com.scooterframework.builtin.CRUDController";

    public static final String EXECUTION_INTERRUPTED = "EXECUTION_INTERRUPTED";

    /**
     * Constructor
     */
    public BaseRequestProcessor() {
    }

    /**
     * <p>Process an <tt>HttpServletRequest</tt> and create the
     * corresponding <tt>HttpServletResponse</tt> or dispatch
     * to another resource.</p>
     *
     * @param request The servlet request we are processing
     * @param response The servlet response we are creating
     * @throws java.io.IOException
     * @throws javax.servlet.ServletException
     */
    public void process(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException
    {
        if (log.isDebugEnabled()) displayHttpRequest(request);

        try {
            processLocale(request, response);

            String requestPath = CurrentThreadCacheClient.requestPath();

            if (!isAdminRequest(requestPath) && JavaCompiler.hasCompileErrors()) {
              processCompileError(request, response);
              return;
            }

            if (isRootAccess(requestPath)) {
              processRootAccess(request, response);
            }
            else {
              String result = null;

              String requstPathKey = CurrentThreadCacheClient.requestPathKey();
              ActionProperties aps = requestPropertiesMap.get(requstPathKey);

              if (aps == null || ApplicationConfig.getInstance().isInDevelopmentEnvironment()) {
                    String requestHttpMethod = CurrentThreadCacheClient.httpMethod();
                    aps = prepareActionProperties(requestPath, requestHttpMethod, request);
                    registerActionProperties(request, aps);
                    requestPropertiesMap.put(requstPathKey, aps);
              }
              else {
                registerActionProperties(request, aps);
              }
                log.debug("aps: " + aps);

                result = executeRequest(aps, request, response);
                log.debug("execution result: " + result);

                if (result != null) {
                    processNotNullResult(request, response, aps, result);
                }
                else {
                    processNullResult(request, response, aps);
                }
            }
        }
        catch(Exception ex) {
            processException(request, response, ex);
        }
    }

    private boolean isAdminRequest(String requestPath) {
      return (requestPath != null && requestPath.toLowerCase().startsWith("/admin"));
    }

    /**
     * <p>Process an <tt>HttpServletRequest</tt>.</p>
     *
     * @param aps properties of request
     * @param request The servlet request we are processing
     * @param response The servlet response we are creating
     * @return execution result
     * @throws java.io.IOException
     * @throws javax.servlet.ServletException
     */
    public String executeRequest(ActionProperties aps,
                HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException
    {
      Object controllerInstance = null;

      if (aps.controllerCreated) {
        controllerInstance = aps.controllerInstance;
      }
      else {
        controllerInstance = getControllerInstance(aps.controllerClassName);
        aps.controllerInstance = controllerInstance;
        aps.controllerCreated = true;
      }

        if (controllerInstance == null) {
            if (EnvConfig.getInstance().allowForwardToControllerNameViewWhenControllerNotExist()) {
                log.info("Controller instance for \"" + aps.controller +
                    "\" does not exist, forward to view \"" + aps.controller +
                    File.separator + aps.action + "\".");
                return null;
            }
            else {
                throw new NoControllerFoundException(aps.controllerClassName);
            }
        }

        Method actionInstance = null;
        if (aps.methodCreated) {
          actionInstance = aps.methodInstance;
        }
        else {
          actionInstance = getActionMethod(controllerInstance.getClass(), aps.action);
          aps.methodInstance = actionInstance;
          aps.methodCreated = true;
        }

        if (actionInstance == null) {
            if (EnvConfig.getInstance().allowForwardToActionNameViewWhenActionNotExist()) {
                log.debug("Action method \"" + aps.action +
                    "\", forward to view \"" + aps.action + "\".");
                return null;
            }
            else {
                throw new MethodCreationException(controllerInstance.getClass().getName(), aps.action);
            }
        }

        return executeControllerAction(controllerInstance, actionInstance);
    }

    /**
     * Sets up action properties for the action execution. The properties are
     * wrapped up in an <tt>ActionProperties</tt> instance.
     *
     * @param request The servlet request we are processing
     * @return an ActionProperties instance
     */
  public ActionProperties prepareActionProperties(String requestPath,
      String requestHttpMethod, HttpServletRequest request) {
        String path = requestPath;
        String controllerPath = null;
        String controller = null;
        String action = null;
        String format = null;

        int lastDot = path.lastIndexOf(".");
        int lastSlash = path.lastIndexOf("/");

        if (lastDot != -1 && lastDot > lastSlash) {
            format = path.substring(lastDot + 1);
            if (EnvConfig.getInstance().hasMimeTypeFor(format)) {
                path = path.substring(0, lastDot);
            }
            else {
              format = null;
            }
        }

        if (path.endsWith("/")) path = path.substring(0, path.length() - 1);

        lastSlash = path.lastIndexOf("/");
        if (lastSlash > 0) {
            action = path.substring(lastSlash + 1);
            controllerPath = path.substring(0, lastSlash);
            lastSlash = controllerPath.lastIndexOf("/");
            if (lastSlash != -1) {
                controller = controllerPath.substring(lastSlash + 1);
            }
        }
        else if (lastSlash == 0) {
            controllerPath = path;
            controller = path.substring(1);
        }

        if (action == null || "".equals(action)) {
            if (EnvConfig.getInstance().allowDefaultActionMethod()) {
                action = EnvConfig.getInstance().getDefaultActionMethod();
            }
            else {
                throw new IllegalArgumentException("The value for action " +
                "is not detected from the request path \"" + requestPath +
                "\" and the default action method is not allowed in property file.");
            }
        }

        ActionProperties aps = new ActionProperties();
        aps.controllerPath = controllerPath;
        aps.controller = controller;
        aps.controllerClassName = getControllerClassName(controllerPath);
        aps.action = action;
        aps.model = (DatabaseConfig.getInstance().usePluralTableName())?WordUtil.singularize(controller):controller;
        aps.format = format;

        return aps;
    }

    /**
     * Puts some action properties in <tt>request</tt> object.
     */
    protected void registerActionProperties(HttpServletRequest request, ActionProperties aps) {
      CurrentThreadCacheClient.cacheController(aps.controller);
        CurrentThreadCacheClient.cacheControllerClass(aps.controllerClassName);
        CurrentThreadCacheClient.cacheControllerPath(aps.controllerPath);
        CurrentThreadCacheClient.cacheAction(aps.action);
        CurrentThreadCacheClient.cacheModel(aps.model);
        CurrentThreadCacheClient.cacheFormat(aps.format);
        request.setAttribute(Constants.CONTROLLER, aps.controller);
        request.setAttribute(Constants.CONTROLLER_CLASS, aps.controllerClassName);
        request.setAttribute(Constants.CONTROLLER_PATH, aps.controllerPath);
        request.setAttribute(Constants.ACTION, aps.action);
        request.setAttribute(Constants.MODEL, aps.model);
        request.setAttribute(Constants.FORMAT, aps.format);
    }

    /**
     * Checks if a request is local. Subclass can override this method if a
     * different logic is used to determine local request.
     *
     * @param request HttpServletRequest
     * @return true if the request is from a localhost
     */
    protected boolean isLocalRequest(HttpServletRequest request) {
        String s = (String)CurrentThreadCache.get(Constants.LOCAL_REQUEST);
        if ((Constants.VALUE_FOR_LOCAL_REQUEST).equals(s)) {
            return true;
        }
        return false;
    }

    protected void processLocale(HttpServletRequest request,
            HttpServletResponse response) {
      //use the requested locale
      Locale locale = request.getLocale();

    if (locale == null) {
      locale = ACH.getAC().getLocale(ActionContext.SCOPE_SESSION);
      if (locale != null) {
        //there is no need to do anything if a locale has been chosen.
        return;
      }
      else {
        //use the configured locale
        locale = ActionContext.getGlobalLocale();
        if (locale == null) {
          locale = Locale.getDefault();
        }
        ACH.getAC().setLocale(locale, ActionContext.SCOPE_SESSION);
      }
    }
    else {
      Locale sessionLocale = ACH.getAC().getLocale(ActionContext.SCOPE_SESSION);
      if (sessionLocale == null || !sessionLocale.equals(locale)) {
        ACH.getAC().setLocale(locale, ActionContext.SCOPE_SESSION);
      }
    }

    log.debug("User locale is '" + locale + "'.");
  }

    /**
     * Returns a controller class name.
     *
     * @param controllerPath controller path
     * @return controller class name
     */
    protected String getControllerClassName(String controllerPath) {
        String controllerClassName = EnvConfig.getInstance().getControllerClassName(controllerPath);
        return controllerClassName;
    }

    /**
     * Returns a controller instance.
     *
     * @param controllerClassName controller class name
     * @return controller instance
     */
    protected Object getControllerInstance(String controllerClassName) {
        return ControllerFactory.createController(controllerClassName, getDefaultControllerClassName());
    }

    /**
     * Returns class name of default controller. This default controller is used
     * when application specific controller is not available and the
     * <tt>auto.crud</tt> property is set to true in the environment.properties
     * file. Subclass must override this method if a different default
     * controller class is used.
     */
    protected String getDefaultControllerClassName() {
        return DEFAULT_CONTROLLER_CLASS;
    }

    /**
     * Returns a method instance related to an action of a controller.
     *
     * @param controllerClass a controller class type
     * @param actionName name of the action method
     * @return the method instance
     */
    protected Method getActionMethod(Class<?> controllerClass, String actionName) {
        if (controllerClass == null || actionName == null) return null;

        Method method = null;
        try {
            method = ControllerFactory.getMethod(controllerClass, actionName);
        }
        catch(Exception ex) {
            log.debug("Failed to create action method instance: " + ex.getMessage());
        }
        return method;
    }

    /**
     * Invokes an action method of a controller.
     *
     * @param controller The controller instance to be invoked
     * @param method The action method
     * @return execution result
     */
    protected String executeControllerAction(Object controller, Method method) {
        if (controller == null || method == null) return null;

        String result = null;
        try {
            boolean beforeIsSuccess = true;
            FilterManager filterManager = FilterManagerFactory.getInstance().getFilterManager(controller.getClass());
            if (filterManager != null && !filterManager.noFilterDeclared()) {
              result = filterManager.executeBeforeFiltersOn(method.getName());
                if (result != null) beforeIsSuccess = false;
            }

            if (beforeIsSuccess) {
                result = (String)method.invoke(controller, (Object[])null);
            }

            if (beforeIsSuccess && filterManager != null && !filterManager.noFilterDeclared()) {
                String afResult = filterManager.executeAfterFiltersOn(method.getName());
                if (afResult != null) {
                    result = afResult;
                }
            }
        } catch (Exception ex) {
      log.error("Error in executeControllerAction controller/action: " + controller + "/" + method, ex);
            ExecutionException eex =
                new ExecutionException(controller.getClass().getName(), method.getName(), null, ex);
            throw eex;
        }

        return result;
    }

    /**
     * <p>Processes not-null result of an action method. "not-null" result is a
     * result string tagged by one of the supporting tags. All supported tags
     * are documented in ActionResult.</p>
     *
     * <pre>
     * Examples of not-null results:
     *
     * Forward result to a view:
     *   forwardTo=>/WEB-INF/views/jsp/sayit.jsp
     *
     * Display result in html format:
     *   html=><h1>Good morning</h1>
     *
     * Return xml formatted document:
     *   xml=><?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><book><title>Java Programming</title><price>$50</price></book>
     *
     * Return plain-text document:
     *   text=>This is a small world.
     * </pre>
     *
     * @param request HTTP servlet request
     * @param response HTTP servlet response
     * @param aps properties of request
     * @param result tagged result of an action
     * @throws java.io.IOException
     * @throws javax.servlet.ServletException
     */
    protected void processNotNullResult(HttpServletRequest request,
                                        HttpServletResponse response,
                                        ActionProperties aps,
                                        String result)
    throws IOException, ServletException
    {
      if (hasRendered(request)) {
        return;
      }

        if (ActionResult.checkResultTag(result, ActionResult.TAG_REDIRECT_TO)) {
            processResultRedirect(request, response, result);
        }
        else if (ActionResult.checkResultTag(result, ActionResult.TAG_FORWARD_TO)) {
            processResultForward(request, response, result);
        }
        else if (ActionResult.checkResultTag(result, ActionResult.TAG_ERROR)) {
            processResultError(request, response, result);
        }
        else {
          if (ActionResult.startsWithContentTypeTag(result)) {
              String tag = ActionResult.getContentTypeTag(result);
              String content = ActionResult.getResultContentByTag(result, tag);
              processResultContentForRequestFormatType(request, response, content, tag);
          }
          else {
            String tag = (aps.format != null)?aps.format:Constants.DEFAULT_RESPONSE_FORMAT;
            processResultContentForRequestFormatType(request, response, result, tag);
          }
        }
    }

    protected void processResultContentForRequestFormatType(
        HttpServletRequest request,
      HttpServletResponse response,
      String content,
      String format)
  throws IOException, ServletException
  {
        ContentHandler handler = ContentHandlerFactory.getContentHandler(format);
        if (handler != null) {
          handler.handle(request, response, content, format);
        }
        else {
      throw new IllegalArgumentException(
          "There is no handler found for format \""
              + format
              + "\". You may create your own as a plugin by "
              + "extending the Plugin class and "
              + "implementing the ContentHandler interface.");
        }
  }

    /**
     * Processes error result.
     *
     * @param request HTTP servlet request
     * @param response HTTP servlet response
     * @param result
     * @throws java.io.IOException
     * @throws javax.servlet.ServletException
     */
    protected void processResultError(HttpServletRequest request, HttpServletResponse response, String result)
    throws IOException, ServletException {
        String message = ActionResult.getResultContentByTag(result, ActionResult.TAG_ERROR);
        processError(request, response, message);
    }

    /**
     * Processes redirect result.
     *
     * @param request HTTP servlet request
     * @param response HTTP servlet response
     * @param result result of action
     * @throws java.io.IOException
     * @throws javax.servlet.ServletException
     */
    protected void processResultRedirect(HttpServletRequest request, HttpServletResponse response, String result)
    throws IOException, ServletException {
        String target = ActionResult.getResultContentByTag(result, ActionResult.TAG_REDIRECT_TO);

        if (target.startsWith("/")) {
            String contextPath = request.getContextPath();
            if (!target.startsWith(contextPath)) target = contextPath + target;
        }
        response.sendRedirect(response.encodeRedirectURL(target));
    }

    /**
     * Processes forward result.
     *
     * Forwards to a URI denoted by the <tt>result</tt>.
     *
     * @param request HTTP servlet request
     * @param response HTTP servlet response
     * @param result result of action
     * @throws java.io.IOException
     * @throws javax.servlet.ServletException
     */
    protected void processResultForward(HttpServletRequest request, HttpServletResponse response, String result)
    throws IOException, ServletException {
        String target = ActionResult.getResultContentByTag(result, ActionResult.TAG_FORWARD_TO);
        doForward(target, request, response);
    }

    /**
     * Processes null result.
     *
     * Forwards to a default URI derived based on controller and action names.
     * See <tt>getDefaultViewUri</tt> method.
     *
     * @param request HTTP servlet request
     * @param response HTTP servlet response
     * @param aps properties of request
     * @throws java.io.IOException
     * @throws javax.servlet.ServletException
     */
    protected void processNullResult(HttpServletRequest request,
                     HttpServletResponse response,
                     ActionProperties aps)
    throws IOException, ServletException {
      if (hasRendered(request)) return;

        String target = getViewURI(StringUtil.toLowerCase(aps.controller), StringUtil.toLowerCase(aps.action));
        doForward(target, request, response);
    }

    private boolean hasRendered(HttpServletRequest request) {
      return (request.getAttribute(Constants.REQUEST_RENDERED) != null)?true:false;
    }

    /**
     * Processes root access.
     *
     * Forwards to a default URI derived based on controller and action names.
     * See <tt>getDefaultViewUri</tt> method.
     *
     * @param request HTTP servlet request
     * @param response HTTP servlet response
     * @throws java.io.IOException
     * @throws javax.servlet.ServletException
     */
    protected void processRootAccess(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException {
        String target = EnvConfig.getInstance().getRootURL();
        doForward(target, request, response);
    }

    /**
     * Processes an error message.
     *
     * @param request HTTP servlet request
     * @param response HTTP servlet response
     * @param error an error message
     * @throws java.io.IOException
     * @throws javax.servlet.ServletException
     */
    protected void processError(HttpServletRequest request, HttpServletResponse response, String error)
    throws IOException, ServletException {
        String message = "Error in \"" + CurrentThreadCache.get(Constants.REQUEST_PATH) + "\": " + error;
        log.error(message);
        CurrentThreadCache.set(Constants.ERROR_MESSAGE, message);
        doForwardToErrorPage(request, response);
    }

    /**
     * Processes a compile error message.
     *
     * @param request HTTP servlet request
     * @param response HTTP servlet response
     * @throws java.io.IOException
     * @throws javax.servlet.ServletException
     */
    protected void processCompileError(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException {
        doForwardToCompileErrorPage(request, response);
    }

    /**
     * Processes an exception.
     *
     * @param request HTTP servlet request
     * @param response HTTP servlet response
     * @param ex
     * @throws java.io.IOException
     * @throws javax.servlet.ServletException
     */
    protected void processException(HttpServletRequest request, HttpServletResponse response, Exception ex)
    throws IOException, ServletException {
        String message = "";
        if (ex instanceof NoRouteFoundException) {
            message = ex.getMessage();
        }
        else {
            message = "Error in \"" + CurrentThreadCache.get(Constants.REQUEST_PATH) + "\": " + ex.toString();
        }

        log.error(message);

        if (!interpretException(ex)) {
          ex.printStackTrace();
        }

        CurrentThreadCache.set(Constants.ERROR_EXCEPTION, ex);
        CurrentThreadCache.set(Constants.ERROR_MESSAGE, message);

        //response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, message);
        doForwardToErrorPage(request, response);
    }

    protected boolean interpretException(Exception ex)  {
      boolean understand = false;
      String error = ex.getMessage();
      if (error == null || "".equals(error)) return false;

      String meaning = "";
      if (error.indexOf("Connections could not be acquired from the underlying database!") != -1) {
        meaning = "Please verify database connection setup or existence of jdbc library for the database.";
        log.error(meaning);
        ActionControl.flash("error", meaning);
        understand = true;
      }
      else if (ex instanceof NoRouteFoundException) {
        meaning = "Please verify either the route is missing or a view path is setup correctly.";
        log.error(meaning);
        ActionControl.flash("error", meaning);
        understand = true;
      }
      else if (ex instanceof NoTemplateHandlerException) {
        meaning = "Please add a template handler for template type \"" +
          ((NoTemplateHandlerException)ex).getTemplateType() + "\".";
        log.error(meaning);
        ActionControl.flash("error", meaning);
        understand = true;
      }
      else if (ex instanceof NoViewFileException) {
        meaning = "Please add a view file for view \"" +
          ((NoViewFileException)ex).getTargetView() + "\".";
        log.error(meaning);
        ActionControl.flash("error", meaning);
        understand = true;
      }
      return understand;
    }

    /**
     * Returns a default view URI. This URI is actually a real view file
     * associated with the controller and the action method name.
     *
     * @param controller the name of the controller
     * @param action the action method
     * @return view URI.
     */
    protected String getViewURI(String controller, String action) {
        String uri = EnvConfig.getViewURI(controller, action, getDefaultViewFilesDirectoryName());
        return uri;
    }

    /**
     * Returns default view file directory name.
     *
     * @return default view file directory name.
     */
    protected String getDefaultViewFilesDirectoryName() {
        return (EnvConfig.getInstance().allowAutoCRUD())?
                EnvConfig.getInstance().getDefaultViewFilesDirectory():null;
    }

    protected void doForwardToErrorPage(
        HttpServletRequest request,
        HttpServletResponse response)
        throws IOException, ServletException {
        doForward(EnvConfig.getInstance().getErrorPageURI(), request, response);
    }

    protected void doForwardToCompileErrorPage(
        HttpServletRequest request,
        HttpServletResponse response)
        throws IOException, ServletException {
        doForward(EnvConfig.getInstance().getCompileErrorPageURI(), request, response);
    }

    /**
     * <p>Do a forward to specified URI using a <tt>RequestDispatcher</tt>.
     * This method is used by all internal method needing to do a forward.</p>
     *
     * @param uri Context-relative URI to forward to
     * @param request HTTP servlet request
     * @param response HTTP servlet response
     * @throws java.io.IOException
     * @throws javax.servlet.ServletException
     */
    protected void doForward(
        String uri,
        HttpServletRequest request,
        HttpServletResponse response)
        throws IOException, ServletException {
        ActionControl.doForward(uri, request, response);
    }

    /**
     * Displays all parameter names and theirs values in a HTTP request.
     *
     * @param request
     */
    public void displayHttpRequest(HttpServletRequest request) {
        if (request != null) {
            @SuppressWarnings("unchecked")
      Enumeration<String> en = request.getParameterNames();
            while(en.hasMoreElements()) {
                String key = en.nextElement();
                String[] values = request.getParameterValues(key);
                if (values != null) {
                    String tmp = "";

                    int length = values.length;
                    for (int i = 0; i < length - 1; i++) {
                        tmp += values[i] + ", ";
                    }

                    tmp += values[length-1];

                    if ("password".equalsIgnoreCase(key)) tmp = "********";
                    log.debug("name=["+key+"] values=["+tmp+"]");
                }
            }
        }
    }

    /**
     * Returns ServletContext
     */
    protected ServletContext getServletContext() {
        return ACH.getWAC().getHttpServletRequest().getSession().getServletContext();
    }

    /**
     * Returns RealPath
     */
    protected String getRealPath() {
        return getServletContext().getRealPath("");
    }

    /**
     * Checks if a request path is a root access. A root access means the
     * <tt>requestPath</tt> is either "/" or starts with "<tt>/index</tt>".
     * @param requestPath
     * @return true if the request path is root path.
     */
    protected boolean isRootAccess(String requestPath) {
        boolean rootAccess = false;
        if ("".equals(requestPath) ||
          "/".equals(requestPath) || requestPath.startsWith("/index")) {
            rootAccess = true;
        }
        return rootAccess;
    }
}
TOP

Related Classes of com.scooterframework.web.controller.BaseRequestProcessor

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.