AutoCloseable EntityManager

Sembra che io non sia l’unico a chiederselo e sembra anche che le risposte date non siano necessariamente soddisfacenti. Non sarebbe bello poter usare il try-with-resources con gli EntityManager?
Pare che il motivo per cui EntityManager non implementa AutoCloseable sia che se lo facesse, potrebbe solo fare la close() di sè stesso, ma non la rollback() dell’eventuale transazione. Questo renderebbe l’intero sforzo inutile, dovendo comunque gestire la transazione attraverso la finally come al solito. Oppure illuderebbe l’utente che l’EntityManager, essendo AutoCloseable, faccia cose che in realtà non fa (la rollback() appunto). Onestamente mi sfugge il motivo per cui l’EntityManager non possa tenere traccia della EntityTransaction, così come fa il mio codice qui sotto, ma è probabile che sia un problema di specifiche e di ricerca della purezza a tutti costi, per venire incontro a qualche possibile implementazione di JPA proveniente da Marte o altri pianeti più lontani…

Ad ogni modo il mio codice funziona, o almeno funziona per me e per il mio stile di programmazione. Può darsi che in realtà non sia sufficientemente generico e che in altri casi non funzioni… se capita a voi fatemi sapere, grazie.

Notate che il fatto che io abbia aggiunto AutoCloseable è una conseguenza di un bug che avevo lasciato in un mio software per cui un EntityManager non veniva chiuso. Per scoprire quale fosse, ho dovuto aggiungere tutto il monitoraggio attraverso JConsole ed interfaccia MBean. Ora quindi avete una classe che vi permette di ottenere un EntityManager (in realtà lo dovete usare per quel che è, ovvero EMWrapper) che nel frattempo è anche monitorabile attraverso JConsole. A voi il codice.

/*
Copyright © 2014 Lucio Crusca <lucio@sulweb.org>

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 
*/

package com.virtual_bit.salix.web.persistence;

import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.Persistence;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.metamodel.Metamodel;

public class PersistenceManager
{
  private static final PersistenceManager singleton = new PersistenceManager();

  private HashMap<String, EntityManagerFactory> emfs;
  
  private static final String mainUnitName = "my-unit";
  
  private static final String[] databases = {
    "some-unit",
    "other-unit"
  };
  
  public static interface EMapMBean extends Map<String, List<EMWrapper>>
  {
    
  }
  
  public static class EMap extends HashMap<String, List<EMWrapper>> implements EMapMBean
  {
    
  }
  
  public static interface EntityMapMXBean
  {
    EMap getMap();
    String[] getOverview();
    String[] getDetails();
  }

  private static EMap activeWrappers;
  
  public static class EntityMap implements EntityMapMXBean
  {
    @Override
    public EMap getMap()
    {
      return activeWrappers;
    }
    
    public String[] getOverview()
    {
      List<String> result = new ArrayList<>();
      synchronized(activeWrappers)
      {
        Set<String> keys = activeWrappers.keySet();
        for (String s: keys)
          result.add(s + " -> " + activeWrappers.get(s).size());
      }
      String[] ares = new String[result.size()];
      return result.toArray(ares);
    }

    @Override
    public String[] getDetails()
    {
      List<String> lresult = new LinkedList<>();
      synchronized(activeWrappers)
      {
        Set<String> keys = activeWrappers.keySet();
        int ki = 0;
        for (String s: keys)
        {
          List<EMWrapper> wrappers = activeWrappers.get(s);
          String[][] traces = new String[wrappers.size()][];
          int i = 0;
          for (EMWrapper emw: wrappers)
            traces[i++] = emw.getStackStrace();
          lresult.add(flatten(traces));
        }
      }
      String[] result = new String[lresult.size()];
      return lresult.toArray(result);
    }

    private String flatten(String[][] traces)
    {
      StringBuilder sb = new StringBuilder();
      for (String[] key: traces)
        for (String values: key)
          sb.append(values).append("\n");
      return sb.toString();
    }

  }

  private PersistenceManager()
  {
  }
  
  private static void initMBean()
  {
    synchronized(PersistenceManager.class)
    {
      if (activeWrappers != null)
        return;
      
      try
      {
        activeWrappers = new EMap();
        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();  
        EntityMap map = new EntityMap();
        ObjectName name = new ObjectName(map.getClass().getPackage().getName() + ":type=" + map.getClass().getSimpleName());
        mbs.registerMBean(map, name);
      }
      catch (InstanceAlreadyExistsException | MBeanRegistrationException | MalformedObjectNameException | NotCompliantMBeanException ex)
      {
        Logger.getLogger(PersistenceManager.class.getName()).log(Level.SEVERE, null, ex);
      }    
    }
  }

  private static PersistenceManager getInstance()
  {
    return singleton;
  }

