Blog

OpenCms 7.5 sobre Tomcat I-Instalación

En este post voy a explicaros los pasos iniciales en la instalación y configuración de OpenCms (gestor de contenidos) para montar un site, en este caso para la Universidad de Valladolid.

Lo primero es descargar opencms.war de www.opencms.org. Una vez descargado debemos copiarlo en la carpeta webapps del Tomcat. Una vez hecho, y con el tomcat en marcha podemos abrir:

http://localhost:8080/opencms/setup

Nos aparecerá la ventana de la licencia, que deberemos aceptar.

instalacion1

A continuación la instalación chequea la instalación del tomcat para comprobar que tenemos todos los componentes necesarios. En caso de que falte alguno deberemos de añadirlo al tomcat.

instalacion2

El siguiente paso es dar los datos de conexión a la base de datos que usará OpenCms.

instalacion3

El penúltimo paso es seleccionar los módulos a instalar. Para el desarrollo o nuestra primera instalación de OpenCms es muy recomendable instalar los módulos por defecto.

instalacion4

Por último introducimos el nombre del servidor y comenzamos la instalación. Tardará un buen rato en importar los módulos así que debemos de tener un poquito de paciencia. Terminado de instalar los móudlos nor indica que debemos leer unas notas importantes y para finalizar que ha bloqueado la instalación.

instalacion5instalacion6

Entonces ya tenemos instalado el OpenCms, pudiendo abrirlo con http://localhost:8080/opencms/opencms/ y la administración del gestor de contenidos la tenemos en http://localhost:8080/opencms/opencms/system/login

Almacenamiento Trecus N8800

Hoy hemos tenido la experiencia de experimentar con un servidor de almacenamiento Trecus N8800 (www.thecus.com) con 12 Teras de almacenamiento en 8 discos de 2TB. El frontal y el contenido de la caja se muestran en las siguientes imagenes

frontal

contenido

Podeis ver que se trata de una placa sencilla con una controladora raid, pero lo importante es lo que permite este software. Al conectarlo a la alimentación no tenemos posibilidad de conectar un teclado o una pantalla, sino que todo se realiza desde el interfaz web. Una vez conectados y asignada la ip podemos empezar a trabajar y ver sus posibilidades.

En primer lugar, el interfaz es sencillo, con mucho Ajax para facilitar su uso. Partiendo de su sencillez podemos empezar a crear los volúmenes, asignar los usuarios y grupos y empezar a usar uno de las múltiples configuraciones RAID que trae. Si ya tenemos un Active Directory podemos enlazar la gestión de usuarios con él.

Otra cosa interesante es que permite instalar módulos (en la web en support->module), encontrando dos muy interesantes, un módulo para montar un servidor web bastante completo con soporte para php5 y mysql, y otro que también nos permite instalar un MySQL gestionable desde un phpMyAdmin.

Con esto vemos muchas posibilidades al servidor. Podemos usarle simplemente como almacenamiento a través de Samba o iSCSI por ejemplo, podemos usarlo como servidor de base de datos e incluso como un servidor web. La ventaja es que usa discos SATA por lo que el coste del almacenamiento no es caro y podemos compararlo con el coste de un servidor normal.

Google Search Appliance (GSA)

Hemos decidido mejorar el sistema de búsqueda ante las múltiples quejas de los usuarios, y nos hemos pasado al GSA (Google Search Appliance). En primer lugar sorprende el colorcito amarillo de la caja tras desembalarlo, pero como gente de sistemas que somos no pudimos evitar abrirlo para ver sus tripas, y resulta ser un servidor Dell con 48Gb de memoria Ram y 6 discos de 750Gb… vamos, un maquinón. Como sistema operativo lleva un Centos tuneado por Google que no nos deja ninguna posibilidad de meterle mano, ya que todo se realiza a través de un interfaz web, desde su configuración inicial de los parámetros de red hasta cualquier acción posterior con el servidor. Sorprende que por ejemplo no tengamos la llave para poder abrir la zona frontal, que nos permitiría cambiar los discos. Ya veremos si se rompe alguno cómo se soluciona.

