/* Copyright (C) 2011 [Gobierno de Espana]
* This file is part of "Cliente @Firma".
* "Cliente @Firma" 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.
* - or The European Software License; either version 1.1 or (at your option) any later version.
* Date: 11/01/11
* You may contact the copyright holder at: soporte.afirma5@mpt.es
*/
package es.gob.jmulticard;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Locale;
import java.util.logging.Logger;
/** Métodos generales de utilidad para toda la aplicación.
* @version 0.3 */
public final class AOUtil {
private AOUtil() {
// No permitimos la instanciacion
}
private static final int BUFFER_SIZE = 4096;
private static final Logger LOGGER = Logger.getLogger("es.gob.jmulticard"); //$NON-NLS-1$
private static final String[] SUPPORTED_URI_SCHEMES = new String[] {
"http", "https", "file", "urn" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
};
/** Crea una URI a partir de un nombre de fichero local o una URL.
* @param file Nombre del fichero local o URL
* @return URI (<code>file://</code>) del fichero local o URL
* @throws URISyntaxException Si no se puede crear una URI soportada a partir de la cadena de entrada */
public static URI createURI(final String file) throws URISyntaxException {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("No se puede crear una URI a partir de un nulo"); //$NON-NLS-1$
}
String filename = file.trim();
if ("".equals(filename)) { //$NON-NLS-1$
throw new IllegalArgumentException("La URI no puede ser una cadena vacia"); //$NON-NLS-1$
}
// Cambiamos los caracteres Windows
filename = filename.replace('\\', '/');
// Realizamos los cambios necesarios para proteger los caracteres no
// seguros
// de la URL
filename =
filename.replace(" ", "%20") //$NON-NLS-1$ //$NON-NLS-2$
.replace("<", "%3C") //$NON-NLS-1$ //$NON-NLS-2$
.replace(">", "%3E") //$NON-NLS-1$ //$NON-NLS-2$
.replace("\"", "%22") //$NON-NLS-1$ //$NON-NLS-2$
.replace("{", "%7B") //$NON-NLS-1$ //$NON-NLS-2$
.replace("}", "%7D") //$NON-NLS-1$ //$NON-NLS-2$
.replace("|", "%7C") //$NON-NLS-1$ //$NON-NLS-2$
.replace("^", "%5E") //$NON-NLS-1$ //$NON-NLS-2$
.replace("[", "%5B") //$NON-NLS-1$ //$NON-NLS-2$
.replace("]", "%5D") //$NON-NLS-1$ //$NON-NLS-2$
.replace("`", "%60"); //$NON-NLS-1$ //$NON-NLS-2$
final URI uri = new URI(filename);
// Comprobamos si es un esquema soportado
final String scheme = uri.getScheme();
for (final String element : SUPPORTED_URI_SCHEMES) {
if (element.equals(scheme)) {
return uri;
}
}
// Si el esquema es nulo, aun puede ser un nombre de fichero valido
// El caracter '#' debe protegerse en rutas locales
if (scheme == null) {
filename = filename.replace("#", "%23"); //$NON-NLS-1$ //$NON-NLS-2$
return createURI("file://" + filename); //$NON-NLS-1$
}
// Miramos si el esquema es una letra, en cuyo caso seguro que es una
// unidad de Windows ("C:", "D:", etc.), y le anado el file://
// El caracter '#' debe protegerse en rutas locales
if (scheme.length() == 1 && Character.isLetter((char) scheme.getBytes()[0])) {
filename = filename.replace("#", "%23"); //$NON-NLS-1$ //$NON-NLS-2$
return createURI("file://" + filename); //$NON-NLS-1$
}
throw new URISyntaxException(filename, "Tipo de URI no soportado"); //$NON-NLS-1$
}
/** Obtiene el flujo de entrada de un fichero (para su lectura) a partir de su URI.
* @param uri URI del fichero a leer
* @return Flujo de entrada hacia el contenido del fichero
* @throws IOException Cuando no se ha podido abrir el fichero de datos. */
public static InputStream loadFile(final URI uri) throws IOException {
if (uri == null) {
throw new IllegalArgumentException("Se ha pedido el contenido de una URI nula"); //$NON-NLS-1$
}
if (uri.getScheme().equals("file")) { //$NON-NLS-1$
// Es un fichero en disco. Las URL de Java no soportan file://, con
// lo que hay que diferenciarlo a mano
// Retiramos el "file://" de la uri
String path = uri.getSchemeSpecificPart();
if (path.startsWith("//")) { //$NON-NLS-1$
path = path.substring(2);
}
return new FileInputStream(new File(path));
}
// Es una URL
final InputStream tmpStream = new BufferedInputStream(uri.toURL().openStream());
// Las firmas via URL fallan en la descarga por temas de Sun, asi que
// descargamos primero
// y devolvemos un Stream contra un array de bytes
final byte[] tmpBuffer = getDataFromInputStream(tmpStream);
return new java.io.ByteArrayInputStream(tmpBuffer);
}
/** Lee un flujo de datos de entrada y los recupera en forma de array de
* bytes. Este método consume pero no cierra el flujo de datos de
* entrada.
* @param input
* Flujo de donde se toman los datos.
* @return Los datos obtenidos del flujo.
* @throws IOException
* Cuando ocurre un problema durante la lectura */
public static byte[] getDataFromInputStream(final InputStream input) throws IOException {
if (input == null) {
return new byte[0];
}
int nBytes = 0;
final byte[] buffer = new byte[BUFFER_SIZE];
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((nBytes = input.read(buffer)) != -1) {
baos.write(buffer, 0, nBytes);
}
return baos.toByteArray();
}
/** Obtiene el nombre común (Common Name, CN) del titular de un
* certificado X.509. Si no se encuentra el CN, se devuelve la unidad organizativa
* (Organization Unit, OU).
* @param c
* Certificado X.509 del cual queremos obtener el nombre
* común
* @return Nombre común (Common Name, CN) del titular de un
* certificado X.509 */
public static String getCN(final X509Certificate c) {
if (c == null) {
return null;
}
return getCN(c.getSubjectX500Principal().toString());
}
/** Obtiene el nombre común (Common Name, CN) de un <i>Principal</i>
* X.400. Si no se encuentra el CN, se devuelve la unidad organizativa
* (Organization Unit, OU).
* @param principal
* <i>Principal</i> del cual queremos obtener el nombre
* común
* @return Nombre común (Common Name, CN) de un <i>Principal</i>
* X.400 */
public static String getCN(final String principal) {
if (principal == null) {
return null;
}
String rdn = getRDNvalueFromLdapName("cn", principal); //$NON-NLS-1$
if (rdn == null) {
rdn = getRDNvalueFromLdapName("ou", principal); //$NON-NLS-1$
}
if (rdn != null) {
return rdn;
}
final int i = principal.indexOf('=');
if (i != -1) {
LOGGER .warning("No se ha podido obtener el Common Name ni la Organizational Unit, se devolvera el fragmento mas significativo"); //$NON-NLS-1$
return getRDNvalueFromLdapName(principal.substring(0, i), principal);
}
LOGGER.warning("Principal no valido, se devolvera la entrada"); //$NON-NLS-1$
return principal;
}
/** Recupera el valor de un RDN (<i>Relative Distinguished Name</i>) de un principal. El valor de retorno no incluye
* el nombre del RDN, el igual, ni las posibles comillas que envuelvan el valor.
* La función no es sensible a la capitalización del RDN. Si no se
* encuentra, se devuelve {@code null}.
* @param rdn RDN que deseamos encontrar.
* @param principal Principal del que extraer el RDN (según la <a href="http://www.ietf.org/rfc/rfc4514.txt">RFC 4514</a>).
* @return Valor del RDN indicado o {@code null} si no se encuentra. */
public static String getRDNvalueFromLdapName(final String rdn, final String principal) {
int offset1 = 0;
while ((offset1 = principal.toLowerCase(Locale.US).indexOf(rdn.toLowerCase(), offset1)) != -1) {
if (offset1 > 0 && principal.charAt(offset1-1) != ',' && principal.charAt(offset1-1) != ' ') {
offset1++;
continue;
}
offset1 += rdn.length();
while (offset1 < principal.length() && principal.charAt(offset1) == ' ') {
offset1++;
}
if (offset1 >= principal.length()) {
return null;
}
if (principal.charAt(offset1) != '=') {
continue;
}
offset1++;
while (offset1 < principal.length() && principal.charAt(offset1) == ' ') {
offset1++;
}
if (offset1 >= principal.length()) {
return ""; //$NON-NLS-1$
}
int offset2;
if (principal.charAt(offset1) == ',') {
return ""; //$NON-NLS-1$
}
else if (principal.charAt(offset1) == '"') {
offset1++;
if (offset1 >= principal.length()) {
return ""; //$NON-NLS-1$
}
offset2 = principal.indexOf('"', offset1);
if (offset2 == offset1) {
return ""; //$NON-NLS-1$
}
else if (offset2 != -1) {
return principal.substring(offset1, offset2);
}
else {
return principal.substring(offset1);
}
}
else {
offset2 = principal.indexOf(',', offset1);
if (offset2 != -1) {
return principal.substring(offset1, offset2).trim();
}
return principal.substring(offset1).trim();
}
}
return null;
}
/** Caracterres aceptados en una codificación Base64 según la
* <a href="http://www.faqs.org/rfcs/rfc3548.html">RFC 3548</a>. Importante:
* Añadimos el carácter ˜ porque en ciertas
* codificaciones de Base64 está aceptado, aunque no es nada
* recomendable */
private static final String BASE_64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz=_-\n+/0123456789\r~"; //$NON-NLS-1$
/** @param data Datos a comprobar si podr6iacute;an o no ser Base64.
* @return <code>true</code> si los datos proporcionado pueden ser una
* codificación base64 de un original binario (que no tiene
* necesariamente porqué serlo), <code>false</code> en caso
* contrario. */
public static boolean isBase64(final byte[] data) {
int count = 0;
// Comprobamos que todos los caracteres de la cadena pertenezcan al
// alfabeto base 64
for (final byte b : data) {
if (BASE_64_ALPHABET.indexOf((char) b) == -1) {
return false;
}
if (b != '\n' && b != '\r') {
count++;
}
}
// Comprobamos que la cadena tenga una longitud multiplo de 4 caracteres
return count % 4 == 0;
}
/** Equivalencias de hexadecimal a texto por la posición del vector.
* Para ser usado en <code>hexify()</code> */
private static final char[] HEX_CHARS = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
/** Convierte un vector de octetos en una cadena de caracteres que contiene
* la representación hexadecimal. Copiado directamente de
* opencard.core.util.HexString
* @param abyte0
* Vector de octetos que deseamos representar textualmente
* @param separator
* Indica si han o no de separarse los octetos con un
* guión y en líneas de 16
* @return Representación textual del vector de octetos de entrada */
public static String hexify(final byte abyte0[], final boolean separator) {
if (abyte0 == null) {
return "null"; //$NON-NLS-1$
}
final StringBuffer stringbuffer = new StringBuffer(256);
int i = 0;
for (int j = 0; j < abyte0.length; j++) {
if (separator && i > 0) {
stringbuffer.append('-');
}
stringbuffer.append(HEX_CHARS[abyte0[j] >> 4 & 0xf]);
stringbuffer.append(HEX_CHARS[abyte0[j] & 0xf]);
++i;
if (i == 16) {
if (separator && j < abyte0.length - 1) {
stringbuffer.append('\n');
}
i = 0;
}
}
return stringbuffer.toString();
}
/** Convierte un vector de octetos en una cadena de caracteres que contiene
* la representación hexadecimal. Copiado directamente de
* opencard.core.util.HexString
* @param abyte0
* Vector de octetos que deseamos representar textualmente
* @param separator
* Indica si han o no de separarse los octetos con un
* guión y en líneas de 16
* @return Representación textual del vector de octetos de entrada */
public static String hexify(final byte abyte0[], final String separator) {
if (abyte0 == null) {
return "null"; //$NON-NLS-1$
}
final StringBuffer stringbuffer = new StringBuffer(256);
for (int j = 0; j < abyte0.length; j++) {
if (separator != null && j > 0) {
stringbuffer.append(separator);
}
stringbuffer.append(HEX_CHARS[abyte0[j] >> 4 & 0xf]);
stringbuffer.append(HEX_CHARS[abyte0[j] & 0xf]);
}
return stringbuffer.toString();
}
/** Carga una librería nativa del sistema.
* @param path Ruta a la libreria de sistema.
* @throws IOException Si ocurre algún problema durante la carga */
public static void loadNativeLibrary(final String path) throws IOException {
if (path == null) {
LOGGER.warning("No se puede cargar una biblioteca nula"); //$NON-NLS-1$
return;
}
final int pos = path.lastIndexOf('.');
final File file = new File(path);
final File tempLibrary =
File.createTempFile(pos < 1 ? file.getName() : file.getName().substring(0, file.getName().indexOf('.')),
pos < 1 || pos == path.length() - 1 ? null : path.substring(pos));
// Copiamos el fichero
copyFile(file, tempLibrary);
// Pedimos borrar los temporales cuando se cierre la JVM
if (tempLibrary != null) {
tempLibrary.deleteOnExit();
}
LOGGER.info("Cargamos " + (tempLibrary == null ? path : tempLibrary.getAbsolutePath())); //$NON-NLS-1$
System.load(tempLibrary != null ? tempLibrary.getAbsolutePath() : path);
}
/** Copia un fichero.
* @param source Fichero origen con el contenido que queremos copiar.
* @param dest Fichero destino de los datos.
* @throws IOException SI ocurre algun problema durante la copia */
public static void copyFile(final File source, final File dest) throws IOException {
if (source == null || dest == null) {
throw new IllegalArgumentException("Ni origen ni destino de la copia pueden ser nulos"); //$NON-NLS-1$
}
final FileInputStream is = new FileInputStream(source);
final FileOutputStream os = new FileOutputStream(dest);
final FileChannel in = is.getChannel();
final FileChannel out = os.getChannel();
final MappedByteBuffer buf = in.map(FileChannel.MapMode.READ_ONLY, 0, in.size());
out.write(buf);
in.close();
out.close();
is.close();
os.close();
}
/** Genera una lista de cadenas compuesta por los fragmentos de texto
* separados por la cadena de separación indicada. No soporta
* expresiones regulares. Por ejemplo:<br>
* <ul>
* <li><b>Texto:</b> foo$bar$foo$$bar$</li>
* <li><b>Separado:</b> $</li>
* <li><b>Resultado:</b> "foo", "bar", "foo", "", "bar", ""</li>
* </ul>
* @param text
* Texto que deseamos dividir.
* @param sp
* Separador entre los fragmentos de texto.
* @return Listado de fragmentos de texto entre separadores.
* @throws NullPointerException
* Cuando alguno de los parámetros de entrada es {@code null}. */
public static String[] split(final String text, final String sp) {
final ArrayList<String> parts = new ArrayList<String>();
int i = 0;
int j = 0;
while (i != text.length() && (j = text.indexOf(sp, i)) != -1) {
if (i == j) {
parts.add(""); //$NON-NLS-1$
}
else {
parts.add(text.substring(i, j));
}
i = j + sp.length();
}
if (i == text.length()) {
parts.add(""); //$NON-NLS-1$
}
else {
parts.add(text.substring(i));
}
return parts.toArray(new String[0]);
}
}