Package org.jdesktop.wonderland.client.jme.cellrenderer

Source Code of org.jdesktop.wonderland.client.jme.cellrenderer.BasicRenderer$CollisionListener

/**
* Open Wonderland
*
* Copyright (c) 2011, Open Wonderland Foundation, All Rights Reserved
*
* Redistributions in source code form must reproduce the above
* copyright and this condition.
*
* The contents of this file are subject to the GNU General Public
* License, Version 2 (the "License"); you may not use this file
* except in compliance with the License. A copy of the License is
* available at http://www.opensource.org/licenses/gpl-license.php.
*
* The Open Wonderland Foundation designates this particular file as
* subject to the "Classpath" exception as provided by the Open Wonderland
* Foundation in the License file that accompanied this code.
*/

/**
* Project Wonderland
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., All Rights Reserved
*
* Redistributions in source code form must reproduce the above
* copyright and this condition.
*
* The contents of this file are subject to the GNU General Public
* License, Version 2 (the "License"); you may not use this file
* except in compliance with the License. A copy of the License is
* available at http://www.opensource.org/licenses/gpl-license.php.
*
* Sun designates this particular file as subject to the "Classpath"
* exception as provided by Sun in the License file that accompanied
* this code.
*/
package org.jdesktop.wonderland.client.jme.cellrenderer;

import com.jme.math.Quaternion;
import com.jme.math.Vector3f;
import com.jme.scene.Node;
import com.jme.scene.Spatial;
import com.jme.scene.state.CullState;
import com.jme.scene.state.RenderState;
import com.jme.scene.state.ZBufferState;
import com.jme.util.resource.ResourceLocator;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jdesktop.mtgame.CollisionComponent;
import org.jdesktop.mtgame.CollisionSystem;
import org.jdesktop.wonderland.client.cell.Cell;
import org.jdesktop.mtgame.Entity;
import org.jdesktop.mtgame.JBulletCollisionComponent;
import org.jdesktop.mtgame.JBulletDynamicCollisionSystem;
import org.jdesktop.mtgame.JBulletPhysicsComponent;
import org.jdesktop.mtgame.JBulletPhysicsSystem;
import org.jdesktop.mtgame.JMECollisionSystem;
import org.jdesktop.mtgame.PhysicsComponent;
import org.jdesktop.mtgame.PhysicsSystem;
import org.jdesktop.mtgame.PostEventCondition;
import org.jdesktop.mtgame.ProcessorArmingCollection;
import org.jdesktop.mtgame.ProcessorComponent;
import org.jdesktop.mtgame.RenderComponent;
import org.jdesktop.mtgame.RenderUpdater;
import org.jdesktop.mtgame.WorldManager;
import org.jdesktop.wonderland.client.ClientContext;
import org.jdesktop.wonderland.client.cell.CellComponent;
import org.jdesktop.wonderland.client.cell.component.CellPhysicsPropertiesComponent;
import org.jdesktop.wonderland.client.cell.CellRenderer;
import org.jdesktop.wonderland.client.cell.ComponentChangeListener;
import org.jdesktop.wonderland.client.cell.InteractionComponent;
import org.jdesktop.wonderland.client.cell.InteractionComponent.InteractionComponentListener;
import org.jdesktop.wonderland.client.cell.MovableComponent;
import org.jdesktop.wonderland.client.cell.asset.AssetUtils;
import org.jdesktop.wonderland.client.comms.WonderlandSession;
import org.jdesktop.wonderland.client.jme.CellRefComponent;
import org.jdesktop.wonderland.client.jme.ClientContextJME;
import org.jdesktop.wonderland.client.jme.utils.traverser.ProcessNodeInterface;
import org.jdesktop.wonderland.client.jme.utils.traverser.TreeScan;
import org.jdesktop.wonderland.common.ExperimentalAPI;
import org.jdesktop.wonderland.common.cell.CellStatus;
import org.jdesktop.wonderland.common.cell.CellTransform;
import org.jdesktop.wonderland.common.cell.component.state.PhysicsProperties;

/**
*
* Abstract Renderer class that implements CellRendererJME
*
* @author paulby
*/
@ExperimentalAPI
public abstract class BasicRenderer implements CellRendererJME {
   