  private EMWrapper createEntityManager(String jndiName)
  {
    EntityManagerFactory emf = _getFactory(jndiName);
    EMWrapper result = new EMWrapper(emf.createEntityManager(), jndiName);
    return result;
  }
  
  private EntityManagerFactory _getFactory(String jndiName)
  {
    if (emfs == null)
      emfs = new HashMap<>();
    if (!emfs.containsKey(jndiName))
      emfs.put(jndiName, Persistence.createEntityManagerFactory(jndiName));
    return emfs.get(jndiName);
  }

  public void closeFactories()
  {
    if (emfs == null)
      return;
    for (EntityManagerFactory emf: emfs.values())
      emf.close();
    emfs.clear();
    emfs = null;
  }
  
  public static EMWrapper getEM()
  {
    return getInstance().createEntityManager(nameEM());
  }
  
  private static String suffix()
  {
    Thread current = Thread.currentThread();
    if (current.isDaemon())
      return "-bg";
    return "";
            
  }

  public static String nameEM()
  {
    return mainUnitName + suffix();
  }
  
  
  public static String nameSomeEM()
  {
    return databases[0] + suffix();
  }
  
  public static String nameOtherEM()
  {
    return databases[1] + suffix();
  }
  
  
  public static EMWrapper getSomeEM()
  {
    return getInstance().createEntityManager(nameSomeEM());
  }
  
  public static EMWrapper getOtherEM()
  {
    return getInstance().createEntityManager(nameOtherEM());
  }
  
  public static EMWrapper getEM(String unitname)
  {
    return getInstance().createEntityManager(unitname);
  }

  public static Iterable<String> nameDatabases()
  {
    ArrayList<String> dbs = new ArrayList<>();
    for (String d: databases)
      dbs.add(d + suffix());
    return dbs;
  }

  public static interface EMWrapperMBean extends EntityManager
  {
    
  };
  
  public static class EMWrapper implements EMWrapperMBean, AutoCloseable
  {
    private final EntityManager wrapped;
    private final String jndiName;
    private StackTraceElement[] ste;
    
    private EMWrapper(EntityManager wrapped, String jndiName)
    {
      this.wrapped = wrapped;
      this.jndiName = jndiName;
      this.ste = Thread.currentThread().getStackTrace();
      initMBean();
      synchronized(activeWrappers)
      {
        List<EMWrapper> active = activeWrappers.get(jndiName);
        if (active == null)
          active = new LinkedList<>();
        active.add(this);
        activeWrappers.put(jndiName, active);
      }
    }

    @Override
    public void persist(Object entity)
    {
      wrapped.persist(entity);
    }

    @Override
    public <T> T merge(T entity)
    {
      return wrapped.<T>merge(entity);
    }

    @Override
    public void remove(Object entity)
    {
      wrapped.remove(entity);
    }

    @Override
    public <T> T find(Class<T> entityClass, Object primaryKey)
    {
      return wrapped.<T>find(entityClass, primaryKey);
    }

    @Override
    public <T> T find(Class<T> entityClass, Object primaryKey, Map<String, Object> properties)
    {
      return wrapped.<T>find(entityClass, primaryKey, properties);
    }

    @Override
    public <T> T find(Class<T> entityClass, Object primaryKey, LockModeType lockMode)
    {
      return wrapped.<T>find(entityClass, primaryKey, lockMode);
    }

    @Override
    public <T> T find(Class<T> entityClass, Object primaryKey, LockModeType lockMode, Map<String, Object> properties)
    {
      return wrapped.<T>find(entityClass, primaryKey, lockMode, properties);
    }

    @Override
    public <T> T getReference(Class<T> entityClass, Object primaryKey)
    {
      return wrapped.<T>getReference(entityClass, primaryKey);
    }

    @Override
    public void flush()
    {
      wrapped.flush();
    }

    @Override
    public void setFlushMode(FlushModeType flushMode)
    {
      wrapped.setFlushMode(flushMode);
    }

    @Override
    public FlushModeType getFlushMode()
    {
      return wrapped.getFlushMode();
    }

    @Override
    public void lock(Object entity, LockModeType lockMode)
    {
      wrapped.lock(entity, lockMode);
    }

    @Override
    public void lock(Object entity, LockModeType lockMode, Map<String, Object> properties)
    {
      wrapped.lock(entity, lockMode, properties);
    }

    @Override
    public void refresh(Object entity)
    {
      wrapped.refresh(entity);
    }

    @Override
    public void refresh(Object entity, Map<String, Object> properties)
    {
      wrapped.refresh(entity, properties);
    }

    @Override
    public void refresh(Object entity, LockModeType lockMode)
    {
      wrapped.refresh(entity, lockMode);
    }

    @Override
    public void refresh(Object entity, LockModeType lockMode, Map<String, Object> properties)
    {
      wrapped.refresh(entity, lockMode, properties);
    }

