viernes, 10 de febrero de 2017

Java: Localizar y leer recursos (imágenes, properties o audio) independiente de la ubicación

Introducción

Localizar y leer recursos puede ser una tarea confusa y frustrante cuando no se tienen fundamentos sólidos, incluso a mi se me olvidan de vez en cuando, es por ello que decidí escribir esta entrada, para consultarla cuando la necesite.
Un recurso puede ser un archivo de texto o binario, imagen, audio, etc,. Usualmente los programas o clases necesitan leer estos recursos de forma independiente a la ubicación. De mi experiencia reconozco que la ubicación de un recurso puede coincidir con algunos de estos escenarios:
  • En la misma ubicación donde se encuentra la clase que necesita leer el recurso.
  • En otro paquete dentro del mismo jar donde se encuentra la clase que necesita leer el recurso
  • En un paquete dentro de un jar distinto al jar donde se encuentra la clase que necesita leer el recurso.
  • En un directorio, donde el directorio se encuentra en el classpath.
  • En algún lugar arbitrario dentro de una aplicación web fuera del jar donde se encuentra la clase que necesita leer el recurso

Las clases Class y ClassLoader contienen métodos para localizar y cargar recursos, cuando digo localizar me refiero a que pueden entregarnos una URL del recurso, y por cargar me refiero a que pueden entregarnos una instancia de un InputStream con el cual podemos leer el contenido.

Nota: El Classpath es una lista de directorios asociada a nuestra aplicación, cada aplicación que se ejecuta en la JVM tiene asociada un Classpath.

Nota: El ClassLoader en palabras simples, es una clase responsable de cargar en memoria nuestra clase principal y todas aquellas clases que necesite.

Recursos, nombres y contexto

Un recurso es identificado por una cadena de caracteres la cual se compone de subcadenas de caracteres separados por slashes (/), seguido por el nombre del recurso, es decir, el nombre del recurso es el nombre del recurso más la ruta (path) absoluta o relativa. Cada subcadena debe ser un identificador Java válido.  El nombre del recurso puede ser de la forma shortName o shortName.extension. Ambos shortName y extensión deben ser identificadores Java.
Aquí una referencia completa  de identificadores válidos en Java:
https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html
El nombre del recurso es independiente de la implementación de Java, en particular el separador path, el cual siempre será un slash (/). Sin embargo, la implementación de Java controla los detalles de como los recursos son mapeados en un archivo, base de datos u otro objeto capaz de contener al recurso.
Nota: La interpretación del nombre del recurso es relativo a la instancia del class loader. Los métodos implementados por la clase ClassLoader hacen esta interpretación.

Como localiza Java los recursos

Java buscará el recurso dentro del "ambiente" de la aplicación, consideramos "ambiente" a todos los jars y directorios que se encuentren en el Classpath. La búsqueda y carga de recursos en Java es llamado independiente de la localización porque no es relevante donde el código está corriendo, basta tener el ambiente correcto para encontrar los recursos.
Las clases ClassLoader y Class nos proveen métodos para buscar recursos deseados, es necesario comprender bien el contexto de uso de cada uno de ellos para evitarnos problemas en un futuro y para decidir cual método usar.

Usando métodos de la clase java.lang.Class, búsqueda relativa y absoluta


La clase Class nos proporciona los siguientes dos metodos para la carga de recursos.

public URL getResource(String name)
public InputStream getResourceAsStream(String name)

El metodo getResource() retorna una instancia URL para el recurso. Si el recurso no existe o no está visible debido a consideraciones de seguridad, el metodo retorna null.

Si el código cliente necesita leer el contenido del recurso como una instancia InputStream, se puede hacer una llamada al metodo openStream() de la instancia URL.  Mejor aun, puedes hacer uso del metodo getResourceAsStream() de la clase Class, ambas formas son equivalentes, la unica diferencia es que el método getResourceAsStream() atrapa la excepción IOException y retorna una instancia null de InputStream.
Incluso podemos ir más lejos, si sabemos que el recurso a leer es una imagen podemos obtener el contenido llamando al metodo getContent() de la clase URL, este metodo nos devuelve una instancia de la clase awt.image.ImageProducer y apartir de aquí podemos pintar la imagen en un Component

Los metodos getResource y getResourceAsStream encuentran un recurso a partir del nombre proporcionado, esta busqueda puede ser relativa al paquete del objeto Class o absoluta. Si el recurso con el nombre especificado no se encuentra retornan una instancia null. Hay un conjunto de reglas que se aplican a la busqueda de recursos, estas reglas son implementadas por el ClassLoader de la instancia Class.
Los metodos  getResource y getResourceAsStream de la clase Class delegan la tarea a los métodos de la clase ClassLoader, no sin antes resolver el nombre del recurso de acuerdo a ciertas reglas.

