martes, 31 de mayo de 2011

Reescritura de urls y webforms de ASP.NET

Existen distintas formas de reescribir una URL, un buen articulo es el el post en el Blog de Scott Guthrie donde se resumen las formas al que habría que añadir la existencia del modulo url-rewrite para iis7.

En el post de Scott Guthrie se recomienda el uso de modulos ya desarrollados, como el url-rewrite, pero mi experiencia es que estos modulos crean muchos problemas con referencias a estilos, imagenes, scripts.

Estos problemas se agravan cuando se usan webforms ASP.NET como adelanta la sección URL Rewriting for ASP.NET Web Forms en las páginas del modulo url-rewite para iis7.

Mi opinión es que el modulo url-rewrite es útil y lo recomiendocuando tienes que trabajar con código ajeno, pero si estas trabajando con un desarrollo propio y tu código está minimamente estructurado será mas eficiente implantar la reescritura de urls en tu codigo. Eficiente tanto en tiempo de ejecución como de desarrollo.

Recordemos que el problema es usar URL del tipo:
http://www.spaintiles.info/dirbeta/0-0-1-1-1-1-15-1-1-E-1/default.aspx
Cuando sabemos que a nuestras páginas les gusta mas del tipo:
http://www.spaintiles.info/dirbeta/default.aspx?P=0-0-1-1-1-1-15-1-1-E-1

El primer paso que equivale la regla de entrada (inbound rule) del Rewrite-module es transformar la URL primera en la segunda.

Para ello usamos el evento de aplicación beginRequest y el metodo rewritePath del contexto.  Dicho en simple, en global.asax editamos el procedimiento Application_BeginRequest con un código similar al que sigue.
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' Se desencadena al comienzo de cada solicitud
        Dim pathOriginal = Request.Url.ToString
        Dim rg As New System.Text.RegularExpressions.Regex("(?http\://\w*(\.\w*)*(?:\:\d*)?)(?/\w*/)(?:(?\w-\w(?:-\w*)*))/(?default\.aspx)(?.*)", RegexOptions.IgnoreCase)
        Dim captura = rg.Matches(pathOriginal)
        If captura.Count = 1 Then
            With captura(0)
                Dim pagina = .Groups("app").Value + "/default.aspx"
                If .Groups("queryString").Value.Length = 0 Then
                    pagina += "?P="
                Else
                    pagina += .Groups("queryString").Value + "&P="
                End If
                pagina += .Groups("argumentos").Value
                Context.RewritePath(pagina, False)
            End With
        End If
    End Sub

Este código podría ser mas simple como el ejemplo de
void Application_BeginRequest(object sender, EventArgs e) {

        string fullOrigionalpath = Request.Url.ToString();
        
        if (fullOrigionalpath.Contains("/Products/Books.aspx")) {
            Context.RewritePath("/Products.aspx?Category=Books");
        }
        else if (fullOrigionalpath.Contains("/Products/DVDs.aspx")) {
            Context.RewritePath("/Products.aspx?Category=DVDs");
        }
    }  
Pero a mi me gustan las expresiones regulares y tenerlo todo bien identificado.

Importante:
En mi código de ejemplo establezco a false el parametro rebaseClientpath:
  Context.RewritePath(pagina, False)
Con esto se resuelven muchos de los problemas relacionados con el uso del simbolo ~ en las urls de imagenes y otros controles web para referenciar la raiz de la aplicación.

El siguiente paso es reescribir las url para navegación entre páginas.  Outbound rule o regla de salida en un rewrite-module.
Aquí es donde puede depender de como estructuraste tu codigo. En mi  caso todas las url se generaban en un único procedimiento url(). A efectos de depuración introduje un setting de aplicación que me permite controlar el tipo de url que quiero generar.
Un primer paso es disponer es generar un patrón de url:
If tipoUrl = tiposDeUrl.caminoIzquierdaSimple Then
    dim privpagina=My.Request.uri.Absoluteuri
    Dim rg As New Regex( _
             "(?http\://\w*(\.\w*)*(?:\:\d*)?/\w*/)" + _
             "(?:(?\w-\w(?:-\w*)*)/)?" + _
             "(?default\.aspx)(?.*)?", _
                  RegexOptions.IgnoreCase)
    Dim capturas = rg.Matches(privpagina)
    If capturas.Count = 1 Then
        privpagina = capturas(0).Groups("raiz").Value + _
                     "{0}/" + _
                     capturas(0).Groups("pagina").Value
    End If
End If
El resultado es que en privPagina tendremos algo asi como:
http://www.spaintiles.info/dirbeta/{0}/default.aspx
Que nos será muy util para reemplazar con el valor del parametro:
Select Case tipoUrl
    Case tiposDeUrl.parametros
              resultado = String.Format("{0}?P={1}", pagina, camino)
    Case tiposDeUrl.caminoIzquierdaSimple
              resultado = String.Format(pagina, camino)
End Select
(recordar que por pruedencia tengo un setting para controlar el tipo de url que utilizo)

En este momento podemos probar porque igual todo funciona directamente.
Es posible que surgan problemas:
A) Al hacer click en un botón o similar.
El problema es que hacemos un post del form asp.net y el path está mal escrito.
Solución:
En el evento Load de la página añadid:
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        form1.Action = Request.RawUrl
.........