    @Override
    public void clear()
    {
      wrapped.clear();
    }

    @Override
    public void detach(Object entity)
    {
      wrapped.detach(entity);
    }

    @Override
    public boolean contains(Object entity)
    {
      return wrapped.contains(entity);
    }

    @Override
    public LockModeType getLockMode(Object entity)
    {
      return wrapped.getLockMode(entity);
    }

    @Override
    public void setProperty(String propertyName, Object value)
    {
      wrapped.setProperty(propertyName, value);
    }

    @Override
    public Map<String, Object> getProperties()
    {
      return wrapped.getProperties();
    }

    @Override
    public Query createQuery(String qlString)
    {
      return wrapped.createQuery(qlString);
    }

    @Override
    public <T> TypedQuery<T> createQuery(CriteriaQuery<T> criteriaQuery)
    {
      return wrapped.<T>createQuery(criteriaQuery);
    }

    @Override
    public <T> TypedQuery<T> createQuery(String qlString, Class<T> resultClass)
    {
      return wrapped.<T>createNamedQuery(qlString, resultClass);
    }

    @Override
    public Query createNamedQuery(String name)
    {
      return wrapped.createNamedQuery(name);
    }

    @Override
    public <T> TypedQuery<T> createNamedQuery(String name, Class<T> resultClass)
    {
      return wrapped.<T>createNamedQuery(name, resultClass);
    }

    @Override
    public Query createNativeQuery(String sqlString)
    {
      return wrapped.createNativeQuery(sqlString);
    }

    @Override
    public Query createNativeQuery(String sqlString, Class resultClass)
    {
      return wrapped.createNativeQuery(sqlString, resultClass);
    }

    @Override
    public Query createNativeQuery(String sqlString, String resultSetMapping)
    {
      return wrapped.createNativeQuery(sqlString, resultSetMapping);
    }

    @Override
    public void joinTransaction()
    {
      wrapped.joinTransaction();         
    }

    @Override
    public <T> T unwrap(Class<T> cls)
    {
      return wrapped.<T>unwrap(cls);
    }

    @Override
    public Object getDelegate()
    {
      return wrapped.getDelegate();
    }
    
    private EntityTransaction transaction;
    
    @Override
    public void close()
    {
      if (transaction != null)
        if (transaction.isActive())
          transaction.rollback();
       
      wrapped.close();
      this.ste = null;
      synchronized(activeWrappers)
      {
        List<EMWrapper> active = activeWrappers.get(jndiName);
        active.remove(this);
        activeWrappers.put(jndiName, active);
      }
    }

    @Override
    public boolean isOpen()
    {
      return wrapped.isOpen();
    }

    @Override
    public EntityTransaction getTransaction()
    {
      transaction = wrapped.getTransaction();
      return transaction;
    }

    @Override
    public EntityManagerFactory getEntityManagerFactory()
    {
      return wrapped.getEntityManagerFactory();
    }

    @Override
    public CriteriaBuilder getCriteriaBuilder()
    {
      return wrapped.getCriteriaBuilder();
    }

    @Override
    public Metamodel getMetamodel()
    {
      return wrapped.getMetamodel();
    }
    
    public static HashMap<String, List<EMWrapper>> getWrappers()
    {
      return activeWrappers;
    }
    
    public String[] getStackStrace()
    {
      if (ste == null)
        return null;
      String[] result = new String[ste.length];
      int i = 0;
      for (StackTraceElement s: ste)
        result[i++] = s.getClassName() + "#" + s.getMethodName() + ":" + s.getLineNumber();
      return result;
    } 
  }
}

LibGDX: BestFitViewport

Today’s post is in english, because software development and code distribution know only one language: english.

While trying to solve this problem I wrote a new Viewport class (and solved said problem). You are encouraged to read that thread first.

Its name is BestFitViewport and it makes sense only on mobile devices in poorly designed apps that force the user to rotate the device between screens. I know, the right solution would be a good user interface design, but chances are you have no choice because someone else badly designed the app and they pay you to code that crap.

Assuming you end up in such a situation, this class may help you in that:
1. it offers cross platform screen orientation (Android and IOs only, desktop and web app developers DO NOT WANT TO USE THIS, unless they want to force users of a 23 inch. CRT display to physically rotate it now and then…). Please note that I tested this thing only on two Android devices (SGS GT I9000 and SGS GT I9505), however it does not make use of any esoteric features, only two common libgdx classes (Viewport and Camera), so I expect it to work more or less everywhere.
2. It’s way faster than setRequestedOrientation()
3. It works where setRequestedOrientation() was failing (SGS GT I9505), maybe my fault, but I couldn’t spot it

Enough bla bla bla, here is the code:

package org.sulweb.bestfitviewport;