    protected static Logger logger = Logger.getLogger(BasicRenderer.class.getName());
    protected Cell cell;
    protected Entity entity;
    private Object entityLock=new Object();
    protected Node rootNode;
    protected Node sceneRoot;
    protected MoveProcessor moveProcessor = null;
   
    private static ZBufferState zbuf = null;

    private CellStatus status = CellStatus.DISK;

    private boolean collisionEnabled = true;
    private boolean pickingEnabled = true;
    private boolean lightingEnabled = true;
    private boolean backfaceCullingEnabled = true;

    private final CollisionListener collisionListener;

    static {
        zbuf = (ZBufferState) ClientContextJME.getWorldManager().getRenderManager().createRendererState(RenderState.RS_ZBUFFER);
        zbuf.setEnabled(true);
        zbuf.setFunction(ZBufferState.TestFunction.LessThanOrEqualTo);
    }
   
    public BasicRenderer(Cell cell) {
        this.cell = cell;

        // update the collidable property of this renderer based on the
        // cell's interaction component
        this.collisionListener = new CollisionListener(cell);
    }

    /**
     * Return the cell that contains this component
     * @return
     */
    public Cell getCell() {
        return cell;
    }

    public CellStatus getStatus() {
        return status;
    }

    public void setStatus(CellStatus status,boolean increasing) {
        this.status = status;
        switch(status) {
            case ACTIVE :
                if (increasing && cell!=null) {
                    Entity parentEntity= findParentEntity(cell.getParent());
                    Entity thisEntity = getEntity();

                    if (thisEntity==null) {
                        logger.severe("Got null entity for "+this.getClass().getName());
                        return;
                    }

                    thisEntity.addComponent(CellRefComponent.class, new CellRefComponent(cell));

                    if (parentEntity!=null) {
                        parentEntity.addEntity(thisEntity);
                    } else {
                        ClientContextJME.getWorldManager().addEntity(thisEntity);
                    }

                    // Figure out the correct parent entity for this cells entity.
                    if (parentEntity!=null && thisEntity!=null) {
                        RenderComponent parentRendComp = (RenderComponent) parentEntity.getComponent(RenderComponent.class);
                        RenderComponent thisRendComp = (RenderComponent)thisEntity.getComponent(RenderComponent.class);
                        if (parentRendComp!=null && parentRendComp.getSceneRoot()!=null && thisRendComp!=null) {
                            thisRendComp.setAttachPoint(parentRendComp.getSceneRoot());
                        }
                    }

                    // enable the collision listener
                    collisionListener.enable();
                } else {
                    logger.info("No Entity for Cell "+cell.getClass().getName());
                }
            break;
            case INACTIVE :
                if (!increasing) {
                    collisionListener.disable();

                    try {
                        Entity child = getEntity();
                        Entity parent = child.getParent();
                        if (parent!=null)
                            parent.removeEntity(child);
                        else
                            ClientContextJME.getWorldManager().removeEntity(child);
                        cleanupSceneGraph(child);
                    } catch(Exception e) {
                        System.err.println("NPE in "+this);
                        e.printStackTrace();
                    }
                }
                break;
        }

    }

    /**
     * Convenience method which attaches the child entity to the specified
     * parent AND sets the attachpoint of the childs RenderComponent to the
     * scene root of the parents RenderComponent
     *
     * @param parentEntity
     * @param child
     */
    public static void entityAddChild(Entity parentEntity, Entity child) {
        if (parentEntity!=null && child!=null) {
            RenderComponent parentRendComp = (RenderComponent) parentEntity.getComponent(RenderComponent.class);
            RenderComponent thisRendComp = (RenderComponent)child.getComponent(RenderComponent.class);
            if (parentRendComp!=null && parentRendComp.getSceneRoot()!=null && thisRendComp!=null) {
                thisRendComp.setAttachPoint(parentRendComp.getSceneRoot());
            }
            parentEntity.addEntity(child);
        }
    }

    /**
     * Traverse up the cell hierarchy and return the first Entity
     * @param cell
     * @return
     */
    private Entity findParentEntity(Cell cell) {
        if (cell==null)
            return null;

        CellRenderer rend = cell.getCellRenderer(ClientContext.getRendererType());
        if (cell!=null && rend!=null) {
            if (rend instanceof CellRendererJME) {
//                    System.out.println("FOUND PARENT ENTITY on CELL "+cell.getName());
                return ((CellRendererJME)rend).getEntity();
            }
        }

        return findParentEntity(cell.getParent());
    }

