sábado, 23 de junio de 2012

Descargar una archivo ZIP de la web y procesarlo en memoria


Enunciado

Usando Visual Basic .NET, descargar un archivo comprimido desde un sitio web, descomprimirlo e importar el contenido, importando sus líneas en una base de datos. Todo ello sin guardar en disco una copia, esto es trabajando en memoria.

Solución

Se utiliza la librería dotnetzip   y la clase webclient de .net Framework

Imports System.Web, System.IO, System.Net, Ionic.Zip, Microsoft.AnalysisServices

Module Module1
    Sub ejemplo()
        Dim cliente As New WebClient
        Dim datos As Byte()
        datos = cliente.DownloadData(My.Settings.CaminoZIP)
        Dim cola = New MemoryStream(datos)
        Dim comprimido = ZipFile.Read(cola)
        Dim Entrada = comprimido.Entries(0)
        Dim colaMemoria As New MemoryStream
        Entrada.Extract(colaMemoria)
        colaMemoria.Position = 0
        Dim lector As New StreamReader(colaMemoria)
        lector.ReadLine()
        Do While Not lector.EndOfStream
            Dim lineaDatos = LeeLineaGas(lector.ReadLine, Today)
            If lineaDatos IsNot Nothing Then 
                                    bd.ImportacionGas.InsertOnSubmit(lineaDatos)
        Loop
    End Sub
end module

Aclaración

Si a alguien le intriga por qué etiqueto con SSAS,... porque los archivos descargados alimentan maravillosamente bases de datos SQL Server Analysis Services. Por ejemplo gracias a este código controlo la evolución precios y orígenes de las importaciones de gas españolas. Otra cosa es encontrar las fuentes que tiene su miga.

domingo, 17 de junio de 2012

Una escala de colores "redonda" para los mapas en Reporting Services

Enunciado

Las formas automáticas para definir la escala de colores en los mapas de reporting services tienen un defecto: los máximos y mínimos de cada rango pueden ser algo así como:

  • De 567 a 1002
  • De 1002 a 3141    
  • De 3141 a 9999
  • De 9999 a 20010
En este post copio un código que genera una escala:

  • De 500 a 1000
  • De 1000 a 3000    
  • De 3000 a 10000    
  • De 10000 a 30000
Cada color contiene "mas o menos" la misma cantidad de elementos.

Solución

Hay que hacer varias cosas:
  1. Evaluar la sentencia del informe (en mi código una sentencia MDX, pero no es necesario podría ser SQL)
  2. Para que haya la misma cantidad de países en cada color usamos percentiles. En el código que muestro hay diez colores, por tanto uso deciles (uso un código similar al de mi post Cuartiles en SSAS, pero es aplicable con otras formas de obtener los datos, i.e. SQL)
  3. A medida que los obtenemos redondeamos el dígito mas alto esto es de 9.999.999 "redondeamos a 10.000.000 y lo guardamos.
  4. Cuando terminamos, comprobamos si hay dos valores iguales y lo resolvemos(lo puede haber provocado el redondeo). Por ejemplo 10000 a 20000,  20000 a 20000, 20000 a 30000 pasaría a ser 10000 a 20000 20000 a 25000 25000 a 30000.
  5. Modificamos la definición del informe (ver mi post sobre cómo hacerlo)
Bien, este es el código:
Public Function RangosMapa(s As String) As List(Of Double)
        Dim r As New List(Of Double)
        Dim isov As String()
        If Ambito = "M" Then
            isov = IsoEsriMundo 'lista de ISO2 paises del mundo
        Else
            isov = IsoEsriEuropa 'Lista de ISO2 paises de Europa
        End If
        Dim lista As New List(Of Double)

        'Calculamos la sentencia en el informe
        Dim cn As New AdomdConnection(My.Settings.SSASConexion)
        cn.Open()
        Dim cmd As AdomdCommand = cn.CreateCommand()
        cmd.CommandText = evaluaSentencia(s)

        Dim lector = cmd.ExecuteReader()
        Do While lector.Read
            If isov.Contains(lector.GetString(0)) Then lista.Add(lector.GetDecimal(4))
        Loop
        lector.Close()
        cn.Close()
        cn.Dispose()

        'recorremos lo deciles
        lista.Sort()
        r.Add(Piso(lista(0)))
        Dim paso = lista.Count / 10
        For i = paso To lista.Count - 2 Step paso
            Dim valor = lista(Math.Round(i) - 1)
            Dim redondo = Redondeo(valor)
            If redondo > r(r.Count - 1) Then
                r.Add(Redondeo(lista(Math.Round(i) - 1)))
            Else
                r.Add(Techo(valor))
            End If
        Next
        r.Add(Techo(lista(lista.Count - 1)))
        'Si hay valores duplicados partimos por la mitad.
        Dim cambio As Boolean
        Do
            cambio = False
            For i = 1 To r.Count - 1
                If r(i) = r(i - 1) Then
                    r(i) = (r(i - 1) + r(i + 1)) / 2
                    cambio = True
                End If
            Next
        Loop Until Not cambio
        Return r
    End Function