/*
* Copyright © 2014 Lucio Crusca <lucio@sulweb.org>
* Based on LibGDX OrthographicCamera.java source code, nightly 2014-04-20.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
*    notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
*    notice, this list of conditions and the following disclaimer in the
*    documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/


import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;

public class BestFitCamera extends Camera
{
  public float zoom = 1, stretchFactorX = 1, stretchFactorY = 1;
  public boolean rotated;

  public BestFitCamera()
  {
    this.near = 0;
  }

  public BestFitCamera(float viewportWidth, float viewportHeight)
  {
    this.viewportWidth = viewportWidth;
    this.viewportHeight = viewportHeight;
    this.near = 0;
    update();
  }

  private final Vector3 tmp = new Vector3();

  @Override
  public void update()
  {
    update(true);
  }

  @Override
  public void update(boolean updateFrustum)
  {
    float left = zoom * stretchFactorX * -(viewportWidth / 2);
    float right = zoom * stretchFactorX * (viewportWidth / 2);
    float top = zoom * stretchFactorY * -(viewportHeight / 2);
    float bottom = zoom * stretchFactorY * viewportHeight / 2;
    if (rotated)
    {
      float tmp = left;
      left = top;
      top = tmp;
      tmp = right;
      right = bottom;
      bottom = tmp;
    }
    projection.setToOrtho(left, right, top, bottom, near, far);
    view.setToLookAt(position, tmp.set(position).add(direction), up);
    combined.set(projection);
    Matrix4.mul(combined.val, view.val);

    if (updateFrustum)
    {
      invProjectionView.set(combined);
      Matrix4.inv(invProjectionView.val);
      frustum.update(invProjectionView);
    }
  }

  /**
   * Sets this camera to an orthographic projection using a viewport fitting the screen resolution,
   * centered at (Gdx.graphics.getWidth()/2, Gdx.graphics.getHeight()/2), with the y-axis pointing
   * up or down.
   * 
   * @param yDown
   *          whether y should be pointing down
   */
  public void setToOrtho(boolean yDown)
  {
    setToOrtho(yDown, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
  }

  /**
   * Sets this camera to an orthographic projection, centered at (viewportWidth/2,
   * viewportHeight/2), with the y-axis pointing up or down.
   * 
   * @param yDown
   *          whether y should be pointing down.
   * @param viewportWidth
   * @param viewportHeight
   */
  public void setToOrtho(boolean yDown, float viewportWidth,
      float viewportHeight)
  {
    if (yDown)
    {
      up.set(0, -1, 0);
      direction.set(0, 0, 1);
    }
    else
    {
      up.set(0, 1, 0);
      direction.set(0, 0, -1);
    }
    position
        .set(zoom * viewportWidth / 2.0f, zoom * viewportHeight / 2.0f, 0);
    this.viewportWidth = viewportWidth;
    this.viewportHeight = viewportHeight;
    update();
  }

  /**
   * Rotates the camera by the given angle around the direction vector. The direction and up vector
   * will not be orthogonalized.
   * 
   * @param angle
   */
  public void rotate(float angle)
  {
    rotate(direction, angle);
  }

  /**
   * Moves the camera by the given amount on each axis.
   * 
   * @param x
   *          the displacement on the x-axis
   * @param y
   *          the displacement on the y-axis
   */
  public void translate(float x, float y)
  {
    translate(x, y, 0);
  }

  /**
   * Moves the camera by the given vector.
   * 
   * @param vec
   *          the displacement vector
   */
  public void translate(Vector2 vec)
  {
    translate(vec.x, vec.y, 0);
  }

}
package org.sulweb.bestfitviewport;

/*
* Copyright © 2014 Lucio Crusca <lucio@sulweb.org>
* Based on LibGDX FitViewport.java and ScalingViewport.java source code, nightly 2014-04-20.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
*    notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
*    notice, this list of conditions and the following disclaimer in the
*    documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Scaling;
import com.badlogic.gdx.utils.viewport.Viewport;

/**
 * This class behaves more or less like a FitViewport; it behaves different
 * when the world units orientation does not match the current display (or
 * window) orientation. This class may be useful only on mobile devices.
 * 
 * You may find this viewport useful only if all the following things hold
 * true for your mobile app:
 * 1. The app has at least one fixed portrait screen
 * 2. The app has at least one fixed landscape screen
 * 3. You agree that 1+2 above is bad user interface design, but your boss does not
 * want to change that
 * 4. You want portable code, i.e. you don't want to call setRequestedOrientation()
 *  because that's Android specific
 * 5. You want a fast orientation switch between screens
 * 6. You're fed up with Android Activity lifecycle and do not want to call setRequestedOrientation()
 * also to avoid a recreate()
 * 7. Your world units are "sane" enough, i.e. the long edge is actually longer, in numbers, 
 * than the short one. If this is not the case for you, it should be reasonably easy to patch
 * my code to support a 800x300 portrait world...
 * 8. Your Activity has a fixed orientation, e.g. the user cannot choose to rotate his device
 * and make the screen layout differently
 * 
 * How to use this class. You basically get the same as a FitViewport that fits the best 
 * fitting orientation, based on world coordinates you pass in and assuming world units
 * are squares.
 * 
 *   stage.setViewport(new BestFitViewport(1000,800));
 *   
 * such a viewport will force landscape orientation and fit the landscape display.
 * 
 *   stage.setViewport(new BestFitViewport(750,1030));
 *   
 * and this one will force portrait orientation and fit the portrait display.
 * 
 * @author Lucio Crusca
 *
 */
