logo

31 дек. 2012 г.

BIEE11g: картинки-миниатюры для информационных панелей

Всем привет и с наступающим Новым годом!

В последнем сообщении 2012 года хочу описать решение по созданию и использованию миниатюр для страниц инфопанелей.

В последнее время занимаюсь повышением usability текущего проекта.
Структура отчетности проекта - это набор информационных панелей. По одной для каждого отчета. Каждый отчет может содержать несколько страниц с различным представлением информации - плоская таблица, таблица среза, диаграмма и т.д.

Для удобства пользователей и снижения нагрузки каждая инфопанель содержит заглавную страницу, на которой расположены ссылки перехода на прочие страницы инфопанели их плюс описание.

И однажды я подумал, что неплохо бы "разбавить" текст ссылки перехода какой-нибудь графикой. Сначала это были просто одинаковые иконки, затем я стал подбирать иконки "по смыслу", и наконец пришел с мысли использовать сжатые скриншоты этих самых страниц (thumbnails).

Например, так теперь выглядит страница-содержание для одного из отчетов:



Итак, как достичь подобного.

Ничего особо сложного тут нет – используется любимый мною Selenium, который и создает скриншоты в момент выполнения регулярного регрессионного тестирования.
Ниже java-код для Selenium-теста, модифицированный для создания скриншотов.
package ru.servplus.tests;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.Properties;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import com.thoughtworks.selenium.DefaultSelenium;
import com.thoughtworks.selenium.SeleneseTestBase;
import com.thoughtworks.selenium.Selenium;

import java.awt.Image;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;

public class CheckAllDashboardPages extends SeleneseTestBase {

    private Selenium selenium; 
    private Properties prop;
    
    private String serverHost;
    private int serverPort;
    private String browserType;
    private String browserUrl;
    private String nqUser;
    private String nqPassword;    
    private int windowWidth;
    private int windowHeight;
    private String doScreenshots;
    private String screenshotsPath;
    private int thumbnailWidth;
    private int thumbnailHeight;

    
 @Before
 public void setUp() throws Exception {
        
  prop = new Properties();
  prop.load(new FileInputStream("selenium.properties"));
   
  serverHost = prop.getProperty("serverHost");
  serverPort = Integer.valueOf(prop.getProperty("serverPort"));
  browserType = prop.getProperty("browserType");
  browserUrl = prop.getProperty("browserUrl"); 
  nqUser = prop.getProperty("nqUser");
  nqPassword = prop.getProperty("nqPassword"); 
  windowWidth = Integer.valueOf(prop.getProperty("windowWidth"));
  windowHeight = Integer.valueOf(prop.getProperty("windowHeight"));
  doScreenshots = prop.getProperty("doScreenshots");
  screenshotsPath = prop.getProperty("screenshotsPath");
  thumbnailWidth = Integer.valueOf(prop.getProperty("thumbnailWidth"));
  thumbnailHeight = Integer.valueOf(prop.getProperty("thumbnailHeight"));
  
  
        selenium = new DefaultSelenium(
          serverHost,
          serverPort,
          browserType,
          browserUrl);
                
        selenium.start();      
 }

 @After
 public void tearDown() {
  selenium.stop();
 }
 
 public String encodePath (String s) {    
  StringBuilder str = new StringBuilder();
  int i = s.length();
  for (int j = 0; j < i; j++) {
   char c = s.charAt(j);
   switch (c) {
   case 34: // '"'
   case 35: // '#'
   case 37: // '%'
   case 42: // '*'
   case 46: // '.'
   case 58: // ':'
   case 60: // '<'
   case 62: // '>'
   case 63: // '?'
   case 124: // '|'
    str.append("_");
    break;

   case 92: // '\\'
    if (++j == i) {
     return null;
    }
    c = s.charAt(j);
    switch (c) {
    case 42: // '*'
    case 47: // '/'
    case 63: // '?'
    case 92: // '\\'
    case 126: // '~'
     str.append("_");
     break;

    default:
     return null;
    }

   default:
    str.append(c);
    break;
   }
  }
  return str.toString();
 }
 