Resolver el nombre del recurso consiste en lo siguiente: si el nombre del recurso no es absoluto, se obtiene el nombre del paquete de la clase asociada al objeto Class, se reemplazan todos los caracteres "." por "/"  y se le antepone al nombre del recurso. En otro caso, si el recurso es absoluto solo se elimina el caracter inicial "/". El nombre de un recurso se considera absoluto si empieza con "/".

Nota: Absoluto respecto a nuestro ambiente, el cual es definido por el ClassLoader y no absoluto con respecto al sistema de archivos del sistema operativo.

Nota: Recordar que después de resolver el nombre del recurso, los métodos de la clase Class delegan la tarea a los métodos de la clase ClassLoader

Usando métodos de la clase ClassLoader

La clase ClassLoader tiene dos metodos para localizar y leer recursos

public URL getResource(String name)
public InputStream getResourceAsStream(String name)

Repito, estos dos métodos son usados por los métodos de la clase Class. En un ambiente Java nos vamos a encontrar con más de un ClassLoader, pudiendo haber una relación entre ellos, cada ClassLoader tiene asociado un directorio al que tiene acceso para cargar las clases, siendo el System Class Loader el que tiene acceso a una lista de directorios, sí, me refiero al Classpath. Es importante conocer cuantos ClassLoader pueden haber en un ambiente Java y como estos están relacionados.

Hay una articulo en la web que explica detalladamente el tema de los ClassLoader, coincido con el autor, así que antes de continuar ver el concepto de ClassLoader 

En resumen, cuando ejecutamos una clase en  la JVM, al menos tres ClassLoader son usados.
  1. Bootstrap class loader.
  2. Extensións class loader.
  3. System class loader

El bootstrap class loader carga las librerías (jars) del núcleo de Java localizado en el directorio <JAVA_HOME>/lib. Esta clase es parte del núcleo de la JVM, esta escrito en código nativo.

El extensions class loader carga las librerías (jars) y directorios que se encuentren en el directorio de extensiones (<JAVA_HOME>/lib/ext, u otro directorio especificado por la propiedad java.ext.dirs ) Esta implementado por la clase sun.misc.Launcher$ExtClassLoader.

El system class loader carga las librerías (jars) y directorios encontrados en el Classpath. Es implementado por la clase sun.misc.Launcher$AppClassLoader.

Las instancias Class Loader siguen una jeraquía, una instancia de Bootstrap class loader puede ser padre de una instancia de Extension class loader, y esta puede ser padre de una instancia de System class loader.
Cuando buscamos un recurso este se busca de arriba hacia abajo, por ejemplo, si a una instancia de System class loader le pedimos un recurso, este se lo pedirá a su padre y su padre se lo pedirá a su padre, así hasta llegar al padre de todos. Si el padre no tiene el recurso se buscará en el hijo, si este no lo tiene se lo pedirá a su hijo, así hasta llegar al System class loader. Todos los Class Loader buscan el recurso de forma absoluta, absoluta a cada uno de los  jar o directorios asociado.

Ahora si, con todos los fundamentos mencionados, es momento de ir a la practica.

Localizar un recurso ubicado en el mismo paquete donde se encuentra la clase que lo necesita

Tengo el siguiente proyecto ya compilado y estructurado como lo muestra la imagen


Vamos a localizar el recurso "duke_wave.png" el cual se encuentra en el mismo paquete que la clase "ResourceLocator". Este recurso está ubicado en dos lugares, dentro de mi proyecto, el cual tiene la ruta absoluta "C:\devel\src\personal\tutorials\tutorials\src\com\rlopez\tutorials\resources" , absoluta en relación al sistema de archivos del Sistema Operativo.
Una vez compilemos el proyecto, explicitamente o ya sea que Netbeans lo haga de forma automática, vamos a tener otro recurso "duke_wave.png", el cual estará ubicado en la ruta absoluta "C:\devel\src\personal\tutorials\tutorials\build\classes\com\rlopez\tutorials\resources", absoluta en relación al sistema de archivos del Sistema Operativo.

Para localizar nuestro recurso lo podemos hacer de dos formas, relativa a nuestra clase o absoluto a nuestro ambiente.

Búsqueda relativa a nuestra clase

public static URL getRelativeResource(String resourceName){
   Class clazz = ResourceLocator.class;
   URL url = clazz.getResource(resourceName);
   return url;
}

Búsqueda absoluta en nuestro ambiente

Al ejecutar la clase main desde Netbeans, se agrega el directorio "C:\devel\src\personal\tutorials\tutorials\build\classes\"al classpath de la aplicación, este directorio pasa a ser parte del ambiente de la aplicación.

public static URL getAbsoluteResource(String resourceName){
   Class clazz = ResourceLocator.class;
   String rpath = "/com/rlopez/tutorials/resources/" + resourceName; 
   URL url = clazz.getResource(rpath);
   return url;
}