En cuanto al buscador en sí la verdad es que me sorprende la eficiencia y las cosas que encuentra. Nos hemos tirado casi dos meses (con autoformación, prueba y error) hasta lograr evitar que buscase más de lo debido. Encuentra cualquier cosa, saltándose incluso el robots (que no deja de ser una buena práctica por parte de los buscadores) y registrando claves, ficheros y todo lo imaginable. Además nuestra web tiene un problema, y es que se puede llegar a un mismo sitio con múltiples parámetros, lo que nos hacía tener una misma página encontrada con distintos parámetros, y para el GSA con que cambie un parámetro ya es una página distinta. Poco a poco, con expresiones regulares y preparando algunas páginas de consulta específicas para el GSA hemos logrado evitar estos problemas, y la verdad es que estamos bastante contentos. Lo hemos puesto en explotación y ahora esperaremos el resultado de los usuarios, que eran los que principalmente se quejaban de que no encontraban nada.

Os pongo una imagen de las tripas del GSA.

GSA
GSA

Aumentar el tamaño de upload de ficheros en IIS6 con asp

El problema es sencillo, los usuarios no podían subir ficheros mayores de 4096Kb o 4Mb… la solución está en aumentar dos parámetros de IIS6, que podemos encontrar en el fichero c:\windows\system32\inetserv\metabase.xml

Probamos a tocar muchso parámetros pero los que realmente funcionaron fueron:

  • AspBufferingLimit de 4194304 a 104857600
  • AspMaxRequestEntityAlloweb de 204800 a 104857600

Con esto aumentamos el límite de subida y se acabaron los problemas.

Espero que os ayude.

Descargar un fichero de una BD con Struts2

Bueno, parece que en a descarga de ficheros tenía problemas con el IE7, así que tuvimos que reformarla, y aprovecho para contaros un poco la experiencia.

Lo primero, usar el resultado «stream» que nos proporciona struts… de manera que la configuración de la acción queda:

<action name=«download» class=«es.uva.aplicaciones.download.struts2.DownloadAdjunto»>

<result name=«success» type=«stream»>

<param name=«contentType»>application/msword</param>

<param name=«inputName»>inputStream</param>

<param name=«contentDisposition»>filename=»PLBValladolid.doc»</param>

<param name=«bufferSize»>1024</param>

</result>

<result name=«error»>/error.jsp</result>

</action>

La clase debe tener el método getInputStream o el correspondiente a la variable InputName que hemos puesto en el struts.xml. En el método execute es donde debemos cargar el fichero de la base de datos y asignarle al inputStream. En mi caso es coger un tipo blob de MySql a través de hibernate.


private InputStream inputStream=null;

public InputStream getInputStream() {
return inputStream;
}

public void setInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}

public String execute() {
HttpServletRequest request = (HttpServletRequest) ServletActionContext
.getRequest();
HttpServletResponse response = (HttpServletResponse) ServletActionContext
.getResponse();

if (request.getParameter(«id») != null) {
String id = request.getParameter(«id»);
file = dao.findAdjunto(id);
} else {
// ERROR: id no encontrado
addActionError(rb.getString(«errors.sinparametro»));
return ERROR;
}
if (file != null) {
response.setContentType(file.getTipo());
response.setContentLength(file.getSize().intValue());
response.setHeader(«Content-Disposition»,»inline; document.fileName=» + file.getNombre() + «»);
response.setHeader(«Cache-Control»,»must-revalidate, post-check=0, pre-check=0″);
response.setHeader(«Pragma», «public»);
response.setDateHeader(«Expires», 0);

Blob blob = file.getDatos();
try {
this.setInputStream(blob.getBinaryStream());
} catch (Exception e) {
System.err.println(e);
addActionError(rb.getString(«errors.escrituraencliente»));
return ERROR;
}
return SUCCESS;
} else {
//Fichero no encontrado
addActionError(rb.getString(«errors.ficheronoencontrado»));
return ERROR;
}
}

Problemas al autenticar un usuario en Samba con Vista

He estado un par de horas dandole vueltas a un fallo que tenía, y es que desde XP me conectaba a un alojamiento samba sin problemas pero desde Vista no… mucho investigar hasta descubrir la solución.

El error que me dejaba en los logs del samba era «libsmb/ntlm_check.c:smb_pwd_check_ntlmv1(55)  smb_pwd_check_ntlmv1: incorrect password length (70)»