    protected Entity createEntity() {
        Entity ret = new Entity(this.getClass().getName()+"_"+cell.getCellID());

        rootNode = new Node();
        rootNode.setName("CellRoot_"+cell.getCellID());
        sceneRoot = createSceneGraph(ret);
        rootNode.attachChild(sceneRoot);
        applyTransform(rootNode, cell.getLocalTransform());
        addRenderState(rootNode);

        addDefaultComponents(ret, rootNode);

        return ret;       
    }

    /**
     * Return the scene root, this is the node created by createSceneGraph.
     * The BasicRenderer also has a rootNode which contains the cell transform,
     * the rootNode is the parent of the scene root.
     * @return
     */
    public Node getSceneRoot() {
        return sceneRoot;
    }
   
    /**
     * Add the default renderstate to the root node. Override this method
     * if you want to apply a different RenderState
     * @param node
     */
    protected void addRenderState(Node node) {
        node.setRenderState(zbuf);
    }
   
    protected void addDefaultComponents(Entity entity, Node rootNode) {
        if (cell.getComponent(MovableComponent.class)!=null) {
            if (rootNode==null) {
                logger.warning("Cell is movable, but has no root node !");
            } else {          
                // The cell is movable so create a move processor
                moveProcessor = new MoveProcessor(ClientContextJME.getWorldManager(), rootNode);
                entity.addComponent(MoveProcessor.class, moveProcessor);
            }
        }

        if (rootNode!=null) {
            rootNode.updateWorldBound();

            // Some subclasses (like the imi collada renderer) already add
            // a render component
            RenderComponent rc = entity.getComponent(RenderComponent.class);
            if (rc==null) {
                rc = ClientContextJME.getWorldManager().getRenderManager().createRenderComponent(rootNode);
                entity.addComponent(RenderComponent.class, rc);
            } else {
                rc.setSceneRoot(rootNode);
            }

            // TODO: shouldn't this just be a call to adjustCollisionSystem()?
            WonderlandSession session = cell.getCellCache().getSession();
            CollisionSystem collisionSystem = ClientContextJME.getCollisionSystem(session.getSessionManager(), "Default");


            CollisionComponent cc=null;

            cc = setupCollision(collisionSystem, rootNode);
            if (cc!=null) {
                entity.addComponent(CollisionComponent.class, cc);
            }

            // set initial lighting
            adjustLighting(entity);

//            PhysicsSystem jBulletPhysicsSystem = ClientContextJME.getPhysicsSystem(session.getSessionManager(), "Physics");
//            CollisionSystem jBulletCollisionSystem = ClientContextJME.getCollisionSystem(session.getSessionManager(), "Physics");
//            if (jBulletPhysicsSystem!=null) {
//                CollisionComponent jBulletCollisionComponent = setupPhysicsCollision(jBulletCollisionSystem, rootNode);
//                PhysicsComponent pc = setupPhysics(jBulletCollisionComponent, jBulletPhysicsSystem, rootNode);
//                entity.addComponent(JBulletCollisionComponent.class, jBulletCollisionComponent);
//                entity.addComponent(JBulletPhysicsComponent.class, pc);
//            }
        } else {
            logger.warning("**** BASIC RENDERER - ROOT NODE WAS NULL !");
        }

    }


    protected CollisionComponent setupCollision(CollisionSystem collisionSystem, Node rootNode) {
        CollisionComponent cc=null;
        if (collisionSystem instanceof JMECollisionSystem) {
            cc = ((JMECollisionSystem)collisionSystem).createCollisionComponent(rootNode);
            cc.setCollidable(collisionEnabled);
            cc.setPickable(pickingEnabled);
        } else {
            logger.warning("Unsupported CollisionSystem "+collisionSystem);
        }

        return cc;
    }