 public String getDashboardPath (int i) {    
        StringBuilder script = new StringBuilder();
        script.append("var div1   = window.document.getElementById('idCatalogItemsAccordion');");
        script.append("var div2   = div1.getElementsByTagName('DIV')[0];");
        script.append("var div3   = div2.getElementsByTagName('DIV')[1];");
        script.append("var tab1   = div3.getElementsByTagName('TABLE')[0];");
        script.append("var tbd1   = tab1.getElementsByTagName('TBODY')[0];");
        script.append("var tr1    = tbd1.childNodes[" + i + "];");
        script.append("var td1    = tr1.getElementsByTagName('TD')[0];");
        script.append("var div4   = td1.getElementsByTagName('DIV')[0];");
        script.append("var trows  = div4.getElementsByTagName('TR');");
        script.append("var tr2    = trows[trows.length-3];");
        script.append("var td2    = tr2.getElementsByTagName('TD')[0];");
        script.append("var a1     = td2.getElementsByTagName('A')[0];");

        script.append("a1.listItem.itemInfo.path.toString();");        
        String path = selenium.getEval(script.toString());
        return path;
 } 
 

 @Test
 public void test() {
  int allErrorCount = 0;
   
  try {
   //входим в BI под админ. учетной записью (имеет смысл создать отдельную учетку-reader)
      //также крайне важно задать язык сессии, так как по англ. заголовкам элементов будет вестись поиск в тесте 
   selenium.open("/analytics/saw.dll?catalog&nqUser=" + nqUser + "&nqPassword=" + nqPassword + "&lang=en");
   
   //перейдем в режим "Расширенного поиска": будем искать все инфопанели в shared-папках
   selenium.open("/analytics/saw.dll?catalog&action=searchpanel&type=all#{\"action\":\"search\",\"location\":\"/shared\",\"mask\":\"*\",\"type\":\"dashboard\",\"favorite\":null}");
     
   //распахнем окно на заданные ширину-высоту, либо сделаем окно полноэкранным
   if (windowWidth > 0 && windowHeight > 0) {
    selenium.getEval("window.resizeTo(" + String.valueOf(windowWidth) + ", " + String.valueOf(windowHeight) + "); window.moveTo(0,0);"); 
   }
   else {
    selenium.windowMaximize();
   }
   
   int dashboardCount = 0;   
   //крайне важно дождаться появления указанных XPath-конструкцией элементов (они догружаются ajax'ом после того как загрузилась сама страница)
   selenium.waitForCondition("selenium.getXpathCount(\"//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr\") > 0", "30000");
     
   //считываем кол-во строк, возвращенных страницей поиска инфопанелей
   dashboardCount = selenium.getXpathCount("//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr").intValue();
   
   //System.out.println("Dashboard count: "+dashboardCount);
   
   //определяем массив инфопанелей (путь + название) и массив страниц каждой инфопанели
   String[] dashboardPaths = new String[dashboardCount];
   String[] dashboardPathsEncoded = new String[dashboardCount];
   String[][] dashboardPages = new String[dashboardCount][];  
   
   //заполняем значениями эти массивы (считываем путь и название каждой инфопанели с помощью XPath-конструкций)
      for(int i=0; i<dashboardCount; i++){      
       //обработаем ситуацию, когда у инфопанели задано "Описание"
       //dashboardDescrRowsCount = selenium.getXpathCount("//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr[" + (i+1) + "]/td/div/table/tbody/tr").intValue();
       //dashboardPaths[i] = selenium.getText("//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr[" + (i+1) + "]/td/div/table/tbody/tr[" + (dashboardDescrRowsCount-1) + "]/td/a") +
       // "/" + selenium.getText("//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr[" + (i+1) + "]/td/div/table/tbody/tr[1]/td[2]/span[1]");
       //dashboardPaths[i] = dashboardPaths[i].replace("/Shared Folders/", "/shared/").replace("/Dashboards/", "/_portal/");
       
       dashboardPaths[i] = getDashboardPath(i);
       dashboardPathsEncoded[i] = dashboardPaths[i].replace("\\", "\\\\");
       dashboardPathsEncoded[i] = dashboardPathsEncoded[i].replace("\"", "\\\"");
       
       //System.out.println("Dashboard "+i + " = " + dashboardPaths[i]);      
       //System.out.println("Dashboard "+i + " = " + dashboardPathsEncoded[i]);
      }
      
      //далее будем считывать значения названий страниц каждой инфопанели
      int dashboardPageCount = 0;
      for(int i=0; i<dashboardCount; i++){
       //не получилось реализовать считывание страниц внутри одного и того же окна браузера
       //возможно, из-за хитрого параметра "type"
       //сейчас реализовано таким образом, что список страниц каждой инфопанели открывается в новом окне
       
       //открываем в новом окне страницу со списком содержимого инфопанели
       selenium.openWindow("/analytics/saw.dll?catalog&action=searchpanel&type=all#{\"location\":\"" + dashboardPathsEncoded[i] + "\"}", dashboardPaths[i]);
       selenium.waitForPopUp(dashboardPaths[i], "30000");
       //перемещаем фокус в новое окно
       selenium.selectWindow(dashboardPaths[i]);
       
       //на всякий случай ставим задержку - даем странице успеть отрисоваться
       try {
     Thread.sleep(1000);
    } catch (InterruptedException e) {
     e.printStackTrace();
    }
   
       //убеждаемся, что на странице появились данные о страницах инфопанели
       selenium.waitForCondition("selenium.getXpathCount(\"//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr\") > 0", "10000");
 
       //считываем кол-во страниц текущей инфопанели
       dashboardPageCount = selenium.getXpathCount("//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr").intValue();
       
       //в цикле считываем наименования страниц инфопанели с помощью Xpath-конструкции
       String[] dashboardPageNames = new String[dashboardPageCount];
       for(int j=0; j<dashboardPageCount; j++){
        dashboardPageNames[j] = selenium.getText("//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr[" + (j+1) + "]/td/div/table/tbody/tr[1]/td[2]/span[1]");       
        //System.out.println(dashboardPageNames[j]);       
       }
       
       //закрываем вспомогательное окно           
       selenium.close();      
       
       //перемещаем фокус в главное окно браузера
       selenium.selectWindow("null");       
       
       dashboardPages[i] = dashboardPageNames;
      }
      
      int errorCount = 0;
      String errorText = null;
      String dashboardPage = null;
      String dashboardPageEncoded = null;
      
      FileInputStream fis = null;
   FileOutputStream fos = null;   
   BufferedImage sourceImage = null;
   Image thumbnail = null;
   BufferedImage bufferedThumbnail = null;
      
      //в цикле по всем инфопанелям
      for(int i=0; i<dashboardCount; i++){
       //в цикле по всем страницам текущей инфопанели
       for(int j=0; j<dashboardPages[i].length; j++){
          
        dashboardPage = dashboardPages[i][j].replace("/", "\\/");
        
        dashboardPageEncoded = dashboardPage.replace("\\", "\\\\");
        dashboardPageEncoded = dashboardPageEncoded.replace("\"", "\\\"");
        
        //открываем страницу инфопанели текущей итерации
        selenium.open("/analytics/saw.dll?Dashboard&PortalPath="+dashboardPathsEncoded[i]+"&page="+dashboardPageEncoded);
        //selenium.windowMaximize();
        
        //даем странице время отрисоваться
        try {
      Thread.sleep(1000);
     } catch (InterruptedException e) {
      e.printStackTrace();
     }
        //убеждаемся что все "часики" отработали, на всякий случай помещаем в try-блок - если за 30 сек. отчет не отработает
        try {
         selenium.waitForCondition("selenium.getXpathCount(\"//div[contains(text(),'Searching... To cancel, click')]\") == 0", "30000");
     } catch (Exception e) {
      e.printStackTrace();
     }       
        
        //считываем кол-во ошибок (Xpath-проверка по наличию элементов с текстом 'Error Details')
        errorCount = selenium.getXpathCount("//a[contains(text(),'Error Details')]").intValue();       
        allErrorCount = allErrorCount + errorCount;
        
        //Выводим информацию о проблемной странице инфопанели в System.out
        if (errorCount > 0) {
         System.out.println("DASHBOARD: " + dashboardPaths[i]);
         System.out.println("DASHBOARD PAGE: " + dashboardPages[i][j]);
         System.out.println("ERRORS COUNT: " + errorCount);
         
         for(int k=0; k<errorCount; k++){
          errorText = selenium.getText("xpath=(//a[contains(text(),'Error Details')]/parent::div/div/div[2]/div)[" + (k+1) + "]");         
          System.out.println("error["+k+"]: " + errorText);
         } 
         
         System.out.println("========================================================================================================");
        }
          
        if (doScreenshots.equalsIgnoreCase("YES")) {
         
         String srcFullPath = dashboardPaths[i].substring(1); //отбрасываем первый слеш /
         StringBuilder destFullPath = new StringBuilder(); 
         srcFullPath = srcFullPath.replace("\\/", ";;;"); //для корректной разбивки строки полного адреса на массив заменим вхождения \/ на псевдо-разделитель ;;;
         String[] srcPaths = srcFullPath.split("/"); //разбиваем строку полного адреса на массив вложенных каталогов
         
         //в цикле по каждому каталогу полного пути
         //преобразуем имена каталогов
         for (int k=0; k<srcPaths.length; k++) {
          destFullPath.append(encodePath(srcPaths[k].replace(";;;", "\\/")) + File.separator);
         }
                  
         //добавим в преобразованный полный путь название файла скриншота
         destFullPath.append(encodePath(dashboardPage));
         
         //создадим сам файл скринота
         File f = new File(screenshotsPath + destFullPath.toString());
         //для создаваемого файла может еще не существовать директории, создадим ее
         if (!f.getParentFile().exists())
          f.getParentFile().mkdirs();
         //System.out.println(f.getAbsolutePath());
         //запишем в файл поток байтов с картинкой
         selenium.captureEntirePageScreenshot(f.getAbsolutePath(),""); 
         
         //откроем поток на чтение файла со скриншотом
         fis = new FileInputStream(f.getAbsolutePath());
         //откроем поток на запись файла с миниатюрой скриншота, также зададим расширение этому файлу
         fos = new FileOutputStream(f.getAbsolutePath() + ".png");
         
         //считаем скриншот
         sourceImage = ImageIO.read(fis);
         //изменим размер скриншота на заданные в properties-файле
         //если ширина или высота указана -1, то сжатие будет происходить пропорционально исходным размерам скриншота
         thumbnail = sourceImage.getScaledInstance(thumbnailWidth, thumbnailHeight, Image.SCALE_SMOOTH);
         bufferedThumbnail = new BufferedImage(thumbnail.getWidth(null),
                                               thumbnail.getHeight(null),
                                               BufferedImage.TYPE_INT_RGB);
         bufferedThumbnail.getGraphics().drawImage(thumbnail, 0, 0, null);
         //запишем ужатую картинку в файл
         ImageIO.write(bufferedThumbnail, "png", fos);
         //закроем потоки
         fis.close(); 
         fos.close();
        }
       }
      }
  } catch (Exception e) {
   e.printStackTrace();
   throw new RuntimeException(e);
   
  } finally {
   
   try {
       //делаем logout текущей BI-сессии
       selenium.click("//span[@id='logout']/span/span");
       
       //закрываем главное окно браузера           
       selenium.close();
       
   } catch (Exception e) {
    e.printStackTrace();
   } 
   
  }  
     
     //если в ходе теста были обнаружены проблемные страница инфопанелей - тест считаем проваленным
     assertTrue("See System.out for error explanation", (allErrorCount == 0) );     
     
 }
}