Resulta que en Vista está configurado para utilizar sólo NTLM versión 2 (NTLMv2) cuando los equipos cliente utilizan autenticación de desafío y respuesta para autenticar a los servidores de autenticación. Este comportamiento predeterminado puede provocar problemas cuando Windows autentica con Samba….

La solución, se supone que ejecutando «secpol.msc» pero si como en mi caso no lo tienes, debes hacer lo siguiente:

  1. Haga clic en inicio , haga clic en Ejecutar , escriba regedit en el cuadro Abrir y, a continuación, haga clic en ACEPTAR .
  2. Busque y haga clic en la siguiente subclave:
    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\LSA
  3. En el panel de detalles, haga doble clic en LMCompatibilityLevel .
  4. En el cuadro de datos de valor , escriba el valor adecuado para su escenario y, a continuación, haga clic en ACEPTAR .

¿Qué valor poner? Veamos las posibilidades:

Valor Descripción Las notas
0 Enviar respuestas de LAN Manager (LM) y respuestas de NTLM. Los equipos cliente envían respuestas de LM y NTLM respuestas. Equipos cliente no utilizan nunca la seguridad de sesión de NTLMv2. Los controladores de dominio aceptan autenticación LM, la autenticación NTLM y la autenticación NTLMv2.
1 Enviar la autenticación de LAN Manager y la autenticación de NTLM y utilizar la seguridad de sesión de NTLMv2 si se negocia. Los equipos cliente utilizan la autenticación de LAN Manager y la autenticación de NTLM. Los equipos cliente utilizan la seguridad de sesión de NTLMv2 si el servidor admite la seguridad de sesión de NTLMv2. Los controladores de dominio aceptan autenticación LM, la autenticación NTLM y la autenticación NTLMv2.
2 Enviar respuestas de NTLM sólo. Los equipos cliente utilizan sólo la autenticación NTLM. Los equipos cliente utilizan la seguridad de sesión de NTLMv2 si el servidor admite la seguridad de sesión de NTLMv2. Los controladores de dominio aceptan autenticación LM, la autenticación NTLM y la autenticación NTLMv2.
3 Enviar únicamente respuestas NTLMv2. Los equipos cliente utilizan sólo la autenticación NTLMv2. Los equipos cliente utilizan la seguridad de sesión de NTLMv2 si el servidor admite la seguridad de sesión de NTLMv2. Los controladores de dominio aceptan autenticación LM, la autenticación NTLM y la autenticación NTLMv2.
4 Enviar respuestas de NTLM sólo y rechaza la autenticación LM. Los equipos cliente utilizan sólo la autenticación NTLM. Los equipos cliente utilizan la seguridad de sesión de NTLMv2 si el servidor admite la seguridad de sesión de NTLMv2. Los controladores de dominio rechaza la autenticación LM. Los controladores de dominio aceptan la autenticación NTLM y la autenticación NTLMv2.
5 Enviar únicamente respuestas NTLMv2 y rechace la autenticación de LAN Manager y la autenticación de NTLM. Los equipos cliente utilizan sólo la autenticación NTLMv2. Los equipos cliente utilizan la seguridad de sesión de NTLMv2 si el servidor admite la seguridad de sesión de NTLMv2. Los controladores de dominio rechaza la autenticación de LAN Manager y la autenticación de NTLM. Los controladores de dominio aceptan sólo la autenticación NTLMv2.

 

Y la respuesta correcta es el 1. Con esto funciona sin problemas. Espero que os sirva de ayuda.

Memoria con tomcat

¿Quien no ha tenido problemas de memoria en tomcat? el famoso java.lang.OutOfMemoryError: PermGen space. Esta memoria PermGen es usada para almacenar el código de las aplicaciones que corren en ella.

¿Por qué me quedo sin memoria? Debemos entender que no sólo ocupa memoria nuestro código, sino también las librerias, y esto pueden ser unos cuantos Mb que vamos metiendo en memoria.

¿Cómo saber en qué valores de memoria nos andamos? Tenemos dos comandos Java: jps y jstat que vienen en el JDK.

  • El jps es como un ps de Unix, nos indica los procesos Java que están en ejecución, devolviendo el PID del programa.
  • El jstat nos permite ver la memoria que usa ese proceso. El parámetro para ver la capacidad actual es «-gccapacity PID» y para medir sólo la memoria PermGen es «-gcpermcapacity PID»