B) No se cargan estilos o scripts.
En mi caso opté por cargarlos mediante código:
Public Function RaizApp() As String
     With me.Request
         If .ApplicationPath.EndsWith("/") Then
            Return .ApplicationPath
         Else
            Return .ApplicationPath + "/"
         End If
     End With
End Function
Sub CargaJava(ByVal nombre As String)
     Dim sc As New HtmlGenericControl("script")
     sc.Attributes("type") = "text/javascript"
     sc.Attributes("src") = String.Format("{0}Scripts/{1}.js", raizapp, nombre)
     me.Header.Controls.Add(sc)
End Sub

Sub CargaCSS(ByVal nombre As String)
Dim css As New HtmlLink
    css.Href = String.Format("{0}estilos/{1}.css", RaizApp, nombre)
    css.Attributes("type") = "text/css"
    css.Attributes("rel") = "stylesheet"
    css.Attributes("media") = "all"
    me.Header.Controls.Add(css)
End Sub

Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
'Event load de la página
    form1.Action = Request.RawUrl
    If Request.QueryString.AllKeys.Contains("_TSM_HiddenField") Then Exit Sub
    ToolkitScriptManager1.Scripts.Add(New UI.ScriptReference(RaizApp + "scripts/dfc.js"))
    CargaJava("jquery-1.3.2.min")
    CargaCSS("estilocomun")
    CargaCSS("estiloceramica")
    CargaCSS("findyourCeramic")
..........
Con esto y algun que otro parche ocasional te funcionará la reescritura de URLs.

Puedes encontrar mas información sobre parchear los paths relativos en  Fixing Relative Paths in C# ASP.NET When Using Url Rewriting.
Gracias a Walter Wang que me dió la última idea en http://www.eggheadcafe.com/software/aspnet/30412853/url-rewriting-and-file-paths.aspx (otro post interesante sobre el tema.

martes, 10 de mayo de 2011

Explotar un PDF en sus paginas y extraer textos claves

Problema:
Tienes un PDF generado por tu programa de nomina (pongamos A3NOM). En un solo archivo tienes las nominas de todos los empleados. Los textos en el PDF son accesibles, puedes copiar por ejemplo el DNI del empleado y el NIF de la empresa.
Tu necesitas guardar cada página en un fichero que tenga por nombre DNI-NIF.PDF

Solución:
Descargas itext desde http://sourceforge.net/projects/itextsharp/
Luego construyes una rutina que vaya recorriendo todas las páginas.
En cada una extraes el texto con GetTextFromPage
Luego extraes el dni y el nif con las expresiones regulares correspondientes.
Finalmente extraes y guardas la página en un archivo con el nombre deseado.
Mejor verlo en código:
Imports iTextSharp.text.pdf, System.Text, System.IO, System.Text.RegularExpressions

Sub recorrePaginas(ByVal archivo As String)
Dim rgDNI = New Regex("\d{8}[A-Z]")
Dim rgNIF = New Regex("[A-Z]\d{8}")
Dim lectorPDF = New PdfReader(archivo)
Dim TotalPaginas = lectorPDF.NumberOfPages
For Pagina = 1 To TotalPaginas
Console.WriteLine(Pagina.ToString + "\" + TotalPaginas.ToString)
Dim texto = parser.PdfTextExtractor.GetTextFromPage(lectorPDF, Pagina)
texto = Encoding.UTF8.GetString (ASCIIEncoding.Convert (Encoding.Default,
Encoding.UTF8, Encoding.Default.GetBytes(texto)))
Dim DNI = rgDNI.Match(texto).Value
Dim NIF = rgNIF.Match(texto).Value
Dim nombreArchivo = String.Format("c:\pruebas\{0}-{1}.pdf", DNI, NIF)
If File.Exists(nombreArchivo) Then File.Delete(nombreArchivo)
Dim doc = New iTextSharp.text.Document(lectorPDF.GetPageSizeWithRotation(Pagina))
Dim escritor = New PdfCopy(doc, File.Create(nombreARchivo))
doc.Open()
Dim paginaPDF = escritor.GetImportedPage(lectorPDF, Pagina)
escritor.AddPage(paginaPDF)
doc.Close()
escritor.Close()
Next
End Sub

Comentario:
Hay que ver como nos complican la vida los programadores de gestiones nominas y demás.
Con lo sencillo que sería dar la opción de generar en PDF separados la nominas y exportar en texto la información mas basica asociada al PDF.
¡Pero que se puede esperar de una gente que siempre escribe lo nombres de los campos en mayúsculas

lunes, 9 de mayo de 2011

SQL server, problema con las politicas si al crear un usuario las dejas activadas

Sintomas:
Creas un usuario en sql server 2008, le pones nombre y contraseña. Pero dejas activadas las politicas por defecto.
Resultado no te deja hacer login ni cambiar las opciones CHECK_POLICY ni CHECK_EXPIRATION mientras tengas activado MUST_CHANGE

Solución:
Cambia la contraseña de las siguiente forma:
ALTER LOGIN [LoginName]
WITH PASSWORD = 'newpassword',unlock
go
ALTER LOGIN [LoginName]
WITH PASSWORD = 'newpassword',unlock,
CHECK_POLICY = OFF,
CHECK_EXPIRATION = OFF