public class BestFitViewport extends Viewport
{
  private Scaling scaling;
  private boolean rotated;
  private int screen_width, screen_height;
  private float currentRotation;
  
  public BestFitViewport(float wwidth, float wheight)
  {
    super();
    this.scaling = Scaling.fit;
    this.worldWidth = wwidth;
    this.worldHeight = wheight;
    this.camera = new BestFitCamera();  
  }
  
  private void updateRotationState(int screenW, int screenH)
  {
    this.screen_width = screenW;
    this.screen_height = screenH;
    rotated = (worldWidth > worldHeight && screenW < screenH) || 
              (worldWidth < worldHeight && screenW > screenH);
  }

  @Override
  public void setWorldHeight(float worldHeight)
  {
    setWorldSize(getWorldWidth(), worldHeight);
  }

  @Override
  public void setWorldSize(float worldWidth, float worldHeight)
  {
    this.worldWidth = worldWidth;
    this.worldHeight = worldHeight;
    updateRotationState(screen_width, screen_height);
  }

  @Override
  public void setWorldWidth(float worldWidth)
  {
    setWorldSize(worldWidth, getWorldHeight());
  }

  @Override
  public void update(int screenWidth, int screenHeight, boolean centerCamera)
  {
    if (screenHeight != 0 && screenWidth != 0 &&
        (screenHeight != this.screen_height || screenWidth != this.screen_width)
       )
    {
      updateRotationState(screenWidth, screenHeight);
      Vector2 scaled;
      if (rotated)
      {
        scaled = scaling.apply(worldWidth, worldHeight, screenHeight, screenWidth);
        viewportWidth = Math.round(scaled.y);
        viewportHeight = Math.round(scaled.x);
      }
      else
      {
        scaled = scaling.apply(worldWidth, worldHeight, screenWidth, screenHeight);
        viewportWidth = Math.round(scaled.x);
        viewportHeight = Math.round(scaled.y);
      }
      
      // center the viewport in the middle of the screen
      viewportX = (screenWidth - viewportWidth) / 2;
      viewportY = (screenHeight - viewportHeight) / 2;

      Gdx.gl.glViewport(viewportY, viewportX, viewportWidth, viewportHeight);
      camera.viewportWidth = worldWidth;
      camera.viewportHeight = worldHeight;
      if (centerCamera) 
        camera.position.set(camera.viewportWidth / 2, camera.viewportHeight / 2, 0); 
  
      float previousRotation = currentRotation;
      currentRotation = rotated ? 270 : 0;
      camera.rotate(currentRotation - previousRotation, 0, 0, 1);
      if (rotated)
      {
        ((BestFitCamera)camera).stretchFactorX = (worldHeight / viewportWidth) / (worldWidth / viewportHeight);
        ((BestFitCamera)camera).stretchFactorY = 1 / ((BestFitCamera)camera).stretchFactorX; 
      }
      ((BestFitCamera)camera).rotated = rotated;      
    }
    camera.update(); 
  }
}

No jars provided, if you want to use it, just copy the code over into your project.

Sviluppo di applicazioni web in team

Le pagine di un sito dinamico che ha sia una forte componente di business logic, sia dei precisi requisiti estetici e comunicativi, devono necessariamente essere sviluppate da più persone, ognuna nel proprio ambito di competenza.

Lo sviluppo contemporaneo dei due aspetti, comunicativo e funzionale, è però problematico, indipendentemente da quale linguaggio usino i programmatori e da quale software di authoring usino i web designers.

I web designers devono poter scrivere (o far scrivere dal loro software preferito) il codice HTML ed il codice CSS e devono anche poterlo modificare all’occorrenza fino a raggiungere il risultato desiderato dal cliente. I programmatori devono, d’altro canto, modificare il codice HTML creato dai web designers, per aggiungere la caratteristica di generazione dinamica dei dati a tali pagine. Le modifiche apportate dai programmatori tipicamente non vengono interpretate nel modo corretto dal software utilizzato dai web designer, il quale smette di riconoscere e di renderizzare l’intera pagina modificata dal programmatore, oppure ignora le parti che non capisce, ma, in questo caso, non risalva le modifiche in modo corretto dopo che il designer ha cambiato qualcosa puramente estetico all’interno della pagina. Tutto ciò ovviamente supponendo che il web designer non usi solamente un semplice editor di testi, cosa piuttosto rara.