На всякий случай выкладываю архив проекта в Eclipse.

Обратите внимание, что входные параметры теста вынесены в файл selenium.properties
serverHost=localhost
serverPort=4444
browserType=*firefox
browserUrl=http://localhost:7001
nqUser=weblogic
nqPassword=weblogic_1
windowWidth=1300
windowHeight=900
doScreenshots=YES
screenshotsPath=C:/Middleware/analyticsRes/
thumbnailWidth=300
thumbnailHeight=-1

Не буду повторяться относительно создания и настройки Selenium-теста - смотрите прошлое сообщение. Буду описывать нововведения.
Новые свойства в properties-файле:
windowWidth - задает ширину окна браузера в момент снятия скриншота (для моего 24 дюймого монитора это актуально);
windowHeight - задает высоту окна браузера в момент снятия скриншота;
doScreenshots - признак того, нужно ли делать скриншоты;
screenshotsPath - абсолютный путь до каталога, куда складируются скриншоты;
thumbnailWidth - ширина ужатой миниатюры;
thumbnailHeight - высота ужатой миниатюры.

Важно знать, что
- если windowWidth или windowHeight не указаны, то окно браузера будет распахнуто максимально;
- если значение thumbnailWidth (или thumbnailHeight) равно -1, то оно будет вычислять ПРОПОРЦИОНАЛЬНО исходным размерам скриншота относительно заданного thumbnailHeight (или thumbnailWidth);
- screenshotsPath должно содержать полный путь до каталога, где будут храниться скриншоты, включая последний разделитель.