Ejemplos:

[www@villacarralon scripts]$ jps
25873 Jps
18156 Bootstrap
[www@villacarralon scripts]$ ps -ef | grep java
www 18156 1 0 09:12 pts/0 00:00:49 /servicio/jdk1.5.0_11//bin/java -server ... org.apache.catalina.startup.Bootstrap start
www 25882 18101 0 11:08 pts/0 00:00:00 grep java


jstat -gccapacity 18156
NGCMN NGCMX NGC S0C S1C EC OGCMN OGCMX OGC OC PGCMN PGCMX PGC PC YGC FGC
58240,0 116480,0 58304,0 8128,0 8640,0 41024,0 466048,0 932096,0 466048,0 466048,0 131072,0 262144,0 131072,0 131072,0 24 0


jstat -gcpermcapacity 18156
PGCMN PGCMX PGC PC YGC FGC FGCT GCT
131072,0 262144,0 131072,0 131072,0 24 0 0,000 0,555

Tenemos tres valores interesantes a observar para la memoria PermGen:

  • el PGCMN: el tamaño mínimo reservado para la memoria PermGen
  • el PGCMX: la memoria máxima que puede usar (en KB) 
  • el PGC, la memoria usada actualmente.

Si vemos que el PGC se aproxima al PGCMX deberíamos de cambiar la opción del tomcat aumentando el tamaño máximo con -XX:MaxPermSize=256m a por ejemplo -XX:MaxPermSize=512m siempre que nuestro equipo nos lo permita.

Si necesitamos monitorizarlo durante una ejecución, podemos plantearnos hacer un scriptillo como:

#!/bin/bash
jps | grep Bootstrap | awk '{print "while true; do jstat -gcpermcapacity " $1 "; sleep 5; done" }' | bash

Hibernate con dos o mas orígenes de datos

Se me planteó un problema, ¿cómo hacer que hibernate tenga más de un orígen de datos? Estuve investigando y aunque encontré varias soluciones, no encontré la ideal, así que hice una mezcla de varias de ellas. Lo primero fue crear una clase HibernateUtil que os copio después, y para utilizarla es muy sencillo.

Primero la inicializamos en mi caso con un origen de datos por defecto y luego otros dos, uno para mysql y otro para una base de datos oracle:

HibernateUtil.createSessionFactory();
HibernateUtil.buildSessionFactory("oracle", "hibernate.oracle.cfg.xml");
HibernateUtil.buildSessionFactory("mysql", "hibernate.mysql.cfg.xml");

Estos ficheros deben estar en el mismo punto que a clase HibernateUtil, sino deberéis modificar su ruta según vuestro interés.

A continuación podemos guardar o leer usando Hibernate de la forma habitual:

public void save(Formulario objeto) {
Session session=HibernateUtil.currentSession("mysql");
Transaction tx = null;
try {
tx = session.beginTransaction();
session.save(objeto);
tx.commit();
} catch (RuntimeException e) {
if (tx != null) {
logger.error("Error al guardar " + e);
}
tx.rollback();
  }
}

 
Y ya por fin, el código de la clase HibernateUtil

package es.uva.pdf.dao;

import java.net.URL;
import java.util.HashMap;

import org.apache.log4j.Logger;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

import com.sun.tools.javac.util.Log;

import es.uva.pdf.struts2.action.ListadoPersonal;