    protected PhysicsComponent setupPhysics(CollisionComponent physicsCC, PhysicsSystem physicsSystem, Node rootNode) {
        if (physicsCC!=null && physicsCC instanceof JBulletCollisionComponent && physicsSystem instanceof JBulletPhysicsSystem) {
            JBulletPhysicsComponent pc = ((JBulletPhysicsSystem)physicsSystem).createPhysicsComponent((JBulletCollisionComponent)physicsCC);
            CellPhysicsPropertiesComponent prop = cell.getComponent(CellPhysicsPropertiesComponent.class);
            if (prop!=null) {
                PhysicsProperties phy = prop.getPhysicsProperties(CellPhysicsPropertiesComponent.DEFAULT_NAME);
                if (phy!=null) {
                    System.err.println("---------------> setting mass on "+this);
                    pc.setMass(phy.getMass());
                }
            }
            return pc;
        } else {
            logger.warning("Unsupported PhysicsSystem "+physicsSystem);
        }

        return null;
    }

    protected CollisionComponent setupPhysicsCollision(CollisionSystem collisionSystem, Node rootNode) {
        CollisionComponent cc=null;
        if (collisionSystem instanceof JBulletDynamicCollisionSystem) {
            cc = ((JBulletDynamicCollisionSystem)collisionSystem).createCollisionComponent(rootNode);
        } else {
            logger.warning("Unsupported CollisionSystem "+collisionSystem);
        }

        return cc;
    }

    /**
     * Create the scene graph. The node returned will have  default
     * components set to handle collision and rendering. The returned graph will
     * also automatically be positioned correctly with the cells transform. This
     * is achieved by adding the returned Node to a rootNode for this renderer which
     * automatically tracks the cells transform.
     * @return
     */
    protected abstract Node createSceneGraph(Entity entity);

    /**
     * Cleanup the scene graph, allowing resources to be gc'ed
     * TODO - should be abstract, but don't want to break compatability in 0.5 API
     *
     * @param entity
     */
    protected void cleanupSceneGraph(Entity entity) {

    }

    /**
     * Apply the transform to the jme node
     * @param node
     * @param transform
     */
    public static void applyTransform(Spatial node, CellTransform transform) {
        node.setLocalRotation(transform.getRotation(null));
        node.setLocalScale(transform.getScaling(null));
        node.setLocalTranslation(transform.getTranslation(null));
    }

    /**
     * Return the entity for this basic renderer. The first time this
     * method is called the entity will be created using createEntity()
     * @return
     */
    public Entity getEntity() {
        synchronized(entityLock) {
            logger.fine("Get Entity "+this.getClass().getName());
            if (entity==null)
                entity = createEntity();
        }
        return entity;
    }

    /**
     * Callback notifying the renderer that the cell transform has changed.
     * @param localTransform the new local transform of the cell
     */
    public void cellTransformUpdate(CellTransform localTransform) {
        // The fast-path case is if the move processor already exists, in
        // which case, we move the cell
        if (moveProcessor != null) {
            moveProcessor.cellMoved(localTransform);
            return;
        }

        // Otherwise, the move processor is null so we will attempt to add it
        // but only if there is a movable component on the cell.
        if (cell.getComponent(MovableComponent.class) != null && rootNode != null) {
            moveProcessor = new MoveProcessor(ClientContextJME.getWorldManager(), rootNode);
            getEntity().addComponent(MoveProcessor.class, moveProcessor);
            moveProcessor.cellMoved(localTransform);
        }
    }

    /**
     * Given a url, determine and return the full asset URL. This is a
     * convenience method that invokes methods on AssetUtils using the session
     * associated with the cell for this cell renderer
     *
     * @param uri The asset URI
     * @return A URL representing the uri
     * @throws MalformedURLException Upon error forming the URL
     */
    protected URL getAssetURL(String uri) throws MalformedURLException {
        return AssetUtils.getAssetURL(uri, cell);
    }

    /**
     * @return the collisionEnabled
     */
    public boolean isCollisionEnabled() {
        return collisionEnabled;
    }

    /**
     * @param collisionEnabled the collisionEnabled to set
     */
    public void setCollisionEnabled(boolean collisionEnabled) {
        if (this.collisionEnabled == collisionEnabled)
            return;

        synchronized(entityLock) {
            this.collisionEnabled = collisionEnabled;

            if (entity!=null) {
                adjustCollisionSystem();
            }
        }
    }

    public void setPickingEnabled(boolean pickingEnabled) {
        if (this.pickingEnabled == pickingEnabled)
            return;

        synchronized(entityLock) {
            this.pickingEnabled = pickingEnabled;

            if (entity!=null) {
                adjustCollisionSystem();
            }
        }
    }