'Funciones de redondeo
    Public Function CalculoFactor10(n As Double) As Integer
        Return Math.Pow(10, Math.Round(Math.Log10(n)))
    End Function
    Public Function Piso(n As Double) As Double
        If RedondeoSuperior Then
            Dim factor10 = CalculoFactor10(n)
            Return Math.Floor(n / factor10) * factor10
        Else
            Return Math.Floor(n)
        End If

    End Function

    Public Function Techo(n As Double) As Double
        If RedondeoSuperior Then
            Dim factor10 = CalculoFactor10(n)
            Return Math.Ceiling(n / factor10) * factor10
        Else
            Return Math.Ceiling(n)
        End If
    End Function

    Public Function Redondeo(n As Double) As Double
        If RedondeoSuperior Then
            Dim factor10 = CalculoFactor10(n)
            Return Math.Round(n / factor10) * factor10
        Else
            Return Math.Round(n)
        End If
    End Function
'parametrización del mapa
 Public Function ParametrizaMapa(informe As XDocument, rangosMapa As List(Of Double)) As XDocument
        Dim df = informe.Root.Name.Namespace
        Dim mapa = informe.Root.Element(
            df + "ReportSections").Element(
            df + "ReportSection").Element(
            df + "Body").Element(
            df + "ReportItems").Element(
            df + "Map")
        Dim capa = mapa.Element(df + "MapLayers").Element(df + "MapPolygonLayer")
        capa.Element(df + "MapPolygons").ReplaceNodes(Poligonos.Nodes)
        Dim reglasColor = capa.Element(df + "MapPolygonRules").Element(df + "MapColorRangeRule")
        Dim culturaUSA = New System.Globalization.CultureInfo("en-US")
        reglasColor.Element(df + "DistributionType").Value = "Custom"
        Dim i = 0
        For Each elemento In reglasColor.Element(df + "MapBuckets").Elements
            elemento.Element(df + "StartValue").Value = rangosMapa(i).ToString(culturaUSA.NumberFormat)
            elemento.Element(df + "EndValue").Value = rangosMapa(i + 1).ToString(culturaUSA.NumberFormat)
                i += 1
        Next
        Return informe
    End Function
Si alguien necesita mas detalles, estoy a su disposición

Autentificación Reporting Services en VB.NET

Enunciado

Construir una clase para autentificarse ante un servidor de informes de Microsoft (SSRS) puede ser útil para:
  1. Utilizar el visor de informes con Informes de servidor (processingMode=Remote)
  2. Utilizar servicios web para Cargar y modificar la definición de informes

Solución

Crearemos la siguiente clase:>

Imports System.Net
Imports System.Security.Principal
Imports Microsoft.Reporting.WebForms
 _
Public NotInheritable Class MyReportServerCredentials
    Implements IReportServerCredentials

    Public ReadOnly Property ImpersonationUser() As WindowsIdentity _
            Implements IReportServerCredentials.ImpersonationUser
        Get

            'Use the default windows user.  Credentials will be
            'provided by the NetworkCredentials property.
            Return Nothing

        End Get
    End Property

    Public ReadOnly Property NetworkCredentials() As ICredentials _
            Implements IReportServerCredentials.NetworkCredentials
        Get

            'Read the user information from the web.config file.  
            'By reading the information on demand instead of storing 
            'it, the credentials will not be stored in session, 
            'reducing the vulnerable surface area to the web.config 
            'file, which can be secured with an ACL.

            'User name
            Dim userName As String = My.Settings.usuario

            If (String.IsNullOrEmpty(userName)) Then
                Throw New Exception("Missing user name from web.config file")
            End If

            'Password
            Dim password As String = My.Settings.password

            If (String.IsNullOrEmpty(password)) Then
                Throw New Exception("Missing password from web.config file")
            End If

            'Domain
            Dim domain As String = My.Settings.dominio

            If (String.IsNullOrEmpty(domain)) Then
                Throw New Exception("Missing domain from web.config file")
            End If

            Return New NetworkCredential(userName, password, domain)

        End Get
    End Property

    Public Function GetFormsCredentials(ByRef authCookie As Cookie, _
                                        ByRef userName As String, _
                                        ByRef password As String, _
                                        ByRef authority As String) _
                                        As Boolean _
            Implements IReportServerCredentials.GetFormsCredentials

        authCookie = Nothing
        userName = Nothing
        password = Nothing
        authority = Nothing

        'Not using form credentials
        Return False

    End Function

End Class

Para usarla en el visor de informes:

Protected Sub Page_Init(ByVal sender As Object, _
                              ByVal e As System.EventArgs) _
                              Handles Me.Init
        Visor.ServerReport.ReportServerUrl = New Uri(My.Settings.SSRS)
        Visor.ServerReport.ReportServerCredentials = _
            New MyReportServerCredentials()
    End Sub

Para usarla en los web Services

        Dim SSRS As New ReportingService2010
        SSRS.Credentials = (New MyReportServerCredentials).NetworkCredentials
        Dim informe = SSRS.GetItemDefinition(My.Settings.mapas + nombre)
        SSRS = Nothing