public class HibernateUtil {

private static Logger log = Logger.getLogger(HibernateUtil.class);

private static HashMap sessionFactoryMap = new HashMap();

public static final ThreadLocal sessionMapsThreadLocal = new ThreadLocal();

public static Session currentSession(String key) throws HibernateException {

HashMap sessionMaps = (HashMap) sessionMapsThreadLocal
.get();

if (sessionMaps == null) {
sessionMaps = new HashMap();
sessionMapsThreadLocal.set(sessionMaps);
}

// Open a new Session, if this Thread has none yet
Session s = (Session) sessionMaps.get(key);
if (s == null) {
s = ((SessionFactory) sessionFactoryMap.get(key)).openSession();
sessionMaps.put(key, s);
}

return s;
}

public static Session currentSession() throws HibernateException {
return currentSession("");
}

public static void closeSessions() throws HibernateException {
HashMap sessionMaps = (HashMap) sessionMapsThreadLocal
.get();
sessionMapsThreadLocal.set(null);
if (sessionMaps != null) {
for (Session session : sessionMaps.values()) {
if (session.isOpen())
session.close();
}
;
}
}

public static void closeSession() {
HashMap sessionMaps = (HashMap) sessionMapsThreadLocal
.get();
sessionMapsThreadLocal.set(null);
if (sessionMaps != null) {
Session session = sessionMaps.get("");
if (session != null && session.isOpen())
session.close();
}
}

public static void closeSession(String key) {
HashMap sessionMaps = (HashMap) sessionMapsThreadLocal
.get();
if (sessionMaps != null) {
Session session = sessionMaps.get(key);
if (session != null && session.isOpen())
session.close();
}
}

public static void buildSessionFactories(HashMap configs) {
try {
// Create the SessionFactory
for (String key : configs.keySet()) {
URL url = HibernateUtil.class.getResource(configs.get(key));
SessionFactory sessionFactory = new Configuration().configure(
url).buildSessionFactory();
sessionFactoryMap.put(key, sessionFactory);
}

} catch (Exception ex) {
ex.printStackTrace(System.out);
log.error("Initial SessionFactory creation failed.", ex);
throw new ExceptionInInitializerError(ex);

} // end of the try - catch block
}

public static void buildSessionFactory(String key, String path) {
try {
// Create the SessionFactory
URL url = HibernateUtil.class.getResource(path);
SessionFactory sessionFactory = new Configuration().configure(url)
.buildSessionFactory();
sessionFactoryMap.put(key, sessionFactory);

} catch (Throwable ex) {

log.error("Initial SessionFactory creation failed.", ex);
throw new ExceptionInInitializerError(ex);

} // end of the try - catch block
}

public static void buildSessionFactory() {
// Por defecto
try {
// Create the SessionFactory
SessionFactory sessionFactory = new Configuration().configure()
.buildSessionFactory();
sessionFactoryMap.put("default", sessionFactory);
} catch (Throwable ex) {
log.error("Initial SessionFactory creation failed.", ex);
throw new ExceptionInInitializerError(ex);

} // end of the try - catch block
}

// alias
public static void createSessionFactory() {
buildSessionFactory();
}

>}

Java FDF: Rellenar y procesar formularios pdf

Para mí ha sido todo un descubrimiento. Con FDF puedes rellenar automáticamente los formularios PDF sin ningún problema, y lo más interesante, procesarles. En nuestro caso lo hemos hecho todo a través de J2EE, con Servlets y lo más divertido a sido el tema de los flags de los campos para decir si eran obligatorios, ocultos, sólo lectura, etc.

Los flags F son de anotación, y la posición de los bits son:

  1. invisible
  2. Oculto
  3. Imprimir el campo.
  4. No Zoom
  5. No rotar
  6. No ver
  7. Solo lectura.

Las combinaciones de estos bits nos permiten activando el oculto por ejemplo que nadie vea un campo de un formulario que usamos para almacenar el identificador del formulario, etc.

Los otros flags interesantes son los Ff, de campo:

  1. Sólo lectura, para que el usuario no pueda modificarlo.
  2. Obligatorio, para que el usuario tenga que rellenarlo antes de su envío.
  3. No exportar, es decir, que no se va a enviar al realizar el envío del formulario

Al final, en nuestro servlet tenemos en el doGet el autorellenado de formulario, ya que el usuario se a autenticado previamente, y le rellenamos los datos de nombre, apellido, etc para facilitarle la tarea. El código sería:

public void doGet(HttpServletRequest req, HttpServletResponse res)
throws IOException, ServletException {
FDFDoc outputFDF = null;
/* Create a new FDF. */
outputFDF = new FDFDoc();
outputFDF.SetFlags("AutorizadorUID", FDFItem.FDFSetF, 2); //Invisible
outputFDF.SetFlags("SolicitanteNombre", FDFItem.FDFSetFf, 1); //Solo lectura
outputFDF.SetFlags("SolicitudDireccionesIP", FDFItem.FDFSetFf, 2); //Obligatorio
outputFDF.SetFlags("SolicitudDireccionesIP", FDFItem.FDFClearFf, 1); //Escribible
try {
outputFDF.SetFile(fichPdf);
fillPDF(outputFDF, formulario);
res.setContentType("application/vnd.fdf");
OutputStream out = res.getOutputStream();
outputFDF.Save(out);
out.close();
} catch (FDFException e) {
/* We handle an error by emitting an html header */
res.setContentType("text/html");
PrintWriter out = res.getWriter();
out.println("Caught FDF exception");
out.println(e.toString());
System.err.println("Error "+e);
e.printStackTrace(out);
}
}