Molti framework e linguaggi di sviluppo software, inoltre, semplificano la vita al programmatore, permettendogli di spezzare la pagina HTML in files più piccoli e più facilmente gestibili, per poi ottenere la pagina finale attraverso una procedura automatica di ricomoposizione delle parti, che avviene al volo durante la generazione dinamica delle pagine stesse. Se da una parte questo semplifica la vita al programmatore, dall’altra la complica infinitamente al web designer, il quale ora si trova con tanti pezzetti di files HTML, apparentemente sparsi senza filo logico (il filo logico è scritto nel codice del linguaggio usato dal programmatore, ovvero in un modo incomprensibile al web designer).

A seconda dei casi specifici, dipendenti dai software e framework usati, dalla composizione del team e dalle competenze di ognuno, ci possono essere varie soluzioni più o meno efficaci a questi problemi, ma l’unica soluzione trasversale ed applicabile indipendentemente da tutti i fattori citati ci arriva dall’oriente: «Modo migliore di evitare pugno… è di non essere li». Tradotto, il modo migliore è evitare la sovrapposizione fra il lavoro dei web designers e quello degli sviluppatori software.
Per poterla evitare, è necessario che i web designers terminino il loro lavoro prima che gli sviluppatori inizino a lavorare sull’interfaccia utente. Per potersi permettere questo lusso, è necessario prevedere i due step di sviluppo, chiarire il problema al committente e chiedergli di approvare la grafica del sito prima che questo vada online, ovvero spiegargli che da quando lui approva la grafica a quando andrà online passerà un tempo maggiore di zero, durante il quale gli sviluppatori creeranno l’interfaccia utente e durante il quale nessuna modifica all’asspetto grafica sarà più possibile. Nella realtà dei fatti poi si cerca di non essere così rigidi, ma è importante far passare il messaggio a priori.

La metodologia di sviluppo del team deve tenere conto del fatto che lo sviluppo delle pagine HTML non deve sovrapporsi, in linea temporale, allo sviluppo dell’interfaccia utente dinamica, ma deve precederla.

I web designers sviluppano quindi prima la struttura HTML delle pagine, utilizzando dati statici e fittizi per le parti che saranno in futuro generate dinamicamente. Contemporaneamente, sul lato sviluppo software, gli sviluppatori partono dal backend (come fra l’altro è normale che sia) e solo in un secondo momento, quando l’aspetto estetico è ormai stabile e deciso, lo si congela e si procede allo sviluppo della parte software che si occuperà dell’interfaccia utente.

Dopo il congelamento dell’aspetto estetico, sarà ancora possibile, per i web designers, apportare qualsiasi modifica essi vogliano ai files CSS, ma non sarà più possibile per loro modificare autonomamente i files HTML. Qualsiasi modifica ai files HTML, dopo il congelamento, dovrà essere fatta dal team di sviluppo software su indicazione dei designers e, in caso di modifiche strutturali, potrebbe comportare il rifacimento di parte del lavoro di sviluppo software per l’interfaccia utente eventualmente già svolto.

Tutto questo deve essere tenuto in debita considerazione nel pianificare i lavori, nel prevedere le date di consegna, nell’anticipare al cliente gli step di produzione del sito ed in generale nel costruire un diagramma di Gantt il più possibile fedele al progetto che si sta facendo.

Va bene, direte voi, ma in che modo questa soluzione sarebbe migliore di altre? Beh, il meglio ed il peggio sono concetti soggettivi, ma a favore di questa soluzione c’è una minor dipendenza da variabili aleatorie che, per quanto aleatorie, giocano sempre a nostro sfavore, e fanno immancabilmente slittare in avanti la data di consegna. In pratica questa soluzione ha il grande vantaggio di permetterci di dire subito al cliente una data di consegna che più probabilmente riusciremo a rispettare. Cliente soddisfatto.

Eccezioni: prevedere l’imprevedibile.

Sale qb. Questa è la parte più antipatica di ogni ricetta di cucina per chi non ha esperienza di cuoco. Di solito arriva al fondo dell’elenco ingredienti. Cosa significa qb? Beh sì, significa “quanto basta”, ma a quanto corrisponde? Potremmo dire che il calcolo del valore della variabile qb procede per approssimazioni successive:

1. Assaggia l'impasto
2. È buono?
2A. Sì, allora non aggiungere sale, la ricetta è terminata
2B. No, allora aggiungi un pizzico di sale e torna al punto 1