Cargar y modificar un informe de Report Server

Enunciado

En ocasiones puede resultar interesante leer un informe definido en el servidor de  Microsoft Reporting Services, modificar la definición y volver subirlo al servidor para visualizar en un webform Report viewer.
Redactaré mas adelante algunos ejemplos de aplicación, por el momento.

Solución

El problema se divide en tres partes:

  1. Leer la definición del informe.
  2. Modificar el XML de esta definición mediante Linq to XML
  3. Establecer la definición modificada en el report Viewer.

Leer la definición del informe desde el servidor


Hay que acceder a los servicios web de Reporting Services, para ello se crea una un clase proxy mediante WSDL.EXE y una clase con las credenciales necesarias para autentificarse.
Una vez hecho esto es relativamente fácil.
 Private Function CargaInforme(nombre As String) As XDocument
        Dim SSRS As New ReportingService2010
        SSRS.Credentials = (New MyReportServerCredentials).NetworkCredentials
        Dim informe = SSRS.GetItemDefinition(My.Settings.mapas + nombre)
        SSRS = Nothing
        Dim stream = New System.IO.MemoryStream(informe)
        Dim lector = System.Xml.XmlReader.Create(stream)
        Dim doc As XDocument = XDocument.Load(lector, LoadOptions.None)
        Return doc
    End Function

En breve, el servicio veb me devuelve un array de bytes, que traslado a un memory stream que a su vez me permite crear un xdocument.

Modificar la definición de informe

Esto es lo mas fácil, aunque a veces engorroso. Por ejemplo vamos a cambiar la definición del Dataset "datos" en el informe.
Dim df = doc.Root.Name.Namespace
            Dim xdatos = (From f In doc.Root.Element(df + "DataSets").Elements(df + "DataSet")).First(
            Function(x As XElement) x.Attribute("Name") = "Datos").Element(df + "Query").Element(df + "CommandText")
            xdatos.SetValue(s)
S es un string con la sentencia SQL, MDX o lo que quiera que utilices, por supuesto tiene que se coherente con el resto del informe en lo que atañe a nombres de campo, parametros,...etc

Visualizar el informe modificado en Report Viewer

Para hacerlo necesitaremos convertir nuestro Xdocument que contiene la definición en un stream:
 Private Function XMLBytes(doc As XDocument) As System.IO.Stream
        Dim stream As New System.IO.MemoryStream
        Dim escritor = System.Xml.XmlWriter.Create(stream)
        doc.Save(escritor)
        escritor.Close()
        stream.Position = 0
        Return stream
    End Function
Lo que sigue es asignar este stream al visor de informes, establecer los parámetros que hagan falta y refrescar
Visor.ServerReport.LoadReportDefinition(e.Informe)
Visor.ServerReport.SetParameters(New ReportParameter("miParametro", suValor))
Visor.ServerReport.Refresh()

viernes, 15 de junio de 2012

Determinar el path a un archivo en global.asax

Enunciado:
Durante el arranque de la aplicación en global.asax quieres leer una archivo de configuración controles.xml almacenado en el raíz.
¿cuál es el path?
Solución

'Separo en dos líneas para facilitar la lectura
Dim nombreArchivo=Server.MapPath("Global.asax").Replace("Global.asax", "controles.xml")
Dim cfg = XElement.Load(nombreArchivo)

jueves, 14 de junio de 2012

Publicar código de programación en blogger usando plantilla de vistas dinámicas

Problema: 
Los informáticos solemos publicar código HTML, XML, Java, SQL, Visual Basic en nuestros blogs.
La forma habitual es utilizar SyntaxHighlighter que es el estandar de facto.
Para incorporarlo se edita el HTML de la plantilla de blogger.
El problema surge cuando queremos usar las plantillas de vistas dinámicas porque en estas no se puede editar el HTML.
Solución:
No voy a entrar en detalles solo daré la idea base y dos enlaces:
Idea base:
  1. Incluir los estilos CSS en personalización de la plantilla/Avanzado/CSS
  2. Incluir en cada post la carga de scripts de autocarga, por ejemplo:
Yo utilizo el script de autocarga de Kobe Wuyts porque carga todos los lenguajes, hay otras alternativas con menos lenguajes, por ejemplo la de los creadores de la idea original crux-framework.tools

Borrar cookies desde ASP.NET (por ejemplo de autenticación por formularios)

Problema:
Sigo las instrucciones en http://msdn.microsoft.com/es-es/library/ms178195(v=vs.80).aspx pero la cookie no desaparece.
Solución:
Establece también el path y el dominio
El código completo sería:
                    Dim myCookie As HttpCookie
                    myCookie = New HttpCookie("miGalleta")
                    myCookie.Path = "/"
                    myCookie.Domain = ".midominio.es"
                    myCookie.Expires = DateTime.Now.AddDays(-1D)
                    Response.Cookies.Add(myCookie)

Esto es particularmente interesante para omitir las cookies de la autenticación por formulario, como las del pobre Forefront TMG.