Очевидно, что недостаточно просто организовать снятие, ужатие и сохранение скриншотов страниц инфопанелей куда-то на диск.
Желательно обеспечить несложное последующее использование полученных миниатюр в интерфейсе OBIEE.

Я использую созданные миниатюры следующим образом:
- для элемента инфопанели "Ссылка или изображение" указываю путь до желаемой страницы информационной панели;
- копирую строку с адресом этой страницы;
- вставляю строку адреса в поле ввода "Изображение";
- добавляю в конец строки "Изображение" текст ".png";
- добавляю в начало строки "Изображение" текст "/analyticsRes".





Ну и само собой, чтобы это работало, необходимо иметь задеплоенное в Weblogic приложение analyticsRes.
- "заготовку" для него можно взять тут - с:\Middleware\instances\instance1\bifoundation\OracleBIPresentationServicesComponent\coreapplication_obips1\analyticsRes ;
- скопируйте папку-заготовку куда-нибудь ПОБЛИЖЕ к корню диска (это важно, так как следует минимизировать суммарный путь до файлов миниатюр, например, у меня это - c:\Middleware\analyticsRes);
- в момент деплоя КРАЙНЕ важно указать, что приложение analyticsRes будет доступно в том же каталоге, где оно расположено, то есть в c:\Middleware\analyticsRes (иначе оно будет скопировано в STAGE-область WLS, нам это не нужно - мы настраиваем Selenium-тест на выгрузку миниатюр в исходную папку).


P.S. Есть одна проблема, который не удалось победить. Если в полный адрес до вашей страницы инфопанели (адрес, который вы видите в CatalogManager или в каталожном браузере OBIEE) содержит спец.символы, которые НЕЛЬЗЯ использовать в названиях каталогов и файлов операционной системы (например, " # * . < > ? : | ), то вам необходимо самим произвести замену этих символов на знак подчеркивания "_" в момент заполнения поля ввода "Изображение".

Комментариев нет:

Отправить комментарий