Facile no? Magari è una procedura un po’ lenta e magari in alcuni casi si finisce per mangiarsi gran parte dell’impasto prima di raggiungere la quantità corretta di sale, ma nella maggior parte dei casi funziona. Non prevede però tutti i casi. Cosa succede se l’impasto è già troppo salato in partenza? Aggiungo zucchero? Butto via tutto e riparto da capo? Uso l’impasto per la cena del cane? Procediamo per passi. Prima di tutto traduciamo in codice la procedura descritta sopra. Faccio notare che ci sarebbero molti modi per tradurla in codice, questo è solo uno dei tanti:

public void adjustSalt()
{
  Recipe r = getRecipe();
  if (r.hasGoodTaste())
    return;
  else
  {
    r.addSalt(0.6);
    ajustSalt();
  }
}

Chiariamo subito che questo codice è incompleto, nulla si dice di cosa sia la classe Recipe o di come siano fatte le varie funzioni che vengono chiamate come se esistessero, a parte la adjustSalt() che è l’unica definita. Inoltre 0.6 è un numero arbitrario che ho deciso che significa “un pizzico di sale”. Quello che ho scritto è una via di mezzo fra codice Java e pseudocodice, serve solo a rendere l’idea ma così com’è non funzionerà mai da nessuna parte. È comunque utile ad illustrare il concetto.
Il problema di questa funzione è il fatto di dare per scontato che per rendere buono l’impasto basti aggiungere sale fino a quando la funzione hasGoodTaste() non ritorna true. Se la funziona hasGoodTaste() ritorna false, in realtà noi sappiamo che l’impasto non è buono, ma non sappiamo veramente il perché.
Un’alternativa potrebbe essere:

public void adjustSalt()
{
  Recipe r = getRecipe();
  try
  {
    r.taste();
    return;
  }
  catch(InsipidException ie)
  {
    r.addSalt(0.6);
  }
  catch(SaltyException se)
  {
    r.addPotato(0.4);
    r.addTomato(0.2);
    r.addCream(0.1);
    r.addWater(0.1);
  }
  adjustSalt();
}

Osserviamo che in questo codice non c’è più l’istruzione if. Al suo posto c’è una try, che significa: “prova a fare quel che ti dico, se succede qualcosa di strano, vediamo poi come arrangiarci”. Quello che si richiede di fare all’interno della try è assaggiare l’impasto, ovvero la chiamata alla funzione taste(). Se questa funzione termina il suo compito senza che accada nulla di strano, significa che l’impasto è buono: il programma prosegue normalmente con l’istruzione successiva che è una return e così la funzione adjustSalt() termina e il controllo ritorna al chiamante. Se invece l’impasto non è buono, la taste() lancia un’eccezione. La differenza, rispetto a prima, è che l’eccezione può essere ti qualsiasi tipo e portare con sè molte informazioni circa il problema che si è verificato, mentre prima il valore di ritorno della hasGoodTaste() poteva essere solo vero o falso, senza tanti perché.
L’istruzione try deve essere seguita da una o più istruzioni catch (non è del tutto vero, ma per ora prendiamolo per vero). Ogni istruzione catch specifica un tipo di problema che si può verificare all’interno del blocco try e la relativa soluzione. Per esempio se l’impasto è insipido (InsipidException) la soluzione è aggiungere sale, mentre se è già troppo salato la soluzione è stemperarlo con patate, pomodori, panna ed acqua.

Ci sono molti altri vantaggi nell’usare le eccezioni invece dei valori di ritorno contenenti codici di errore, ma per ora mi fermo qui. Il resto ve lo dirò in un prossimo post.

Tipizzazione forte, oltre lo scripting