Haciendo uso de nuestros métodos

/*
 * Tutorials
 * Copyright (C) 20017 Roberto Lopez marcos.roberto.lopez@gmail.com
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 * 
 * Roberto Lopez
 * CDMX, México
 * Email: marcos.roberto.lopez@gmail.com
 */

package com.rlopez.tutorials.resources;

import java.net.URL;
/**
 *
 * @author Roberto Lopez
 */
public class ResourceLocator {
 
 public static void main(String args[]){
  System.out.println("The App Classpath:" + System.getProperty("java.class.path"));
  System.out.println("Test get resource using relative way");
  URL url = ResourceLocator.getRelativeResource("duke_wave.png");
  System.out.println("Resource [Relative]:" + url.getFile());
  
  System.out.println("Test get resource using Absolute way");
  URL urlAbs = ResourceLocator.getAbsoluteResource("duke_wave.png");
  System.out.println("Resource [Absolute]:" + urlAbs.getFile());
 }
 
 public static URL getRelativeResource(String resourceName){
  Class clazz = ResourceLocator.class;
  URL url = clazz.getResource(resourceName);
  return url;
 }
 
 public static URL getAbsoluteResource(String resourceName){
  Class clazz = ResourceLocator.class;
  String rpath = "/com/rlopez/tutorials/resources/" + resourceName;
  URL url = clazz.getResource(rpath);
  return url;
 }
}

Run:
The App Classpath:C:\devel\src\personal\tutorials\tutorials\build\classes
Test get resource using relative way
Resource [Relative]:/C:/devel/src/personal/tutorials/tutorials/build/classes/com/rlopez/
tutorials/resources/duke_wave.png
Test get resource using Absolute way
Resource [Absolute]:/C:/devel/src/personal/tutorials/tutorials/build/classes/com/rlopez/
tutorials/resources/duke_wave.png


Localizar un recurso en otro paquete dentro del mismo jar donde se encuentra la clase que necesita leer el recurso
Modificamos el proyecto agregando un nuevo paquete

Nuevamente vamos a usar los métodos que definimos anteriormente y vamos a buscar el recurso de forma relativa y absoluta.
En la búsqueda relativa iremos al directorio superior con el comando ".." y hemos realizado una modificación a la búsqueda absoluta.

/*
 * Tutorials
 * Copyright (C) 20017 Roberto Lopez marcos.roberto.lopez@gmail.com
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 * 
 * Roberto Lopez
 * CDMX, México
 * Email: marcos.roberto.lopez@gmail.com
 */

package com.rlopez.tutorials.resources;

import java.net.URL;
/**
 *
 * @author Roberto Lopez
 */
public class ResourceLocator {
 
 public static void main(String args[]){
  System.out.println("The App Classpath:" + System.getProperty("java.class.path"));
  System.out.println("Test get resource using relative way");
  URL url = ResourceLocator.getRelativeResource("../images/duke_wave.png");
  System.out.println("Resource [Relative]:" + url.getFile());
  
  System.out.println("Test get resource using Absolute way");
  URL urlAbs = ResourceLocator.getAbsoluteResource("/com/rlopez/tutorials/images/
duke_wave.png");
  System.out.println("Resource [Absolute]:" + urlAbs.getFile());
 }
 
 public static URL getRelativeResource(String resourceName){
  Class clazz = ResourceLocator.class;
  URL url = clazz.getResource(resourceName);
  return url;
 }
 
 public static URL getAbsoluteResource(String resourceName){
  Class clazz = ResourceLocator.class;
  if(!resourceName.startsWith("/")){
   resourceName = "/" + resourceName;
  }
  URL url = clazz.getResource(resourceName);
  return url;
 }
}

run:
The App Classpath:C:\devel\src\personal\tutorials\tutorials\build\classes
Test get resource using relative way
Resource [Relative]:/C:/devel/src/personal/tutorials/tutorials/build/classes/com/rlopez/
tutorials/images/duke_wave.png
Test get resource using Absolute way
Resource [Absolute]:/C:/devel/src/personal/tutorials/tutorials/build/classes/com/rlopez/
tutorials/images/duke_wave.png

En un paquete dentro de un jar distinto al jar donde se encuentra la clase que necesita leer el recurso



Localizar un recurso en un directorio, donde el directorio se encuentra en el classpath.

Pendiente

Localizar un recurso en algún lugar arbitrario dentro de una aplicación web y fuera del jar donde se encuentra la clase que necesita leer el recurso

Pendiente

Ultimas consideraciones
Pendiente

Referencia:
http://www.thinkplexx.com/
http://docs.oracle.com/javase
http://docs.oracle.com/javase/7

No hay comentarios:

Publicar un comentario

Hola A todos