    /**
     * Adjust the collision system after a change to picking or collision
     */
    private void adjustCollisionSystem() {
        CollisionComponent cc = entity.getComponent(CollisionComponent.class);
        if (collisionEnabled==false && pickingEnabled==false && cc!=null)
            entity.removeComponent(CollisionComponent.class);

        if (cc==null) {
            WonderlandSession session = cell.getCellCache().getSession();
            CollisionSystem collisionSystem = ClientContextJME.getCollisionSystem(session.getSessionManager(), "Default");
            cc = setupCollision(collisionSystem, rootNode);
            entity.addComponent(CollisionComponent.class, cc);
            return;
        }

        cc.setCollidable(collisionEnabled);
        cc.setPickable(pickingEnabled);
    }

    public void setLightingEnabled(final boolean lightingEnabled) {
        if (this.lightingEnabled == lightingEnabled) {
            return;
        }

        this.lightingEnabled = lightingEnabled;

        if (entity != null) {
            adjustLighting(entity);
        }
    }

    public void setBackfaceCullingEnabled(boolean backfaceCullingEnabled) {
        if (this.backfaceCullingEnabled==backfaceCullingEnabled)
            return;

        this.backfaceCullingEnabled = backfaceCullingEnabled;

        if (entity!=null) {
            final RenderComponent rc = entity.getComponent(RenderComponent.class);
            final CullState.Face face = backfaceCullingEnabled ? CullState.Face.Back : CullState.Face.None;
            if (rc!=null && rc.getSceneRoot()!=null) {
                ClientContextJME.getWorldManager().addRenderUpdater(new RenderUpdater() {

                    public void update(Object arg0) {
                        TreeScan.findNode(rc.getSceneRoot(), new ProcessNodeInterface() {
                            public boolean processNode(Spatial node) {
                                CullState cs = (CullState) node.getRenderState(RenderState.RS_CULL);
                                if (cs==null) {
                                    cs = (CullState) ClientContextJME.getWorldManager().getRenderManager().createRendererState(RenderState.RS_CULL);
                                    node.setRenderState(cs);
                                }
                                cs.setCullFace(face);

                                return true;
                            }
                        });
                        ClientContextJME.getWorldManager().addToUpdateList(rc.getSceneRoot());
                    }
                }, null);
            }
        }
    }

    private void adjustLighting(final Entity entity) {
        RenderComponent rc = entity.getComponent(RenderComponent.class);
        rc.setLightingEnabled(lightingEnabled);
    }

    /**
     * JME Asset locator using WL Asset manager
     */
    public class AssetResourceLocator implements ResourceLocator {

        private String modulename;
        private String path;
        private String protocol;

        /**
         * Locate resources for the given file
         * @param url
         */
        public AssetResourceLocator(URL url) {
            // The modulename can either be in the "user info" field or the
            // "host" field. If "user info" is null, then use the host name.
//            System.out.println("ASSET RESOURCE LOCATOR FOR URL " + url.toExternalForm());

            if (url.getUserInfo() == null) {
                modulename = url.getHost();
            }
            else {
                modulename = url.getUserInfo();
            }
            path = url.getPath();
            path = path.substring(0, path.lastIndexOf('/')+1);
            protocol = url.getProtocol();

//            System.out.println("MODULE NAME " + modulename + " PATH " + path);
        }

        public URL locateResource(String resource) {
//            System.err.println("Looking for resource "+resource);
//            System.err.println("Module "+modulename+"  path "+path);
            try {
                if (resource.startsWith("/")) {
                    URL url = getAssetURL(protocol + "://"+modulename+resource);
//                    System.err.println("Using alternate "+url.toExternalForm());
                    return url;
                } else {
                    String urlStr = trimUrlStr(protocol + "://"+modulename+path + resource);

                    URL url = getAssetURL(urlStr);
//                    System.err.println("Using " + url.toExternalForm());
                    return url;
                }
            } catch (MalformedURLException ex) {
                logger.log(Level.SEVERE, "Unable to locateResource "+resource, ex);
                return null;
            }
        }