Apro, con questo articolo, una serie di suggerimenti sulla programmazione. Non è un corso, è una collezione di informazioni sperabilmente utili. Userò del codice Java quando sarà necessario fare esempi, ma esporrò concetti validi in tutti i linguaggi. Partiamo.
Quale differenza passa fra Java e JavaScript? A parte i nomi somiglianti ed una sintassi simile, tutto il resto. Ed è circa la stessa differenza che passa fra una ricetta cucinata da un famoso chef e la stessa ricetta cucinata da me. Avete mai cucinato? No? Vi svelo un segreto: al posto delle acciughe, se restate senza, si possono usare i capperi. Incredibile no? I capperi, un vegetale, al posto delle acciughe, un pesce. Certo, un vero chef probabilmente non lo farebbe mai, ma io sì.
Allo stesso modo in Java, ovvero un qualsiasi linguaggio con tipizzazione forte, non potete scambiare gli ingredienti a piacimento, mentre in JavaScript, un linguaggio con tipizzazione debole, potete. Bene, ma quando si programma, quali sono gli ingredienti? Naturalmente i dati in input del programma, o della singola funzione, o ancora della singola istruzione. I dati elaborati, ovvero quelli in output, sono l’equivalente della ricetta cucinata, ed a cucinare i dati ci pensa il computer seguendo le nostre istruzioni. Il codice che scriviamo sono come le istruzioni di una ricetta, in cui insegnamo al computer come cucinare i dati. I tipi di dati sono equivalenti, più o meno, ai tipi di ingredienti. Le acciughe sono pesci, quello è il tipo di ingrediente. I capperi sono vegetali. Il mio nome è una stringa, ovvero “Lucio”. La mia età è un numero, ovvero 40, nel momento in cui sto scrivendo.
In un linguaggio con tipizzazione debole, come per esempio JavaScript, io posso calcolare la mia età usando stringhe al posto di numeri, così:
eta = "2013" - "1973";
In questo caso il linguaggio, trovando l’operatore di sottrazione, ovvero il segno meno, sa che ha bisogno di numeri per poterlo eseguire e tenta una conversione automatica in numeri delle stringhe che abbiamo messo attorno al segno meno. Questo è utile per piccoli programmi (script) dove eventuali errori in questa conversione automatica non siano catastrofici. Per esempio, supponiamo che le due stringhe “2013” e “1973” siano frutto di una digitazione da tastiera richiesta all’utente che usa il programma e supponiamo quindi che il programma serva esattamente a dire quanti anni hai. Prima ti chiede in che anno siamo, poi ti chiede in che anno sei nato ed infine ti dice quanti anni hai. Ora supponiamo anche che l’utente, mentre inserisce i dati, per sbaglio metta una lettera I maiuscola al posto della cifra 1 (uno). Ci troveremmo a fare questo calcolo:
eta = "20I3" - "I973";
Nel caso di JavaScript, tipizzazione debole, il calcolo viene eseguito comunque, ma il risultato non è quello sperato, in quanto la variabile eta a questo punto vale NaN, cioè “Not A Number”. Un valore inutile, perché eta è un “non numero” (caspita mi sembra di parlare dei “non morti” di Monkey Island…), ma che cosa sia veramente non lo sappiamo. Peggio che mai, visto che il calcolo viene eseguito comunque ed il programma non viene fermato, potremmo accorgerci di questo “non numero” molto più avanti, quando ormai è troppo tardi per scoprire la causa del problema e correggere l’errore. Supponiamo per esempio di usare l’età per scrivere il nome dell’utente in un file e di avere un file diverso per ogni età, perché stiamo organizzando le cene dei coscritti di tutte le età del nostro paese. Ad un certo punto scriveremo il nome in un file che invece di chiamarsi “coscritti40.txt” si chiamerà “coscrittiNaN.txt”. In quel file ci finiranno tutti quelli che sbagliano a digitare i numeri. E non parteciperanno quindi ad alcuna cena dei costritti. Vogliamo veramente far morire di fame le persone?
Lo stesso programma, scritto in Java o in altro linguaggio con tipizzazione forte, non eseguirebbe la sottrazione. In Java non posso proprio scrivere
int eta = "2013" - "1973";
perché è sintatticamente errato. La sintassi Java impedisce di sottrarre stringhe. Java non fa conversioni automatiche di tipo se non in casi particolari e non pericolosi. Sicuramente non converte mai automaticamente da stringa a numero. Una riga di codice come quella, in Java, non viene neppure compilata: Java non ci permette neppure di provare il programma fino a quando non correggiamo quella riga:
int anno = Integer.parseInt("2013");
int nascita = Integer.parseInt("1973");
int eta = anno - nascita;

Ecco, questo è ciò che Java ci costringe a fare, ovvero ci costringe a scrivere esplicitamente il codice per convertire da stringa a numero intero. Lo fa perché è brutto e cattivo? Lo fa perché è antiquato? No, lo fa perché è un linguaggio con tipizzazione forte. E così facendo, da una parte scarica sul programmatore la responsabilità di eventuali errori di conversione, dall’altra può bloccare il programma e darci tutte le informazioni del caso, se quello che stiamo cercando di convertire non è convertibile nel tipo di dato che ci serve. Il principio è quello del “fail fast”, ovvero ottenere informazioni sugli errori prima possibile, in modo da poterli correggere (richiedere all’utente di digitare correttamente l’anno) prima che sia troppo tardi.
È evidente che il codice Java è più lungo, tre righe al posto di una sola. È anche evidente che il codice Java è più preciso, spiega passo passo cosa stiamo facendo. E, infine, risulta evidente come il programma sia più robusto, ovvero come sia più difficile fregarlo e mandarlo in una condizione di errore non prevista: Java fa di tutto per costringerci a prevedere le condizioni di errore. Resta un dubbio: come fa Java a bloccare il programma nel caso in cui non si possa tradurre la stringa in un numero intero? Solleva un’eccezione, ma di questo vi parlerò la prossima volta.