En cuanto al procesado del formulario tenemos algo parecido:

try {
FdfInput = null;
// get the length of incoming data
int howMany = req.getContentLength();
byte data[] = new byte[howMany];
// read data into byte array
req.getInputStream().read(data);
// create FDFDoc from data
FdfInput = new FDFDoc(data);
//Get ComunicadorUID value
String comunicadorUID=FdfInput.GetValue("ComunicadorUID");
} catch (FDFException e) {
reportError(req, res, "Error al generar el pdf. " + e.toString());
// Pte: DEjar un log del error
}

y luego obtenidas las variables del formulario, podemos guardarlo en una base de datos, mandar un correo electrónico, generar otro formulario, etc, etc.

Sinceramente, no me esperaba un tratamiento tan sencillo de los pdf’s. Es una muy buena idea, pero con una limitación, necesitamos acrobat reader 7 o superior, sino este invento no funciona.

Pasar un fichero txt a html

Hace poco nos encontramos con el problema de pasar un fichero txt a html. Después de estudiar varias soluciones optamos por Txt to Html Converter una solución OpenSource que parecía solucionar nuestros problemas.

Lo instalamos para lo cual necesitamos antes Perl y los módulos:

  • Module::Build
  • Getopt::Long
  • Getopt::ArgvFile
  • Pod::Usage
  • File::Basename

y el propio de la herramienta, nos dispusimos a hacer unas pruebas.

 

¡Qué sorpresa! Aunque prometía cambiar también los famosos acentos que usamos n nuestra lengua de Cervantes, pues no, no lo hacía. Después de mucho indagar, y el principal motivo de incluirlo en el foro, es que aunque según la documentación debería existir un txt2html.dict en algún sitio, éste no existía y daba igual si lo ponías o se lo pasabas como parámetro, ya que en esta versión la conversión la hace en el código.

 

La solución fue buscar el fichero TextToHTML.pm que es donde guarda la conversión (que debería estar en txt2html.dict), que en nuestro caso estaba en /usr/lib/perl5/site_perl/5.8.8/HTML/TextToHTML.pm y modificarle añadiendo las siguientes reglas:

#ACENTOS

 

|á -h-> &aacute;

|é -h-> &eacute;

|í -h-> &iacute;

|ó -h-> &oacute;

|ú -h-> &uacute;

 

|Á -h-> &aacute;

|É -h-> &eacute;

|Í -h-> &iacute;

|Ó -h-> &oacute;

|Ú -h-> &uacute;

 

|ä -h-> &auml;

|ë -h-> &euml;

|ï -h-> &iuml;

|ö -h-> &ouml;

|ü -h-> &uml;

 

|Ä -h-> &Auml;

|Ë -h-> &Euml;

|Ï -h-> &Iuml;

|Ö -h-> &Ouml;

|Ü -h-> &Uml;

 

|â -h-> &acirc;

|ê -h-> &ecirc;

|î -h-> &icirc;

|ô -h-> &ocirc;

|û -h-> &ucirc;

|Â -h-> &Acirc;

|Ê -h-> &Ecirc;

|Î -h-> &Icirc;

|Ô -h-> &Ocirc;

|Û -h-> &Ucirc;

|à -h-> &agrave;

|è -h-> &egrave;

|ì -h-> &igrave;

|ò -h-> &ograve;

|ù -h-> &ugrave;

|À -h-> &Agrave;

|È -h-> &Egrave;

|Ì -h-> &Igrave;

|Ò -h-> &Ograve;

|Ù -h-> &Ugrave;

|ñ -h-> &ntilde;

|Ñ -h-> &Ntilde;

|Ç -h-> &Ccedil;

|\\n -h-> <br>

|¿ -h-> &iquest;

|¡ -h-> &iexcl;