        /**
         * Trim ../ from url
         * @param urlStr
         */
        private String trimUrlStr(String urlStr) {
            int pos = urlStr.indexOf("/../");
            if (pos==-1)
                return urlStr;

            StringBuilder buf = new StringBuilder(urlStr);
            int start = pos;
            while(buf.charAt(--start)!='/') {}
            buf.replace(start, pos+4, "/");
//            System.out.println("Trimmed "+buf.toString());

           return buf.toString();
        }

    }

    /**
     * An mtgame ProcessorCompoenent to process cell moves.
     */
    public class MoveProcessor extends ProcessorComponent {

        private CellTransform cellTransform;
        private boolean dirty = false;
        private Node node;
        private WorldManager worldManager;
        private Vector3f tmpV3f = new Vector3f();
        private Vector3f tmp2V3f = new Vector3f();
        private Quaternion tmpQuat = new Quaternion();

        private final long postId = ClientContextJME.getWorldManager().allocateEvent();

        private PostEventCondition postCondition = new PostEventCondition(this, new long[] {postId});
       
        public MoveProcessor(WorldManager worldManager, Node node) {
            this.node = node;
            this.worldManager = worldManager;
        }
       
        @Override
        public void compute(ProcessorArmingCollection arg0) {
        }

        @Override
        public void commit(ProcessorArmingCollection arg0) {
            // The dirty flag is important for Avatars, as we chain
            // the moveProcessor to the avatarcontrol which update per frame
            // This needs breaking out at some point in the future

            synchronized(this) {
                if (dirty) {
//                    System.err.println("BasicRenderer.cellMoved "+node.getLocalTranslation()+"  "+cellTransform.getTranslation(null));
                    node.setLocalTranslation(cellTransform.getTranslation(tmpV3f));
                    node.setLocalRotation(cellTransform.getRotation(tmpQuat));
                    node.setLocalScale(cellTransform.getScaling(tmp2V3f));
                    dirty = false;
                    worldManager.addToUpdateList(node);
//            System.err.println("--------------------------------");
                }
            }

            // Clear the triggering events
            if (arg0.size() != 0) {
               PostEventCondition pec = (PostEventCondition)arg0.get(0);
               pec.getTriggerEvents();
            }
        }

        @Override       
        public void initialize() {
            setArmingCondition(postCondition);
        }

        /**
         * Notify the MoveProcessor that the cell has moved
         *
         * @param transform cell transform in world coordinates
         */
        public void cellMoved(CellTransform transform) {
            synchronized(this) {
                this.cellTransform = transform;
                dirty = true;
//                System.err.println("CellMoved "+postId);
                ClientContextJME.getWorldManager().postEvent(postId);
            }
        }

        @Override
        protected void finalize() {
            ClientContextJME.getWorldManager().freeEvent(postId);
        }
    }

    private class CollisionListener
            implements ComponentChangeListener, InteractionComponentListener
    {
        private final Cell cell;
        private InteractionComponent ic;

        public CollisionListener(Cell cell) {
            this.cell = cell;
        }

        public void enable() {
            cell.addComponentChangeListener(this);
            setInteractionComponent(cell.getComponent(InteractionComponent.class));
        }

        public void disable() {
            cell.removeComponentChangeListener(this);
            setInteractionComponent(null);
        }

        public void componentChanged(Cell cell, ChangeType type, CellComponent component) {
            if (component instanceof InteractionComponent) {
                switch (type) {
                    case ADDED:
                        setInteractionComponent((InteractionComponent) component);
                        break;
                    case REMOVED:
                        setInteractionComponent(null);
                        break;
                }
            }
        }

        public void collidableChanged(boolean collidable) {
            setCollisionEnabled(collidable);
        }

        public void selectableChanged(boolean selectable) {
            // ignore
        }

        private void setInteractionComponent(InteractionComponent ic) {
            if (this.ic != null) {
                this.ic.removeInteractionComponentListener(this);
            }

            this.ic = ic;

            if (ic != null) {
                ic.addInteractionComponentListener(this);
                setCollisionEnabled(ic.isCollidable());
            } else {
                // if there is no interaction component, set collision to
                // the default, true
                setCollisionEnabled(true);
            }
        }

    }
}
TOP

Related Classes of org.jdesktop.wonderland.client.jme.cellrenderer.BasicRenderer$CollisionListener

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.