En vacaciones, hacer un pequeño juego

Parabollic2Este, creo, es el primer artículo, de este blog, en el que no voy a hablar de Alfresco. Resulta que estas vacaciones no sabía que hacer y he decidido rememorar los tiempos en los que me hacía mis juegos con el MSX, si, un poco de programación rápida, sin planear nada de ante mano y en unas 5 tardes. De hecho, comencé por mirar los gráficos AWT de Java ya que no tenía grandes pretensiones y casi en el momento empecé a escribir un programa que, en principio, iba a ser un algoritmo de parábola para una bolita, pero luego encontré por Internet un par de gráficos de navecitas así que, finalmente, he hecho un juego de naves.

El juego es muy sencillo, derecha, izquierda, disparo y listo, los enemigos pueden usar 3 tipos de estrategia, una que sigue a la nave amiga, otro que de desplaza a la derecha y otra a la izquierda.

Solo he usado estos tres gráficos que pongo a continuación, que podéis descargar para usarlos:

laser32.png
laser32

nave_azul32.png
nave_azul32

nave_verde32.png
nave_verde32

 

Sobre los sonidos, he usado los siguientes, igualmente podéis usarlos:

explosion2.wav


game_over.wav

 

laser_shot2.wav

 

ready_go.wav

 

Y nada más, a continuación os pongo el código fuente de cada fichero. No esperéis nada especial ni un código modelo dentro de la programación orientada a objetos, lo he ido escribiendo sobre la marcha para hacerlo en eso, cinco tardes de verano.

Fichero: Enemy.java

package com.fegor.games.parabolic;

import java.awt.Graphics;
import java.util.ArrayList;

/**
 * @author 	fegor
 * 
 */
public class Enemy implements Trace {
  private final int STRATEGY_RIGHT = 0;
  private final int STRATEGY_LEFT = 1;
  private final int STRATEGY_PERSECUTION = 2;

  private final int MAX_ENEMY = 7;

  private Graphics gr = null;

  private int enemies;
  private ArrayList<Sprite> enemy;
  private ArrayList<Sprite> laserEnemy;
  private ArrayList<Boolean> shootLaser;
  private ArrayList<Sound> soundLaser;
  private ArrayList<Sound> soundExplotion;
  private ArrayList<Integer> strategy;
  private ArrayList<Boolean> isAlive;

  private int screen_width = 480;
  private int screen_height = 640;

  /**
   * 
   */
  public Enemy() {
    enemy = new ArrayList<Sprite>();
    laserEnemy = new ArrayList<Sprite>();
    shootLaser = new ArrayList<Boolean>();
    soundLaser = new ArrayList<Sound>();
    soundExplotion = new ArrayList<Sound>();
    strategy = new ArrayList<Integer>();
    isAlive = new ArrayList<Boolean>();

    initEnemies();
  }

  /**
   * 
   */
  public void draw() {
    for (int i = 0; i < MAX_ENEMY; i++) {
      if (isAlive.get(i)) {
        enemy.get(i).setGr(gr);
        enemy.get(i).draw();
      }

      if (shootLaser.get(i)) {
        laserEnemy.get(i).setGr(gr);
        laserEnemy.get(i).draw();
      }
    }
  }

  /**
   * @param targetX
   * @param targetDistance
   */
  public void refreshPosition(int targetX, int targetDistance) {
    for (int i = 0; i < MAX_ENEMY; i++) {

      // move from strategy
      if (isAlive.get(i) && strategy.get(i) == STRATEGY_RIGHT) {
        if (enemy.get(i).getX() > screen_width)
          enemy.get(i).setX(0);

        enemy.get(i).moveRight();
      }

      else if (isAlive.get(i) && strategy.get(i) == STRATEGY_LEFT) {
        if (enemy.get(i).getX() == 0)
          enemy.get(i).setX(screen_width);

        enemy.get(i).moveLeft();
      }

      else if (isAlive.get(i) && strategy.get(i) >= STRATEGY_PERSECUTION) {
        if (enemy.get(i).getX() < (targetX - targetDistance))
          enemy.get(i).moveRight();

        else if (enemy.get(i).getX() > (targetX + targetDistance))
          enemy.get(i).moveLeft();
      }

      // down
      if (isAlive.get(i) && enemy.get(i).getY() > screen_height - (enemy.get(i).getHeight() * 2)) {
        enemyDestroy(i);
      }

      else if (isAlive.get(i))
        if ((i == 0) || ((i > 0 && (enemy.get(i - 1).getY() > (32 * 2)))) || (!isAlive.get(i - 1))) {
          enemy.get(i).moveDown();
        }
    }

    // all enemies dead?
    if (enemies <= 0)
      enemiesReset();
  }

  /**
   * 
   */
  public void shoot() {
    for (int i = 0; i < MAX_ENEMY; i++) {
      if ((!shootLaser.get(i)) && isAlive.get(i))
        if (((int) (Math.random() * 99)) == 0) {
          shootLaser.set(i, true);
          soundLaser.get(i).play();
        }

      if (shootLaser.get(i)) {
        if (laserEnemy.get(i).getX() == 0) {
          laserEnemy.get(i).setX(enemy.get(i).getX() + (enemy.get(i).getWidth() / 2));
          laserEnemy.get(i).setY(enemy.get(i).getY() + (enemy.get(i).getHeight()));
        }

        if (shootLaser.get(i) && (laserEnemy.get(i).getY() < screen_height - laserEnemy.get(i).getHeight()))
          laserEnemy.get(i).moveDown();

        else {
          laserEnemy.get(i).setX(0);
          shootLaser.set(i, false);
        }
      }
    }
  }

  /**
   * @param s
   * @return
   */
  public boolean collition(Sprite s) {
    boolean res = false;
    for (int i = 0; i < MAX_ENEMY; i++) {
      if (isAlive.get(i) && enemy.get(i).hasCollition(s)) {
        soundExplotion.get(i).play();
        res = true;
        enemyDestroy(i);
        if (isTrace) 
          System.out.println("Collition enemy " + i + " to " + s.getName());
      }
    }

    return res;
  }

  /**
   * @param s
   * @return
   */
  public boolean laserCollition(Sprite s) {
    boolean res = false;
    for (int i = 0; i < MAX_ENEMY; i++) {
      if (shootLaser.get(i) && laserEnemy.get(i).hasCollition(s)) {
        res = true;
        laserDestroy(i);
        if (isTrace)
          System.out.println("Collition laser " + i + " to " + s.getName());
      }
    }

    return res;
  }

  /**
   * 
   */
  private void initEnemies() {

    // Create enemies, laser and soundss
    for (int i = 0; i < MAX_ENEMY; i++) {
      enemy.add(new Sprite());
      isAlive.add(true);
      laserEnemy.add(new Sprite());
      laserEnemy.get(i).setMoveStepY(10);
      shootLaser.add(false);
      soundLaser.add(new Sound("laser_shot2.wav"));
      soundExplotion.add(new Sound("explosion2.wav"));
      strategy.add(null);

      enemy.get(i).addImage("nave_verde32.png");
      laserEnemy.get(i).addImage("laser32.png");
    }

    enemiesReset();
  }

  /**
   * @param e
   * @param i
   */
  private void reset(Sprite e, int i) {
    e.setX((screen_width / MAX_ENEMY) * i);
    e.setY(32);
    e.setMoveStepX(1);
  }

  /**
   * 
   */
  private void enemiesReset() {
    if (isTrace)
      System.out.println("Enemies reset");
    
    for (int i = 0; i < MAX_ENEMY; i++) {
      reset(enemy.get(i), i);
      strategy.set(i, (int) (Math.random() * 3));
      isAlive.set(i, true);
    }

    enemies = MAX_ENEMY;
  }

  /**
   * @param i
   */
  private void enemyDestroy(int i) {
    isAlive.set(i, false);
    enemies--;
  }

  /**
   * @param i
   */
  private void laserDestroy(int i) {
    shootLaser.set(i, false);
  }

  /**
   * @param gr
   */
  public void setGr(Graphics gr) {
    this.gr = gr;
  }

  /**
   * @param screen_width
   */
  public void setScreen_width(int screen_width) {
    this.screen_width = screen_width;
  }

  /**
   * @param screen_height
   */
  public void setScreen_height(int screen_height) {
    this.screen_height = screen_height;
  }

  /**
   * @return
   */
  public ArrayList<Boolean> getIsAlive() {
    return isAlive;
  }

  /**
   * @param isAlive
   */
  public void setIsAlive(ArrayList<Boolean> isAlive) {
    this.isAlive = isAlive;
  }

  /**
   * @return
   */
  public int getEnemies() {
    return enemies;
  }

  /**
   * @param enemies
   */
  public void setEnemies(int enemies) {
    this.enemies = enemies;
  }

  /**
   * @return
   */
  public ArrayList<Boolean> getShootLaser() {
    return shootLaser;
  }

  /**
   * @return
   */
  public ArrayList<Sound> getSoundLaser() {
    return soundLaser;
  }

}

 

Fichero: Game.java

package com.fegor.games.parabolic;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.ArrayList;

import javax.swing.JPanel;

/**
 * @author fegor
 *
 */
public class Game extends JPanel implements Runnable, Trace {

  private static final long serialVersionUID = -3239737141818043435L;
  private static final int STARSHIPS_COLLITION = 1;
  private static final int STARSHIPBLUE_LOSS = 2;
  private static final int STARSHIPGREEN_LOSS = 3;
  private static final int INIT_DELAY = 30;
  private static final int SCREEN_WIDTH = 480;
  private static final int SCREEN_HEIGHT = 640;
  private static final int MAX_STARS = 100;
  private static final int NUM_STARSHIPS = 5;

  private int delay = INIT_DELAY;

  private Color colorTextBlink;
  private int score = 0;
  private int numStarShips = NUM_STARSHIPS;
  private boolean shootStarShipBlue = false;
  private boolean isGameOver = false;
  private boolean isWaitStart = true;
  private int dx;

  private Enemy enemy;
  private Sprite starShipBlue;
  private Sprite laserStarShipBlue;

  private Sound soundLaserBlue;
  private Sound soundExplosionBlue;
  private Sound soundReadyGo;
  private Sound soundGameOver;

  private ArrayList<Star> stars = new ArrayList<Star>();
  private int moveStars = 0;
  private int distance = 10;

  private Thread thread;

  /**
   * 
   */
  public Game() {
    setBackground(Color.BLACK);
    setDoubleBuffered(true);

    enemy = new Enemy();
    enemy.setScreen_width(SCREEN_WIDTH);
    enemy.setScreen_height(SCREEN_HEIGHT);

    starShipBlue = new Sprite();
    starShipBlue.addImage("nave_azul32.png");
    resetStarShipBlue();

    laserStarShipBlue = new Sprite();
    laserStarShipBlue.addImage("laser32.png");
    laserStarShipBlue.setMoveStepY(10);

    soundReadyGo = new Sound("ready_go.wav");
    soundGameOver = new Sound("game_over.wav");
    soundLaserBlue = new Sound("laser_shot2.wav");
    soundExplosionBlue = new Sound("explosion2.wav");

    // Generate stars
    int nColor;
    for (int i = 0; i < MAX_STARS; i++) {
      nColor = (int) (Math.random() * 256 - 1);
      Color color = new Color(nColor, nColor, nColor);
      stars.add(new Star((int) (Math.random() * SCREEN_WIDTH - 1),
          (int) (Math.random() * (SCREEN_HEIGHT - 1 + 32)), color));
    }
  }

  /* (non-Javadoc)
   * @see java.lang.Runnable#run()
   */
  @Override
  public void run() {
    while (true) {
      if (isWaitStart) {
        colorTextBlink = (colorTextBlink == Color.GREEN ? Color.BLACK : Color.GREEN);
        repaint();
        try {
          Thread.sleep(300);
        }

        catch (InterruptedException ie) {
          System.out.println(ie);
        }
      }

      else if (isGameOver) {
        colorTextBlink = (colorTextBlink == Color.GREEN ? Color.BLACK : Color.GREEN);
        repaint();
        try {
          Thread.sleep(300);
        }

        catch (InterruptedException ie) {
          System.out.println(ie);
        }
      }

      else {
        loop();
        repaint();
        try {
          Thread.sleep(delay);
        }

        catch (InterruptedException ie) {
          System.out.println(ie);
        }
      }
    }
  }

  /**
   * 
   */
  private void resetGame() {
    delay = INIT_DELAY;
    score = 0;
    numStarShips = NUM_STARSHIPS;
    resetStarShipBlue();
    enemy = null;
    enemy = new Enemy();
  }

  /**
   * 
   */
  private void resetStarShipBlue() {
    starShipBlue.setX((SCREEN_WIDTH + starShipBlue.getWidth()) / 2);
    starShipBlue.setY(SCREEN_HEIGHT - (starShipBlue.getHeight() * 2));
    starShipBlue.setMoveStepX(5);
  }

  /**
   * 
   */
  private void resetLaserStarShipBlue() {
    laserStarShipBlue.setX(-32);
    laserStarShipBlue.setY(-32);
    shootStarShipBlue = false;
  }

  /* (non-Javadoc)
   * @see javax.swing.JComponent#addNotify()
   */
  @Override
  public void addNotify() {
    super.addNotify();
    thread = new Thread(this);
    thread.start();
  }

  /* (non-Javadoc)
   * @see javax.swing.JComponent#paint(java.awt.Graphics)
   */
  @Override
  public void paint(Graphics g) {
    super.paint(g);
    Graphics2D g2d = (Graphics2D) g;

    if (isGameOver) {
      g.setColor(Color.GREEN);
      g.setFont(new Font("Monospaced", Font.BOLD, 40));
      g.drawString("GAME OVER", (SCREEN_WIDTH / 2) - (3 * 40), SCREEN_HEIGHT / 2);
      g.setFont(new Font("Monospaced", Font.BOLD, 26));
      g.drawString("SCORE: " + score, (SCREEN_WIDTH / 2) - (5 * 26), (SCREEN_HEIGHT / 2) + 120);
      g.setColor(colorTextBlink);
      g.drawString("PRESS S TO BEGIN", (SCREEN_WIDTH / 2) - (5 * 26), (SCREEN_HEIGHT / 2) + 60);
    }

    else if (isWaitStart) {
      g.setColor(Color.GREEN);
      g.setFont(new Font("Monospaced", Font.BOLD, 30));
      g.drawString("PARABOLIC STARSHIPS", (SCREEN_WIDTH / 2) - (6 * 30), (SCREEN_HEIGHT / 2));
      g.setColor(colorTextBlink);
      g.setFont(new Font("Monospaced", Font.BOLD, 20));
      g.drawString("PRESS S TO BEGIN", (SCREEN_WIDTH / 2) - (5 * 20), (SCREEN_HEIGHT / 2) + 60);
    }

    else {
      g.setColor(Color.GREEN);
      g.setFont(new Font("Monospaced", Font.BOLD, 20));
      g.drawString("LIVES: " + numStarShips, 10, 16);
      g.drawString("LEVEL: " + ((INIT_DELAY + 1) - delay), (SCREEN_WIDTH / 2) - (3 * 20), 16);
      g.drawString("SCORE: " + score, SCREEN_WIDTH - (7 * 20), 16);

      for (int i = 0; i < MAX_STARS; i++) {
        g.setColor(stars.get(i).getColor());
        stars.get(i).setGr(g2d);
        stars.get(i).draw();
      }

      if (enemy.getEnemies() == 0 && delay > 5)
        delay--;

      enemy.setGr(g2d);
      enemy.draw();

      starShipBlue.setGr(g2d);
      starShipBlue.draw();

      if (shootStarShipBlue) {
        laserStarShipBlue.setGr(g2d);
        laserStarShipBlue.draw();
      }
    }

    Toolkit.getDefaultToolkit().sync();
    g.dispose();
  }

  /**
   * 
   */
  public void loop() {
    int i;

    moveShipBlue();

    // stars move
    if (moveStars == 5) {
      for (i = 1; i < MAX_STARS; i++) {
        if (stars.get(i).getY() < SCREEN_HEIGHT)
          stars.get(i).setY(stars.get(i).getY() + 1);

        else {
          stars.get(i).setX((int) (Math.random() * (SCREEN_WIDTH - 1)));
          stars.get(i).setY(32);
        }

      }
      moveStars = 0;
    }

    else
      moveStars++;

    // enemy move
    enemy.refreshPosition(starShipBlue.getX(), distance);

    // Laser of starship blue
    if (shootStarShipBlue) {
      if (laserStarShipBlue.getX() == -32) {
        laserStarShipBlue.setX(starShipBlue.getX() + (starShipBlue.getWidth() / 2));
        laserStarShipBlue.setY(starShipBlue.getY() - (starShipBlue.getHeight()));
      }

      if (laserStarShipBlue.getY() > 32)
        laserStarShipBlue.moveUp();

      else
        resetLaserStarShipBlue();
    }

    // Laser of enemies
    enemy.shoot();

    // Collitions
    if (enemy.collition(starShipBlue)) {
      managerCollition(STARSHIPS_COLLITION);
    }

    else if (enemy.collition(laserStarShipBlue)) {
      managerCollition(STARSHIPGREEN_LOSS);
    }

    else if (enemy.laserCollition(starShipBlue)) {
      managerCollition(STARSHIPBLUE_LOSS);
    }
  }

  /**
   * @param status
   */
  private void managerCollition(int status) {
    if (status == STARSHIPS_COLLITION || status == STARSHIPBLUE_LOSS) {
      if (numStarShips > 0) {
        if (status == STARSHIPS_COLLITION) {
          soundExplosionBlue.play();
          resetStarShipBlue();
        }

        else {
          soundExplosionBlue.play();
          resetStarShipBlue();
        }

        numStarShips--;
      }

      else {
        soundGameOver.play();
        isGameOver = true;
        if (isTrace)
          System.out.println("Game Over");
      }
    }

    else if (status == STARSHIPGREEN_LOSS) {
      score++;
      resetLaserStarShipBlue();
    }
  }

  /**
   * @return
   */
  public int getScreenWidth() {
    return SCREEN_WIDTH;
  }

  /**
   * @return
   */
  public int getScreenHeight() {
    return SCREEN_HEIGHT;
  }

  /**
   * 
   */
  private void moveShipBlue() {
    int x = starShipBlue.getX();
    
    if (x > 1 && dx == -1)
      starShipBlue.moveLeft();

    else if (x < (SCREEN_WIDTH - starShipBlue.getWidth()) && dx == 1)
      starShipBlue.moveRight();
  }

  /**
   * @return
   */
  public KeyListener keyboardControl() {
    KeyListener keyListener = new KeyListener() {

      @Override
      public void keyPressed(KeyEvent e) {
        int c = e.getKeyCode();

        switch (c) {
        case KeyEvent.VK_LEFT:
          dx = -1;

          break;

        case KeyEvent.VK_RIGHT:
          dx = 1;
          break;

        case KeyEvent.VK_S:
          if (isWaitStart) {
            soundReadyGo.play();
            isWaitStart = false;
            if (isTrace)
              System.out.println("Start");
          }

          else if (isGameOver) {
            soundReadyGo.play();
            resetGame();
            isGameOver = false;
            if (isTrace)
              System.out.println("Start");
          }
          break;

        case KeyEvent.VK_SPACE:
          if (!shootStarShipBlue) {
            shootStarShipBlue = true;
            soundLaserBlue.play();
          }
          break;

        default:
          break;
        }

        repaint();
      }

      /**
       * @param e
       */
      @Override
      public void keyReleased(KeyEvent e) {
        int c = e.getKeyCode();

        switch (c) {
        case KeyEvent.VK_LEFT:
          dx = 0;
          break;

        case KeyEvent.VK_RIGHT:
          dx = 0;
          break;

        default:
          break;
        }
      }

      /**
       * @param e
       */
      @Override
      public void keyTyped(KeyEvent e) {
        // TODO Auto-generated method stub
      }
    };

    return keyListener;
  }
}

 

Fichero: Main.java

package com.fegor.games.parabolic;

import javax.swing.JFrame;

/**
 * @author fegor
 *
 */
public class Main extends JFrame {

  private static final long serialVersionUID = 1L;

  /**
   * 
   */
  public Main() {
    Game game = new Game();
    setTitle("Naves");
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    setSize(game.getScreenWidth(), game.getScreenHeight());
    setLocationRelativeTo(null);
    setResizable(false);
    add(game);
    addKeyListener(game.keyboardControl());
    setFocusable(true);
    setVisible(true);
  }
  
  /**
   * @param args
   */
  public static void main(String args[]) {
    new Main();
  }
}

 

Fichero: Sound.java

package com.fegor.games.parabolic;

import java.io.BufferedInputStream;
import java.io.IOException;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;

/**
 * @author fegor
 *
 */
public class Sound implements Runnable {

  Thread thread;

  String soundFile;
  BufferedInputStream bs;
  AudioInputStream ais;
  Clip clip;

  /**
   * @param soundFile
   */
  public Sound(String soundFile) {
    this.soundFile = soundFile;
  }

  /**
   * 
   */
  public void play() {
    thread = new Thread(this);
    thread.start();
  }

  /* (non-Javadoc)
   * @see java.lang.Runnable#run()
   */
  /* (non-Javadoc)
   * @see java.lang.Runnable#run()
   */
  @Override
  public void run() {
    try {
      if (bs != null) {
        return;
      }
      
      bs = new BufferedInputStream(
          Sound.class.getResourceAsStream("/com/fegor/games/parabolic/resources/" + soundFile));
      ais = AudioSystem.getAudioInputStream(bs);
      
      final int BUFFER_SIZE = 128000;
      SourceDataLine sourceLine = null;

      AudioFormat audioFormat = ais.getFormat();
      DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);

      sourceLine = (SourceDataLine) AudioSystem.getLine(info);
      sourceLine.open(audioFormat);
      
      sourceLine.start();
      int nBytesRead = 0;
      byte[] abData = new byte[BUFFER_SIZE];
      while (nBytesRead != -1) {
        try {
          nBytesRead = bs.read(abData, 0, abData.length);
        } catch (IOException e) {
          e.printStackTrace();
        }
        if (nBytesRead >= 0) {
          sourceLine.write(abData, 0, nBytesRead);
        }
      }

      sourceLine.drain();
      sourceLine.close();
      bs.close();
      bs.close();
      bs = null;
    } 
    
    catch (IOException e) {
      e.printStackTrace();
    } 
    
    catch (LineUnavailableException e) {
      e.printStackTrace();
    System.exit(1);
    } 
    
    catch (Exception e) {
      e.printStackTrace();
      System.exit(1);
    }
  }
}

 

Fichero: Sprite.java

package com.fegor.games.parabolic;

import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.net.URL;
import java.util.ArrayList;
import javax.imageio.ImageIO;

/**
 * @author fegor
 *
 */
public class Sprite implements ImageObserver {
  private int x;
  private int y;
  private int width;
  private int height;
  private int nImage;
  private int moveStepX;
  private int moveStepY;
  private String name;
  
  private ArrayList<BufferedImage> alImage = new ArrayList<BufferedImage>();
  private Graphics gr = null;
  
  /**
   * 
   */
  public Sprite() {
    x = 0;
    y = 0;
    width = 16;
    height = 16;
    nImage = 0;
    moveStepX = 1;
    moveStepY = 1;	
    name = null;
  }
  
  /**
   * 
   */
  public void moveRight() {
    x += moveStepX;
  }
  
  /**
   * 
   */
  public void moveLeft() {
    x -= moveStepX;
  }
  
  /**
   * 
   */
  public void moveUp() {
    y -= moveStepY;
  }
  
  /**
   * 
   */
  public void moveDown() {
    y += moveStepY;
  }
  
  /**
   * 
   */
  public void draw() {
    if (gr == null) 
      System.out.println("Not graphics found for visualize sprite");
    
    else {
      if (nImage < alImage.size()) 
        nImage = 0;
      
      else
        nImage++;
      
      gr.drawImage(alImage.get(nImage), x, y, (ImageObserver) this);
    }
  }
  
  /**
   * @param img
   */
  public void addImage(String img) {
    if (name == null) 
      name = img;
    alImage.add(loadImage(img));
  }
  
  /**
   * @param image
   * @return
   */
  private BufferedImage loadImage(String image) {
    BufferedImage bi = null;
    try {
      URL url = Sprite.class.getResource("/com/fegor/games/parabolic/resources/" + image);
      bi = ImageIO.read(url);
      this.width = bi.getWidth();
      this.height = bi.getHeight();
      return bi;
    }

    catch (Exception e) {
      System.out.println("Image not found");
      return null;
    }
  }

  /**
   * @return
   */
  public int getX() {
    return x;
  }

  /**
   * @param x
   */
  public void setX(int x) {
    this.x = x;
  }

  /**
   * @return
   */
  public int getY() {
    return y;
  }

  /**
   * @param y
   */
  public void setY(int y) {
    this.y = y;
  }

  /**
   * @return
   */
  public int getWidth() {
    return width;
  }

  /**
   * @param width
   */
  public void setWidth(int width) {
    this.width = width;
  }

  /**
   * @return
   */
  public int getHeight() {
    return height;
  }

  /**
   * @param height
   */
  public void setHeight(int height) {
    this.height = height;
  }

  /**
   * @param gr
   */
  public void setGr(Graphics gr) {
    this.gr = gr;
  }

  /**
   * @param moveStepX
   */
  public void setMoveStepX(int moveStepX) {
    this.moveStepX = moveStepX;
  }

  /**
   * @param moveStepY
   */
  public void setMoveStepY(int moveStepY) {
    this.moveStepY = moveStepY;
  }

  /* (non-Javadoc)
   * @see java.awt.image.ImageObserver#imageUpdate(java.awt.Image, int, int, int, int, int)
   */
  @Override
  public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
    System.out.println("imageUpdate");
    return false;
  }
  
  /**
   * @return
   */
  public Rectangle getBounds() {
    return new Rectangle(x, y, width, height);
  }
  
  /**
   * @param s
   * @return
   */
  public boolean hasCollition(Sprite s) {
    return s.getBounds().intersects(getBounds());
  }

  /**
   * @return
   */
  public String getName() {
    return name;
  }

  /**
   * @param name
   */
  public void setName(String name) {
    this.name = name;
  }
}

 

Fichero: Star.java

package com.fegor.games.parabolic;

import java.awt.Color;
import java.awt.Graphics;

/**
 * @author fegor
 *
 */
public class Star {
  int x, y;
  Color color;
  private Graphics gr = null;

  /**
   * @param x
   * @param y
   * @param color
   */
  public Star(int x, int y, Color color) {
    this.x = x;
    this.y = y;
    this.color = color;
  }
  
  /**
   * 
   */
  public void draw() {
    if (gr == null) 
      System.out.println("Not graphics found for visualize sprite");
    
    else 
      gr.drawLine(x, y, x, y);
  }
  
  /**
   * @return
   */
  public int getX() {
    return x;
  }

  /**
   * @param x
   */
  public void setX(int x) {
    this.x = x;
  }

  /**
   * @return
   */
  public int getY() {
    return y;
  }

  /**
   * @param y
   */
  public void setY(int y) {
    this.y = y;
  }

  /**
   * @return
   */
  public Color getColor() {
    return color;
  }

  /**
   * @param color
   */
  public void setColor(Color color) {
    this.color = color;
  }
  
  /**
   * @param gr
   */
  public void setGr(Graphics gr) {
    this.gr = gr;
  }
}

 

Fichero: Trace.java

package com.fegor.games.parabolic;

/**
 * @author fegor
 *
 */
public interface Trace {
  public static final Boolean TRON = true;
  public static final Boolean TROFF = false;
  
  public static Boolean isTrace = TRON;
}

 

¡FELIZ VERANO!

 

 

Alfresco y Scala

scala-logo-croped¿Que es Scala?

Scala es un lenguaje multi-paradigma que integra características de lenguajes funcionales y orientados a objetos, al contrario que Java que es un lenguaje imperativo (excepto en la versión 8 que Oracle ha incluido algunas características de lenguaje funcional como las funciones lambda).

Por tanto, es una buena idea poder utilizar Scala y Java en Alfresco ya que, de esta forma, podemos tener los dos paradigmas –funcional e imperativo– a nuestro servicio.

 

Versión de Alfresco

La prueba se ha realizado sobre Alfresco 5.0.c y se ha utilizado maven con el arquetipo de creación de módulos AMP, versión 2.2.0 del SDK.

 

Instalación de componentes en Eclipse

Los siguientes componentes son recomendables para poder utilizar Scala en Eclipse IDE:

– Instalar desde Marketplace: Scala IDE 4.2.x (recomendable)
– Instalar desde “Install new software” (opcional):

Nombre: Maven for Scala
URL: http://alchim31.free.fr/m2e-scala/update-site/

Crear proyecto de módulo AMP en Alfresco (SDK 2.2.0)

mvn archetype:generate -Dfilter=org.alfresco:

[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] >>> maven-archetype-plugin:2.2:generate (default-cli) > generate-sources
@ standalone-pom >>>
[INFO]
[INFO] <<< maven-archetype-plugin:2.2:generate (default-cli) < generate-sources
@ standalone-pom <<<
[INFO]
[INFO] --- maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom --
-
[INFO] Generating project in Interactive mode
[INFO] No archetype defined. Using maven-archetype-quickstart (org.apache.maven.
archetypes:maven-archetype-quickstart:1.0)
Choose archetype:
1: remote -> org.alfresco.maven.archetype:alfresco-allinone-archetype (Sample mu
lti-module project for All-in-One development on the Alfresco plaftorm. Includes
modules for: Repository WAR overlay, Repository AMP, Share WAR overlay, Solr co
nfiguration, and embedded Tomcat runner)
2: remote -> org.alfresco.maven.archetype:alfresco-amp-archetype (Sample project
with full support for lifecycle and rapid development of Repository AMPs (Alfre
sco Module Packages))
3: remote -> org.alfresco.maven.archetype:share-amp-archetype (Share project wit
h full support for lifecycle and rapid development of AMPs (Alfresco Module
Packages))
Choose a number or apply filter (format: [groupId:]artifactId, case sensitive co
ntains): : 2
Choose org.alfresco.maven.archetype:alfresco-amp-archetype version:
1: 2.0.0-beta-1
2: 2.0.0-beta-2
3: 2.0.0-beta-3
4: 2.0.0-beta-4
5: 2.0.0
6: 2.1.0
7: 2.1.1
8: 2.2.0
Choose a number: 8: 8
Downloading: https://repo.maven.apache.org/maven2/org/alfresco/maven/archetype/a
lfresco-amp-archetype/2.2.0/alfresco-amp-archetype-2.2.0.jar
Downloaded: https://repo.maven.apache.org/maven2/org/alfresco/maven/archetype/al
fresco-amp-archetype/2.2.0/alfresco-amp-archetype-2.2.0.jar (26 KB at 16.2 KB/se
c)
Downloading: https://repo.maven.apache.org/maven2/org/alfresco/maven/archetype/a
lfresco-amp-archetype/2.2.0/alfresco-amp-archetype-2.2.0.pom
Downloaded: https://repo.maven.apache.org/maven2/org/alfresco/maven/archetype/al
fresco-amp-archetype/2.2.0/alfresco-amp-archetype-2.2.0.pom (3 KB at 16.3 KB/sec
)
Define value for property 'groupId': : com.fegor.alfresco.scala
Define value for property 'artifactId': : AlfScala
[INFO] Using property: version = 1.0-SNAPSHOT
Define value for property 'package': com.fegor.alfresco.scala: :
Confirm properties configuration:
groupId: com.fegor.alfresco.scala
artifactId: AlfScala
version: 1.0-SNAPSHOT
package: com.fegor.alfresco.scala
Y: :

 

Crear carpetas para trabajar con Scala

Crear los directorios para trabajar con Scala dentro del proyecto:

src/main/scala
src/test/scala

 

Modificar pom.xml para integrar compilación con Scala

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.fegorsoft.alfresco.scala</groupId>
    <artifactId>AlfScala</artifactId>
    <version>1.0-SNAPSHOT</version>  
    <name>AlfScala Repository AMP project</name>
    <packaging>amp</packaging>
    <description>Manages the lifecycle of the AlfScala Repository AMP (Alfresco Module Package)</description>

    <parent>
        <groupId>org.alfresco.maven</groupId>
        <artifactId>alfresco-sdk-parent</artifactId>
        <version>2.2.0</version>
    </parent>

  <repositories>
      <repository>
          <id>artima</id>
          <name>Artima Maven Repository</name>
          <url>http://repo.artima.com/releases</url>
      </repository>
  </repositories>

    <!-- 
        SDK properties have sensible defaults in the SDK parent,
        but you can override the properties below to use another version.
        For more available properties see the alfresco-sdk-parent POM.
       -->
    <properties>
        <!-- The following are default values for data location and Alfresco Community version.
             Uncomment if you need to change (Note. current default version for Enterprise edition is 5.1)
          <alfresco.version>5.1.e</alfresco.version>
          <alfresco.data.location>/absolute/path/to/alf_data_dev</alfresco.data.location> -->

        <!-- This control the root logging level for all apps uncomment and change, defaults to WARN
            <app.log.root.level>WARN</app.log.root.level>
        -->

        <!-- Set the enviroment to use, this controls which properties will be picked in src/test/properties
             for embedded run, defaults to the 'local' environment. See SDK Parent POM for more info.
        <env>local</env>
        -->
        <recompileMode>incremental</recompileMode>
    <fsc>false</fsc>
    </properties>

    <!-- Here we realize the connection with the Alfresco selected platform 
        (e.g.version and edition) -->
    <dependencyManagement>
        <dependencies>
            <!-- Setup what versions of the different Alfresco artifacts that will be used (depends on alfresco.version),
                 so we don't have to specify version in any of the dependency definitions in our POM.
                 For more info see:
                    http://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#Importing_Dependencies
            -->
            <dependency>
                <groupId>${alfresco.groupId}</groupId>
                <artifactId>alfresco-platform-distribution</artifactId>
                <version>${alfresco.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- Following dependencies are needed for compiling Java code in src/main/java;
             <scope>provided</scope> is inherited for each of the following;
             for more info, please refer to alfresco-platform-distribution POM -->
        <dependency>
            <groupId>${alfresco.groupId}</groupId>
            <artifactId>alfresco-repository</artifactId>
        </dependency>

        <!-- If we are running tests then make the H2 Scripts available
             Note. tests are skipped when you are running -Pamp-to-war -->
        <dependency>
            <groupId>${alfresco.groupId}</groupId>
            <artifactId>alfresco-repository</artifactId>
            <version>${alfresco.version}</version>
            <classifier>h2scripts</classifier>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>*</groupId>
                    <artifactId>*</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
  	    <dependency>
          <groupId>org.scala-lang</groupId>
          <artifactId>scala-compiler</artifactId>
          <version>2.11.8</version>
          <scope>compile</scope>
      </dependency>
      <dependency>
          <groupId>org.scala-lang</groupId>
          <artifactId>scala-library</artifactId>
          <version>2.11.8</version>
      </dependency>
      <dependency>
          <groupId>com.typesafe.scala-logging</groupId>
          <artifactId>scala-logging-slf4j_2.11</artifactId>
          <version>2.1.2</version>
      </dependency>
    <dependency>
      <groupId>org.scala-lang.modules</groupId>
      <artifactId>scala-xml_2.11</artifactId>
      <version>1.0.3</version>
    </dependency>
    <dependency>
      <groupId>org.scalactic</groupId>
      <artifactId>scalactic_2.11</artifactId>
      <version>3.0.0-M15</version>
    </dependency>
    <dependency>
      <groupId>org.scalatest</groupId>
      <artifactId>scalatest_2.11</artifactId>
      <version>3.0.0-M15</version>
      <scope>test</scope>
    </dependency>
    <!-- 
      <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
      </dependency>
       -->
    </dependencies>

    <profiles>
        <!--
            Brings in the extra Enterprise specific repository classes,
            if the 'enterprise' profile has been activated, needs to be activated manually.
            -->
        <profile>
            <id>enterprise</id>
            <dependencies>
                <dependency>
                    <groupId>${alfresco.groupId}</groupId>
                    <artifactId>alfresco-enterprise-repository</artifactId>
                    <version>${alfresco.version}</version>
                    <scope>provided</scope>
                </dependency>
            </dependencies>
        </profile>
  </profiles>
    
  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>net.alchim31.maven</groupId>
          <artifactId>scala-maven-plugin</artifactId>
          <version>3.2.1</version>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>2.0.2</version>
        </plugin>
      </plugins>
    </pluginManagement>
    <plugins>
            <plugin>
                <inherited>true</inherited>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.scala-tools</groupId>
                <artifactId>maven-scala-plugin</artifactId>
                <version>2.15.2</version>
                <executions>
                    <!-- Run scala compiler in the process-resources phase, so that dependencies on
                         scala classes can be resolved later in the (Java) compile phase -->
                    <execution>
                        <id>scala-compile-first</id>
                        <phase>process-resources</phase>                        
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
 
                    <!-- Run scala compiler in the process-test-resources phase, so that dependencies on
                         scala classes can be resolved later in the (Java) test-compile phase -->                    
                    <execution>
                      <id>scala-test-compile</id>
                      <phase>process-test-resources</phase>
                      <goals>
                          <goal>testCompile</goal>
                      </goals>
                	</execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>build-helper-maven-plugin</artifactId>
                <executions>
                    <!-- Add src/main/scala to source path of Eclipse -->
                    <execution>
                        <id>add-source</id>
                        <phase>generate-sources</phase>
                        <goals>
                            <goal>add-source</goal>
                        </goals>
                        <configuration>
                            <sources>
                                <source>src/main/scala</source>
                            </sources>
                        </configuration>
                    </execution>
                    <!-- Add src/test/scala to test source path of Eclipse -->
                    <execution>
                        <id>add-test-source</id>
                        <phase>generate-test-sources</phase>
                        <goals>
                            <goal>add-test-source</goal>
                        </goals>
                        <configuration>
                            <sources>
                                <source>src/test/scala</source>
                            </sources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>          
            <!-- to generate Eclipse artifacts for projects mixing Scala and Java -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-eclipse-plugin</artifactId>
                <version>2.8</version>
                <configuration>
                    <downloadSources>true</downloadSources>
                    <downloadJavadocs>true</downloadJavadocs>
                    <projectnatures>
                        <projectnature>org.scala-ide.sdt.core.scalanature</projectnature>
                        <projectnature>org.eclipse.jdt.core.javanature</projectnature>
                    </projectnatures>
                    <buildcommands>
                        <buildcommand>org.scala-ide.sdt.core.scalabuilder</buildcommand>
                    </buildcommands>
                    <classpathContainers>
                        <classpathContainer>org.scala-ide.sdt.launching.SCALA_CONTAINER</classpathContainer>
                        <classpathContainer>org.eclipse.jdt.launching.JRE_CONTAINER</classpathContainer>
                    </classpathContainers>
                    <excludes>
                        <!-- in Eclipse, use scala-library, scala-compiler from the SCALA_CONTAINER rather than POM <dependency> -->
                        <exclude>org.scala-lang:scala-library</exclude>
                        <exclude>org.scala-lang:scala-compiler</exclude>
                    </excludes>
                    <sourceIncludes>
                        <sourceInclude>**/*.scala</sourceInclude>
                        <sourceInclude>**/*.java</sourceInclude>
                    </sourceIncludes>
                </configuration>
            </plugin>
            <!-- When run tests in the test phase, include .java and .scala source files -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <includes>
                        <include>**/*.java</include>
                        <include>**/*.scala</include>
                    </includes>
                </configuration>
            </plugin>
    </plugins>
  </build>
</project>

 

Crear una prueba unitaria

En este caso se crea una prueba unitaria para recuperar el contenido que hay dentro de ‘Company Home’.

package com.fegor.alfresco.scala.test

/*
 *  @author Fernando González (fegor at fegor dot com)
 *  
 */
import org.scalatest.junit.AssertionsForJUnit
import org.slf4j.LoggerFactory
import com.typesafe.scalalogging.slf4j.Logger
import org.junit.Assert._
import org.junit.{ Test, Before }
import org.junit.runner.RunWith
import com.tradeshift.test.remote.{ Remote, RemoteTestRunner }
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner
import org.springframework.context.ApplicationContext
import org.springframework.beans.factory.annotation.{ Autowired, Qualifier }
import org.alfresco.util.ApplicationContextHelper
import org.alfresco.model.ContentModel
import org.alfresco.service.cmr.repository.{ StoreRef, NodeService }
import org.alfresco.service.cmr.search.{ SearchService, ResultSet }
import org.alfresco.repo.security.authentication.AuthenticationUtil

/** Test Alfresco in Scala
 *  
 * @constructor create tests for Alfresco with Scala
 */
@RunWith(classOf[RemoteTestRunner])
@Remote(runnerClass = classOf[SpringJUnit4ClassRunner])
@ContextConfiguration(Array("classpath:alfresco/application-context.xml"))
class RepositoryTest extends AssertionsForJUnit {
  val logger = Logger(LoggerFactory.getLogger(classOf[RepositoryTest]))
  val storeRef: StoreRef = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore")
  val ctx: ApplicationContext = ApplicationContextHelper.getApplicationContext
  val nodeService: NodeService = ctx.getBean("NodeService").asInstanceOf[NodeService]

  var searchService: SearchService = ctx.getBean("SearchService").asInstanceOf[SearchService]

  /* Initialize
   */
  @Before
  def initialize() {
    AuthenticationUtil.setFullyAuthenticatedUser("admin")
  }

  /* Alfresco repository test
   */
  @Test
  def testRepositoryList() {
    val rs: ResultSet = searchService.query(storeRef, SearchService.LANGUAGE_LUCENE, "PATH:\"/app:company_home/*\"")
    assert(rs.length() > 0)
    logger.info((for (i <- 0 until rs.length()) yield nodeService.getProperty(rs.getNodeRef(i), ContentModel.PROP_NAME)) mkString ",")
    rs.close()
  }
}

 

Activar depuración de clases en log4j.properties

log4j.logger.com.fegor.alfresco.scala.test.RepositoryTest=DEBUG

 

Resultado de la ejecución de la prueba

2016-03-30 21:23:09,884  INFO  [extensions.webscripts.ScriptProcessorRegistry] [main] Registered script processor javascript for extension js
 2016-03-30 21:23:10,165  INFO  [scala.test.RepositoryTest] [main] Guest Home,User Homes,Shared,Imap Attachments,IMAP Home,Sites,Data Dictionary
 Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 41.923 sec - in com.fegor.alfresco.scala.test.RepositoryTest

 

Webs de referencia

http://scala-ide.org/docs/tutorials/m2eclipse/index.html

http://www.scalatest.org/getting_started_with_junit_4_in_scala

http://ted-gao.blogspot.com.es/2011/09/mixing-scala-and-java-in-project.html

https://en.wikipedia.org/wiki/Scala_(programming_language)

https://es.wikipedia.org/wiki/Scala_(lenguaje_de_programaci%C3%B3n)

http://www.scala-lang.org/

 

LockService en JScript (Java Backend)

folder-blue-locked-iconDesde Java se puede acceder al servicio de bloqueo de nodos de Alfresco, pero desde JScript (WebScripts) no se puede hacer bloquear, si desbloquear (document.unlock()) pero en muchas ocasiones nos interesa bloquear el documento con el que estamos trabajando.

Podemos usar varias soluciones: Sobrecargar o añadir métodos al objeto ScriptNode, exponer el servicio o crear una acción y llamarla desde JScript. En este caso vamos a implementar la exposición de un objeto locker en JScript a través de la forma Java-Backend.

Para esto hay que realizar dos ficheros, la definición del bean para que Rhino pueda tenerlo disponible y el código en Java que defina los servicios para JScript.

En primer lugar, el fichero de definición de bean (javascript-context.xml) puede ser algo así:

<?xml version='1.0' encoding='UTF-8'?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
 http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd">
 <bean id="scriptLocker" class="es.omc.ae.javascript.ScriptLocker"
 parent="baseJavaScriptExtension">
 <property name="extensionName">
 <value>locker</value>
 </property>
 <property name="lockService">
 <ref bean="LockService" />
 </property>
 </bean>
</beans>

Y el código en Java (ScriptLocker.java):


package es.omc.ae.javascript;

import org.alfresco.repo.jscript.ScriptNode;
import org.alfresco.repo.processor.BaseProcessorExtension;
import org.alfresco.service.cmr.lock.LockService;
import org.alfresco.service.cmr.lock.LockStatus;
import org.alfresco.service.cmr.lock.LockType;

/**
 * Locker and Unlocker (Backend JScript)
 * 
 * @author Fernando González (fegor [at] fegor [dot] com)
 * @version 1.0
 *
 */
public final class ScriptLocker extends BaseProcessorExtension {

 private LockService lockService;

 /**
 * @param nodeRef
 */
 public void unlock(ScriptNode scriptNode) {
 LockStatus lockStatus = lockService.getLockStatus(scriptNode.getNodeRef());
 if (LockStatus.LOCKED.equals(lockStatus)
 || LockStatus.LOCK_OWNER.equals(lockStatus)) {
 this.lockService.unlock(scriptNode.getNodeRef());
 }
 }

 /**
 * @param nodeRef
 */
 public void nodeLock(ScriptNode scriptNode) {
 this.lockService.lock(scriptNode.getNodeRef(), LockType.NODE_LOCK);
 }

 /**
 * @param scriptNode
 * @param timeToExpire (in seconds)
 */
 public void nodeLock(ScriptNode scriptNode, int timeToExpire) {
 this.lockService.lock(scriptNode.getNodeRef(), LockType.NODE_LOCK, timeToExpire);
 }
 
 /**
 * @param nodeRef
 */
 public void readOnlyLock(ScriptNode scriptNode) {
 this.lockService.lock(scriptNode.getNodeRef(), LockType.READ_ONLY_LOCK);
 }

 /**
 * @param scriptNode
 * @param timeToExpire (in seconds)
 */
 public void readOnlyLock(ScriptNode scriptNode, int timeToExpire) {
 this.lockService.lock(scriptNode.getNodeRef(), LockType.READ_ONLY_LOCK, timeToExpire);
 }
 
 /**
 * @param nodeRef
 */
 public void writeLock(ScriptNode scriptNode) {
 this.lockService.lock(scriptNode.getNodeRef(), LockType.WRITE_LOCK);
 }

 /**
 * @param scriptNode
 * @param timeToExpire (in seconds)
 */
 public void writeLock(ScriptNode scriptNode, int timeToExpire) {
 this.lockService.lock(scriptNode.getNodeRef(), LockType.WRITE_LOCK, timeToExpire);
 }
 
 /**
 * @param nodeRef
 * @return
 */
 public String getLockStatus(ScriptNode scriptNode) {
 return this.lockService.getLockStatus(scriptNode.getNodeRef()).name();
 }

 /**
 * @param nodeRef
 * @return
 */
 public String getLockType(ScriptNode scriptNode) {
 return this.lockService.getLockType(scriptNode.getNodeRef()).name();
 }

 /**
 * @param lockService
 */
 public void setLockService(LockService lockService) {
 this.lockService = lockService;
 }
}

Ya solo queda usarlo, por ejemplo en JScript tenemos el objeto document, podemos hacer:


locker.nodeLock(document);

Para bloquearlo, o:


locker.unlock(document);

Para desbloquearlo posteriormente. También disponemos de los mismos métodos pero con un parámetro más para bloquear un número determinado de segundos.

En la versión 5 de Alfresco existen más métodos de este servicio pero he implementado los mínimos para que pueda funcionar desde versiones 3.4 y 4.x.

 

Enlaces de interés:

http://dev.alfresco.com/resource/docs/java/org/alfresco/service/cmr/lock/LockService.html

 

Solucionar el error “Found 1 integrity violations” en Alfresco 5.0.2

Cuando se instala Alfresco 5.0.2 y 5.1.EA, si esta instalación recoge los parámetros para español, se produce un error como el siguiente:

 

Caused by: org.alfresco.repo.node.integrity.IntegrityException: 00270001 Found 1 integrity violations:

Invalid property value:

Node: workspace://SpacesStore/141296a2-92d5-4160-82ec-828421bd8a4a

Name: Rep. Dem.

Type: {http://www.alfresco.org/model/content/1.0}category

Property: {http://www.alfresco.org/model/content/1.0}name

 

Este error ya se resuelve en la incidencia https://issues.alfresco.com/jira/browse/ALF-21423 por Angel Borroy y lo que voy a explicar es, simplemente, como arreglarlo en el propio fichero WAR, de forma que, si instalamos de nuevo, no se produzca de nuevo el error.

 

Primero descomprimimos el fichero alfresco.war:

cd tomcat/webapps

mkdir fix

cp alfresco.war fix

cd fix

unzip alfresco.war

 

Descomprimimos el fichero alfresco-repository-5.0.x.x.jar (cambia las x por el número que tenga):

cd WEB-INF/lib

mkdir fix

cd fix

jar -xvf ../alfresco-repository-5.0.2.5.jar

 

Cambiamos el mensaje que falla en bootstrap-messages_es.properties:

vi alfresco/messages/bootstrap-messages_es.properties

[Buscar dentro de vi, p.e. /Rep\.\ Dem\.]

[Cambiar “Rep. Dem.” por “Rep\u00fablica”]

Nota: Se podría hacer de una vez sustituyendo en vi, pero eso os lo dejo a vosotros 😉

[Guardar con ESC+:wq]

 

Volvemos a crear el JAR (cuidado con los números de revisión), sobreescribimos el original y borramos este directorio:

jar cvf alfresco-repository-5.0.2.5.jar *

cp alfresco-repository-5.0.2.5.jar ../

cd ..

rm -rf fix

 

Volvemos a crear el WAR, creamos una copia del original, otra del nuevo, sustituimos el original y borramos el directorio de trabajo:

cd ../../

zip -r alfresco.war *

cd ..

cp alfresco.war alfresco.war-orig

cp fix/alfresco.war .

cp alfresco.war alfresco.war-fix

mv alfresco.war-orig ../../

mv alfresco.war-fix ../../

rm -rf fix

cd ../../

 

Reiniciamos el servicio de Alfresco y, si todo ha ido bien, arrancará sin problemas.

 

Nota: No olvidéis borrar previamente el despliegue (webapps/alfresco) así como el contenido del directorio work.

 

Eso es todo, solo me queda dar las gracias a Angel Borroy por su aportación en la resolución de este problema.

 

Más información:

https://forums.alfresco.com/es/forum/usu%C3%A1rio-alfresco/instalaci%C3%B3n/value-rep-dem-not-valid-file-name-10202015-1558

https://issues.alfresco.com/jira/browse/ALF-21423

 

Errores de red con Windows 10 y Alfresco

Con la actualización de Windows 10 pueden volver algunos errores que en otras versiones y sistemas operativos ya estaban solucionadas.

Estos errores vienen derivados principalmente de las nuevas configuraciones de red implementadas en Windows, a partir de la versión 7 en realidad, y que en su versión 10 han sufrido un gran cambio.

Por ejemplo, una vez instalado Alfresco puede presentarse el siguiente error:

— log —
ERROR [sf.ehcache.Cache] [main] Unable to set localhost. This prevents creation of a GUID. Cause was: Mordor: Mordor
 java.net.UnknownHostException: Mordor: Mordor
at java.net.InetAddress.getLocalHost(Unknown Source)
at net.sf.ehcache.Cache.(Cache.java:155)
[…]
— log —

Si se realiza una investigación sobre este error se encuentran muchas páginas donde la solución es introducir la dirección IP 127.0.0.1 y el nombre del host o “hostname” en el fichero hosts, tanto en Linux, Windows y Mac OSX, pero esta solución ya no funciona en Windows, debido principalmente a dos cuestiones:
1. El uso de DNSSEC 
2. El uso de IPv6 sobre IPv4
Por lo tanto, para solucionarlo, no basta con poner nada en el fichero %SystemRoot%system32Driversetchosts, de hecho, no hace falta.
La solución es más sencilla que esto, hay que usar IPv4. Se puede hacer (tocando el registro de Windows) que la versión 4 tenga prevalencia sobre la versión 6 del TCP/IP, pero también, y creo que esto es mejor, se puede arrancar la máquina virtual de Java para que use la versión de TCP/IP que se necesita. El parámetro es: 
-Djava.net.preferIPv4Stack=true
Con este parámetro se solucionan los posibles errores de detección del host que es necesario para el arranque de Alfresco.
Además, hay que incluirlo también en otros sitios donde se utilicen descargas o llamadas a direcciones de Internet, como el caso de maven, eclipse, etc.
Por ejemplo, mi variable MAVEN_OPTS es la siguiente:
-Xms256m -Xmx1024m -XX:PermSize=320m -Xss1024k -Djava.net.preferIPv4Stack=true
Y que es la misma que tengo en la configuración de eclipse (Servers) para cada servidor de Tomcat y para cada llamada de maven.
Evidentemente hay que usar este parámetro también en el fichero eclipse.ini si se necesitan instalar plugins.

Nueva revisión de Alfviral (Alfresco Virus Alert)

Hace unos días, recibí un correo electrónico donde se me avisaba de un error de Alfviral cuando se intentaba actualizar un documento desde Share en la versión 4.2.

Al parecer, en esta versión se usa un nuevo sistema de actualización asíncrona y cuando se actualiza un documento se produce un borrado de nodo para crear otro que es el actualizado. El sistema de eventos salta con cada paso por lo que hay que verificar antes de nada que el nodo sigue todavía “vivo” y no ha sido borrado por el propio sistema de actualización. Esto, curiosamente no pasa en el contexto del Explorer (Alfresco).

En resumen, he re-factorizado algo más el código, he arreglado el problema y he reorganizado el proyecto tipo all-in-one como dos submódulos de repositorio y otro para share, así creo que está más claro y es más sencillo de instalar.

La nueva revisión se puede descargar desde: https://github.com/fegorama/alfviral/releases/tag/v1.3.2-SNAPSHOT

Alfresco y aplicaciones de los DataList

— Entorno —

Sistema: GNU/Linux Mint 17.1 Rebbeca (x86_64)
Versión Alfresco: 4.2.2 Enterprise
Base de datos: MySQL 5
Cliente Web: Google Chrome 40.0.2214.115 (64-bit)
Indexador: Apache-Lucene

Las pruebas se han realizado con Apache-Lucene así pues en Solr podrían ser distintas.

— Introducción —

Los DataList en Alfresco son un sistema de mantener “tablas” de datos para ser usadas de forma básica. Estas “tablas” tienen en el repositorio una estructura donde una carpeta representa el DataList y como “hijos” contiene cada uno de los “items” u opciones que vamos añadiendo. A mi parecer no es un sistema muy eficiente en lo que respecta a la forma de gestionarlo posteriormente ya que seguramente usando como persistencia tablas de la base de datos que se use en la instancia de Alfresco seguramente serían más rápidas y estables, pensemos en alguna DataList que necesitemos con 5000 registros (items). Dicho esto, es al menos una nueva forma con la que podemos contar para gestionar datos tabulados en Alfresco.

— Aplicaciones —

Las aplicaciones van en principio por la creación de DataList que hay ya preconfigurados como la lista de contactos, eventos, agenda, etc. pero también podemos crearnos nuestra propia DataList que permita ayudarnos en la posterior gestión o en tareas como carga dinámica de nuevos controles. La creación de un nuevo tipo de DataList es muy sencillo, basta con crear un tipo que herede de “dl:dataListItem” y añadir las propiedades que necesitamos guardar.

Hasta aquí bien, de hecho es muy fácil crearnos tipos nuevos muy sencillos con uno o dos propiedades y que nos sirvan para guardar información de “datos maestros” como provincias, municipios, temperaturas, cantidades, etc.

Aquí es donde viene una posible aplicación, cuando necesitamos recoger datos de fuentes externas en muchas ocasiones estas fuentes van a ser sustituidas por Alfresco o bien solo se necesitan en determinadas ocasiones por lo que tenerlas como fuente de datos desde Alfresco es un posible problema por si se eliminan, paran o cambian. Aquí se puede recoger esa información y crear DataList con ella de forma que luego podamos usarla simplemente haciendo una serie de consultas y recorridos por la propia DataList.

— Problema —

Bien, ¿sencillo verdad?, en realidad si, pero hay algunos problemas que solventar, el primero es que al crear nuestro tipo de datos y al heredar de “dl:dataListItem”, cuando creamos un DataList de nuestro propio tipo, el nombre junto con el prefijo quedan en una propiedad llamada “dl:dataListItemType” de la siguiente forma:

Efectivamente su valor es “dh:listasClara”, pero ¿que pasa entonces?, ¿todo bien, verdad?, bueno, no del todo, ya que resulta que cuando queremos buscar por este término para poder acotar solamente la búsqueda a este tipo de DataList nos encontramos con un problema, no se encuentra…

¿Por que?

Esto es debido al tipo de indexación que tiene por defecto esta propiedad, podemos verlo mejor si usamos la herramienta LukeAll:

Como podemos ver, los valores tomados por la propiedad están divididos, de forma que realizando una búsqueda directamente como @dl:dataListItemType:”dh:listasClara” no obtendremos ningún resultado, también podemos verlo realizando la búsqueda en la propia utilidad:

Aquí se encuentran los registros porque LukeAll parte el valor como “dh” y “listasClara”, de forma que en Alfresco si usamos una consulta que solo busque “dh” la encontraremos:

Pero claro, esto ni es elegante ni fiable ya que no estamos buscando por todo el término completo. Esto nos interesa para, como digo, encontrar un tipo solamente de DataList, ya que buscando como tipo dataList encontraría todas las DataList de todos los sites.

¿Como se soluciona?

En concreto esto podemos solucionarlo pero hay que modificar el propio modelo de datos de Alfresco de la siguiente forma:

Donde se ha incluido la indexación desactivando la capacidad “tokenised”, de esta forma cuando volvemos a reindexar todo y comprobamos obtenemos:

Que es el resultado correcto y por tanto se puede buscar:

Pero esto implica, si, que tenemos que modificar el fichero original o sobrecargarlo para que funcione correctamente. Esto además es para todos los tipos, incluidos los que ya vienen predefinidos en Alfresco así que cuidado.

Bien, solucionado ¿no?, pues no, resulta que además para poder posicionarnos en el propio DataList que necesitamos (es un “folder”) debemos buscar por algo más, en principio podría ser por el nombre, pero Alfresco no nos deja que pongamos cualquier nombre, le pone uno de forma automática y poco descriptivo:

Lo que nos dificulta la búsqueda. Podemos cambiarlo, si, pero seguimos con el mismo problema que teníamos con el tipo “dl:dataListItemType”. Bueno, pues como si nos deja poner un título por ahí, igual, seguimos en las mismas…

— Solución —

La solución sencilla que he encontrado a esto es crear un aspecto nuevo y que lo asignemos a los DataList que necesitamos, por ejemplo:

De esta forma asignamos el aspecto y en “dh:nameOfDataList” podemos incluir cualquier nombre que buscará como tal, como cadena de texto.

Por ejemplo, si creando la siguiente DataList:

Y ahora buscando su “folder” en el repositorio:

Podemos asignarle directamente el aspecto:

Y el valor del campo que necesitamos:

Recomiendo un nombre bastante descriptivo como dl + site + nombre, por ejemplo para un site llamado Personal y una lista de Oficinas podría ser “dlPersonalOficinas”. Ahora ya solo nos queda comprobar que se indexa correctamente:

Y que se encuentra en Alfresco de forma correcta:

Con lo que ya tendríamos el “folder” del DataList y solo tendríamos que recorrer los “hijos” para cargar por ejemplo un control personalizado para rellenar alguna propiedad que nos interese.

Esta solución es viable para, como he comentado, usar tablas de datos maestros que se quieran tener directamente almacenadas en Alfresco (repositorio) y no tener que leerlas de fuentes externas como bases de datos o ficheros.

Además proporciona una ventaja adicional sobre la solución de leer datos de documentos que podemos tener también en Alfresco y es la facilidad que ya nos ofrece de crear nuevos “items” del DataList, modificarlos y borrarlos.

— Enlaces de interés —

https://code.google.com/p/luke/downloads/detail?name=lukeall-3.5.0.jar&

Alfviral 1.3.1-beta

Este puente de 3 días de fiesta los he dedicado, entre otras cosas, a crear mis proyectos y pasarlos de Google Code (¡gracias Google!) a GitHub y a refactorizar el código del módulo para la detección de virus en Alfresco, aunque las versiones anteriores, hasta la 1.3.0-420 seguirán estando en Google Code incluido el código fuente.

Entre las cosas que quedaban pendientes me ha dado tiempo a incluir 3 características principales aunque mi “hoja de ruta” va cambiando conforme tengo tiempo disponible así como las funcionalidades que los propios usuarios me vais pidiendo.
Las características principales añadidas a esta versión son:

  1. Incorporación del protocolo ICAP
  2. Notificaciones de infecciones a usuario y administrador
  3. Refactorización del código y creación del servicio AntivirusService

1. Incorporación del protocolo ICAP

El protocolo ICAP (Internet Content Adaptation Protocol) es un protocolo abierto para la redirección de contenidos con fines de filtrado y conversión. Este es muy usado para reenviar tráfico hacia antivirus, traducción, etc. En este caso, evidentemente, se utiliza para el envío de documentos de Alfresco hacia un servidor ICAP que se conecte a un antivirus, si bien, en realidad también podría utilizarse para más cosas, entre ellas traducción del documento, compresión, transformación, etc.

Se encuentra estandarizado en la RFC 3507 y para obtener más información se puede ir aquí.

Ahora, en Alfviral se puede configurar este modo como ICAP y con 3 parámetros de configuración que serán el servidor, puerto y servicio al que se necesita conectar. P.e. si usamos el servicor c-icap y lo configuramos para que utilice ClamAV podemos configurar el fichero alfviral.properties como:
alfviral.mode=ICAP
alfviral.icap.host=192.168.56.101
alfviral.icap.port=1344
alfviral.icap.service=srv_clamav
Aunque este sistema creo que es el mejor para casi todos los casos de uso he dejado los métodos anteriores para que puedan seguir siendo utilizados.

2. Notificaciones de infecciones a usuario y administrador

Aunque se podía realizar vía reglas de contenido, por ejemplo si hacíamos que los documentos infectados se movieran a una carpeta de cuarentena o infectados y ahí creábamos una acción de envío de correo, ahora se puede automatizar de forma general en la configuración de Alfviral. Por ahora se envían notificaciones al usuario que ha subido el documento y/o al administrador (admin) en forma de texto plano (text/plain) pero estoy trabajando para poder asignarle una plantilla personalizada según el caso. 
Por ahora para configurarlo basta con indicar a quién queremos enviarle las notificaciones.
alfviral.notify.user=true
alfviral.notify.admin=true

3. Refactorización del código y creación del servicio AntivirusService

Esto era algo que quería hacer hace tiempo. Hasta ahora todo el peso lo llevaba la clase VirusScan que era una acción, ahora ha pasado a llamarse VirusScanAction y he pasado la mayor parte del código a una nueva clase llamada AntivirusService y que funciona como servicio público, de hecho también he creado AntivirusServiceDescriptorRegistry y AntivirusServiceRegistry.
Esto hará más sencilla la actualización y extensión del módulo y la posibilidad de añadirle más métodos al servicio.
El módulo para descargar está disponible en el siguiente enlace: https://github.com/fegorama/alfviral/releases/download/v1.3.1-beta/alfviral-1.3.1-beta.zip
El código fuente se puede descargar de: https://github.com/fegorama/alfviral/archive/v1.3.1-beta.zip
El repositorio está en: https://github.com/fegorama/alfviral

Entrando por la “puerta de atrás” en Alfresco

Entre la arquitectura de funcionamiento de Alfresco se encuentra la capa de persistencia donde se guardan los datos necesarios para realizar las operaciones y tareas que hacen falta. Esta capa se divide a su vez en 4 elementos, la parte de configuración con ficheros de propiedades, la parte de almacenamiento de los documentos, el almacenamiento de los índices y el almacenamiento de las propiedades y otros valores (incluidas también configuraciones).

Esta última parte es guardada en un SGBD o Sistema de Gestión de Base de Datos (relacional) que puede ser MySQL, PostgreSQL, Oracle, SQL-Server, etc.
A veces, por determinadas circunstancias o necesidades debemos hacer uso de consultas directas a la base de datos para obtener datos, también sería posible modificar estos datos directamente pero no es aconsejable debido a que el control de lo que se guarda, modifica y borra lo tiene exclusivamente la aplicación de Alfresco. Como digo, en determinadas ocasiones es una posibilidad más el poder consultar directamente a la base de datos determinados datos que sean necesarios y de esta forma evitar pasar por la aplicación, por ejemplo en casos de que el servidor de aplicaciones (Alfresco) no levante correctamente, en casos de pérdidas de documentos y datos o de integridad que paren el servidor de aplicaciones o para monitorización por parte de algún programa externo.
Evidentemente este tipo de acciones son muy dependientes de la versión del esquema (schema) de Alfresco ya que este cambia con cada versión si bien, al estar en la capa del modelo de datos (no confundir con el modelo documental) cambia relativamente poco, sobre todo determinadas tablas muy importantes. De todas formas, es bueno tener el mapa de tablas y relaciones entre ellas para poder realizar bien cualquier consulta y tener la seguridad de que los datos proporcionados son coherentes. Hay que tener en cuenta que este modelo ha ido creciendo desde las primeras versiones, por ejemplo en la versión 2.1.0 Community había 24 tablas con el prefijo ALF, en la 3.4.11 había 46 y en la versión 4.2.2 hay 48 (solo dos tablas más con prefijo ALF). No se han tenido en cuenta las tablas usadas para el repositorio AVM, para el motor jBMP ni las utilizadas para Activiti.
Versión 2.1.0

Versión 2.2.6
Versión 3.4.11

Versión 4.2.2

Seguidamente voy a poner una serie de ejemplos en SQL lo más estándar posible, si bien, incluso lo más básico es dependiente también de la base de datos que use Alfresco en determinado momento. Estos son solo unos cuantos ejemplos de extracción de datos a través de la base de datos pero se pueden realizar muchas más operaciones de consulta según la necesidad concreta de cada uno.

Stores disponibles

Aunque no es muy necesario sí que a veces resulta interesante saber cuantos “almacenes” tiene nuestro repositorio, esto se podría realizar con la siguiente consulta:
SELECT * 
FROM alf_store;

Nodos disponibles

En Alfresco casi cualquier cosa es un nodo, un usuario, un grupo, una carpeta o un documento por ejemplo, para sacar los nodos disponibles podemos hacerlo como:
SELECT * 
FROM alf_node;
Pero no vamos a quedarnos aquí, podemos saber a que almacén pertenece cada nodo como:
SELECT uuid, protocol, identifier, alf_store.version 
FROM alf_node, alf_store
WHERE alf_node.store_id = alf_store.id;
Y ahora vamos a realizar una consulta que nos devuelva el nodo con la nomenclatura del tipo NodeRef, pero para esto ya nos encontramos con problemas, con la misma concatenación de campos, ya que dependerá de cada sistema de base de datos:
— Para Oracle
SELECT protocol || ‘://’ || identifier || ‘/’ || uuid AS nodeRef, 
alf_store.version, local_name   
FROM alf_node, 
alf_store, 
alf_qname   
WHERE alf_node.store_id = alf_store.id
AND   alf_node.type_qname_id = alf_qname.id;
— Para MySQL
SELECT CONCAT(protocol, ‘://’, identifier, ‘/’, uuid) AS nodeRef, 
alf_store.version, 
local_name   
FROM alf_node, 
alf_store, 
alf_qname   
WHERE alf_node.store_id = alf_store.id
AND   alf_node.type_qname_id = alf_qname.id;
— Para SQL-Server
SELECT protocol + ‘://’ + identifier + ‘/’ + uuid AS nodeRef, 
alf_store.version, 
local_name   
FROM alf_node, 
alf_store, 
alf_qname   
WHERE alf_node.store_id = alf_store.id
AND   alf_node.type_qname_id = alf_qname.id;

Extrayendo metadatos

La tabla más importante en Alfresco es alf_data_properties, esta es la que mantiene los valores de todas la propiedades de todos los nodos disponibles y es la que es más utilizada para devolver información ya que generalmente casi cualquier dato en Alfresco será una propiedad de un nodo. Por tanto, las siguientes tablas podríamos decir que son las básicas a la hora de realizar consultas directas:
  • alf_store : Guarda la información de los almacenes disponibles
  • alf_node : Guarda los nodos, aquí obtenemos el uuid que corresponde (con otros campos) al llamado NodeRef (referencia de nodo)
  • alf_qname : En versiones posteriores a 2.1 (2.2.x, 3.1.x, 3.2.x, 3.4.x, etc.) contiene los nombres identificativos de los tipos de propiedades, en versiones anteriores se almacenaba en un campo llamado alf_qname de la tabla alf_node_properties
  • alf_node_properties : Sin duda la tabla más importante a la hora de extraer datos en Alfresco, guarda los valores de las propiedades de los modelos de datos documentales de Alfresco, incluyendo los datos de las carpetas, documentos, usuarios, grupos, etc.
Con estas y alguna más de forma auxiliar podremos obtener la información necesaria.
Es interesante el uso de la tabla alf_qname (en versiones 2.2.x en adelante) porque con esta podemos posteriormente determinar que tipo de propiedad queremos, por tanto es buena idea tener un listado de los posibles nombres cualificados posibles:
— Versiones 2.2.x y siguientes
SELECT
FROM    alf_qname;
— Versiones 2.1.x
SELECT alf_node_properties.qname 
FROM alf_node_properties;
Veamos dos ejemplos sobre el uso de extraer propiedades, uno sobre los usuarios y otro sobre los documentos:
En ocasiones necesitamos saber los datos de un usuario en particular, las siguientes consultas nos devuelven un listado de usuarios:
— Versiones 2.2.x y siguientes
SELECT
    alf_node_properties.node_id,
    alf_node_properties.string_value
FROM
    alf_node_properties,
    alf_qname
WHERE
    alf_node_properties.qname_id = alf_qname.id
AND
    alf_qname.local_name = ‘userName’;
— Versiones 2.1.x
SELECT
    alf_node_properties.node_id,
    alf_node_properties.string_value 
FROM
    alf_node_properties 
WHERE
    alf_node_properties.qname = ‘{http://www.alfresco.org/model/content/1.0}userName’;
Y las siguientes (según la versión de Alfresco) nos devolverán la propiedades de un usuario concreto (sustituir NOMBRE-DEL-USUARIO por el nombre del usuario a buscar)
— Versiones 2.2.x y siguientes
SELECT
    alf_node_properties.node_id,
    alf_qname.local_name,
    alf_node_properties.string_value
FROM
    alf_node_properties,
    alf_qname
WHERE
    alf_node_properties.qname_id = alf_qname.id
AND
    alf_node_properties.node_id = (
        SELECT
            alf_node_properties.node_id
        FROM
            alf_node_properties,
            alf_qname
        WHERE
            alf_node_properties.qname_id = alf_qname.id
        AND
            alf_qname.local_name = ‘userName’
        AND
            alf_node_properties.string_value = ‘admin’
        );
— Versiones 2.1.x
SELECT
    alf_node_properties.node_id,
    alf_node_properties.qname, 
    alf_node_properties.string_value 
FROM
    alf_node_properties 
WHERE
    alf_node_properties.node_id = (
        SELECT
            alf_node_properties.node_id
        FROM
            alf_node_properties 
        WHERE
            alf_node_properties.qname = ‘{http://www.alfresco.org/model/content/1.0}userName’
        AND
            alf_node_properties.string_value = ‘NOMBRE-DEL-USUARIO’
        );
Y por último una aplicación real cuando se necesita el nombre que Alfresco ha puesto en el repositorio. En casos de recuperación de documentos necesitamos saber donde está guardado el fichero en el repositorio a partir del nombre del documento guardado en Alfresco. Esta consulta nos devuelve esta información a partir del nombre del documento. Es para la versión 3.4.x y 4.x
— Versiones 3.4.x y 4.x
SELECT alf_node_properties.node_id,
alf_node_properties.string_value,
alf_content_url.content_url
FROM alf_node_properties,
alf_qname,
alf_content_data,
alf_node_properties,
alf_content_url
WHERE alf_node_properties.string_value = ‘NOMBRE-DEL-DOCUMENTO’ 
AND alf_node_properties.qname_id = alf_qname.id 
AND alf_qname.local_name = ‘name’ 
AND alf_content_data.id = alf_node_properties.long_value 
AND alf_node_properties.node_id = alf_node_properties.node_id 
AND alf_node_properties.long_value = alf_content_data.id 
AND alf_content_data.content_url_id = alf_content_url.id;

Cluster con Hazelcast en Alfresco One 4.2.2

Si hay algo en lo que Alfresco ha trabajado en cada una de las versiones que han visto la luz ha sido el tema de cluster y la comunicación entre los nodos. En las versiones 2.x con EHCache y un sistema de multicast que era bastante pobre, usando JGroups en la 3.x hasta llegar a la 4.2.x con Hazelcast.

Pero, ¿qué es Hazelcast?

Es según la propia página web oficial un “Open Source In-Memory Data Grid”, es decir, una plataforma para la distribución de datos de código abierto. Entre sus características podemos encontrar:

  • Implementaciones distribuidas de Set, List, Map, Lock, MultiMap
  • Mensajería distribuida P/S
  • Soporte transaccional e integración JEE vía JCA
  • Soporte encriptación a nivel de sockets
  • Persistencia síncrona o asíncrona
  • Clusterizado Sesión HTTP
  • Discovery dinámico
  • Monitorización JMX
  • Escalado dinámico
  • Particionado dinámico
  • Fail-over dinámico

Como se puede ver, es una herramienta fantástica para cumplir las especificaciones de cluster que necesita Alfresco.

Las funciones de las que se sirve Alfresco y que son comunes a Hazelcast están:

  • Compartir datos/estados entre varios servidores: como compartición sesión Web
  • Cacheo distribuido de datos
  • Comunicación segura entre servidores
  • Particionado de datos en memoria
  • Distribución de trabajo entre servidores
  • Procesamiento paralelo
  • Gestión fail-safe de datos

Además se lleva muy bien con Hibernate como caché de segundo nivel y con Spring.

¿Cómo configuramos el cluster de Alfresco 4.2.2?

Para configurar un sistema de cluster en Alfresco 4.2.2 es tan fácil como cuando se configuraba con EHCache o JGroups e incluso más todavía y eso sí, se comprueba la fiabilidad que tiene este producto integrado en Alfresco.

Hay que entender que aquí explico solamente como montar el cluster, es decir, que ambos nodos se comuniquen entre sí, un sistema completo de alta disponibilidad requiere de un balanceador ya sea hardware o software, un sistema de cluster en la base de datos, etc.

Lo primero que hay que hacer es quitar cualquier referencia a EHCache y JGroups antiguos, esto va orientado a sistemas que han ido siendo actualizados desde versiones antiguas principalmente:

Por ejemplo, el fichero que está dentro de {alfrescoRoot}/tomcat/shared/clases/alfresco/extensión:

ehcache-custom.xml

También en dicha localización (si existe) el fichero:

hazelcastConfig.xml

Este fichero se ha incluido ya dentro del fichero alfresco.war con lo que no hace falta.

Así como las siguientes propiedades que están dentro de {alfrescoRoot}/tomcat/shared/clases/alfresco-global.properties:

alfresco.cluster.name
alfresco.ehcache.rmi.hostname
alfresco.ehcache.rmi.port
alfresco.ehcache.rmi.remoteObjectPort
alfresco.jgroups.defaultProtocol
alfresco.jgroups.bind_address
alfresco.jgroups.bind_interface
alfresco.tcp.start_port
alfresco.tcp.initial_hosts
alfresco.tcp.port_range
alfresco.udp.mcast_addr
alfresco.udp.mcast_port
alfresco.udp.ip_ttl
filesystem.cluster.enabled
filesystem.cluster.configFile

Configuración del cluster para el repositorio

Por defecto si apuntamos dos instancias de Alfresco al mismo repositorio y base de datos, estos formarán de forma automática un grupo de repositorio, no obstante hay que realizar una pequeña configuración para que todo funcione correctamente.

Montar el repositorio de forma compartida y visible para todos los nodos, por ejemplo vía NAS o SAN a través de protocolo NFS.

Configurar el acceso a la base de datos para la misma base de datos en cada uno de los nodos.

Abrir el puerto 5701 TCP en el cortafuegos de los nodos para que puedan ser accesibles entre ellos.

Especificar correctamente la IP (sea en wildcard como por ejemplo 192.168.1.*) de la tarjeta de red del cluster:

alfresco.cluster.interface=192.168.1.101

Fijar la propiedad para activar Hazelcast en JMX

hazelcast.jxm=true

Y por razones de seguridad se debería fijar la contraseña con la siguiente propiedad:

alfresco.hazelcast.password=

Un ejemplo de la parte del fichero alfresco-global.properties para la configuración del cluster puede ser la siguiente:

alfresco.cluster.enabled=true
alfresco.cluster.interface=192.168.1.101
alfresco.hazelcast.password=clavehazelcast
alfresco.hazelcast.port=5701
alfresco.hazelcast.autoinc.port=false
alfresco.hazelcast.mancenter.enabled=false
alfresco.hazelcast.max.no.heartbeat.seconds=15

Una vez arrancada la primera instancia se puede observar un mensaje como el siguiente:

2014-06-30 22:38:36,148 INFO [cluster.core.ClusteringBootstrap] [localhost-startStop-1] Cluster started, name: MainRepository-fea9ebdf-04f3-495e-9456-cf43c24b8e91
2014-06-30 22:38:36,152 INFO [cluster.core.ClusteringBootstrap] [localhost-startStop-1] Current cluster members:
192.168.1.101:5701 (hostname: alfnode1.localdomain)

Finalmente al arrancar el segundo en este se observará lo siguiente:

2014-07-02 10:58:12,108 INFO [cluster.core.ClusteringBootstrap] [localhost-startStop-1] Cluster started, name: MainRepository-fea9ebdf-04f3-495e-9456-cf43c24b8e91
2014-07-02 10:58:12,111 INFO [cluster.core.ClusteringBootstrap] [localhost-startStop-1] Current cluster members:
192.168.1.102:5701 (hostname: alfnode2.localdomain)
192.168.1.101:5701 (hostname: alfnode1.localdomain)

También se puede ver que el cluster está bien configurado mediante la nueva consola de administración cuya URL es:

http://
:8080/alfresco/service/enterprise/admin

En “Servicio de repositorio” y dentro de este en “Agrupación de servidores del repositorio” se puede ver toda la información del cluster, además se puede validar con el botón “Validar grupo” que realiza las comprobaciones necesarias para saber si ambos nodos se están comunicando correctamente:

Propiedades de Hazelcast

Todas las propiedades admitidas por Hazelcast en alfresco-global.properties son:

alfresco.cluster.enabled
Ejemplo: true
Descripción: Activa el cluster de Alfresco para este nodo

alfresco.cluster.interface
Ejemplo: 192.168.80.1
Descripción: Especifica la tarjeta de red usada para el cluster. Se puede usar tipo de dirección wildcard, por ejemplo 192.168.80.*

alfresco.cluster.nodetype
Ejemplo: NodoDesconectado001
Descripción: Especifica un nombre “amigable” para ese nodo del cluster, generalmente utilizado para servidores que se han unido al repositorio pero no forman parte del cluster (p.e. servidores de indexación)

alfresco.hazelcast.password
Ejemplo: mipasswd
Descripción: Define el password que usarán los nodos del cluster

alfresco.hazelcast.port
Ejemplo: 5701
Descripción: Establece el puerto de comunicación entre nodos del cluster

alfresco.hazelcast.autoinc.port
Ejemplo: false
Descripción: Realiza varios intentos de puertos para hayar uno libre desde la configuración alfresco.hazelcast.port. Alfresco no recomienda establecer esta propiedad

alfresco.hazelcast.mancenter.enabled
Ejemplo: false
Descripción: Activa las estadísticas y otros valores del cluster donde se puede acceder a través del Centro de gestión de Hazelcast

alfresco.hazelcast.mancenter.url
Ejemplo: http://localhost:8080/mancenter
Descripción: URL de acceso al centro de gestión de Hazelcast, evidentemente alfresco.hazelcast.mancenter.enabled debe estar en valor true

alfresco.hazelcast.max.no.heartbeat.seconds
Ejemplo: 15
Descripción: Tiempo máximo de monitorización para que se de por hecho que un nodo no está respondiendo

Configuración de Hazelcast en Share

En un entorno de cluster, Alfresco Share ahora utiliza Hazelcast para proporcionar mensajes entre los nodos de la capa web. Como resultado, las cachés ya no necesitan estar deshabilitadas para cualquier nodo. Cada uno funciona prácticamente tan rápido como una sola instancia de Share, mejorando así su rendimiento general.

Se pueden realizar dos configuraciones según las necesidades, con multicast o a nivel de TCP directo.

En todo caso, en balanceadores hay que seguir usando el sistema de Sticky-Session para funcionar correctamente. Hay que configurar correctamente el fichero share-config-custom.xml dentro de {extensionRoot}/alfresco/classes/web-extension poniendo correctamente el host y puerto de acceso al repositorio en caso necesario.

También hay que tener en cuenta que si se usa autenticación Kerberos o NTML con SSO las sesiones utilizarán la cookie JSESSIONID por lo que habrá que tenerla en cuenta por parte del balanceador.

Para esto hay que configurar el fichero custom-slingshot-application-context.xml
que hay en {extensionRoot}/alfresco/classes/web-extension (quitándole la extensión .sample)

Ejemplo para multicast:

 <!– Hazelcast distributed messaging configuration – Share web-tier cluster
    config (3.4.8 and 4.0.1) – see http://www.hazelcast.com/docs.jsp – and specifically
    http://www.hazelcast.com/docs/1.9.4/manual/single_html/#SpringIntegration –>
<!– Configure cluster to use either Multicast or direct TCP-IP messaging
    – multicast is default –>
<!– Optionally specify network interfaces – server machines likely to have
    more than one interface –>
<!– The messaging topic – the "name" is also used by the persister config
    below –>
<hz:topic id="topic" instance-ref="webframework.cluster.slingshot"
    name=”slingshot-topic” />

    
        
        
            
                <hz:multicast enabled="true" multicast-group="224.2.2.5"
                    multicast-port=”54327″ />
                
                    
                
            
            
                192.168.1.*
            
        
    

<bean id="webframework.slingshot.persister.remote"
    class=”org.alfresco.web.site.ClusterAwarePathStoreObjectPersister”
    parent=”webframework.sitedata.persister.abstract”>
    
    
        alfresco/site-data/${objectTypeIds}
    
    
    
        slingshot-topic
    

<bean id="webframework.factory.requestcontext.servlet" class="org.alfresco.web.site.ClusterAwareRequestContextFactory"
    parent=”webframework.factory.base”>
    
    
    
    

Ejemplo para conexión directa TCP:

<!– Hazelcast distributed messaging configuration – Share web-tier cluster
    config (3.4.8 and 4.0.1) – see http://www.hazelcast.com/docs.jsp – and specifically
    http://www.hazelcast.com/docs/1.9.4/manual/single_html/#SpringIntegration –>
<!– Configure cluster to use either Multicast or direct TCP-IP messaging
    – multicast is default –>
<!– Optionally specify network interfaces – server machines likely to have
    more than one interface –>
<!– The messaging topic – the "name" is also used by the persister config
    below –>
<hz:topic id="topic" instance-ref="webframework.cluster.slingshot"
    name=”slingshot-topic” />

    
        
        
            
                <hz:multicast enabled="false" multicast-group="224.2.2.5"
                    multicast-port=”54327″ />
                
                    alfnode1,alfnode2
                
            
            
                192.168.1.*
            
        
    

<bean id="webframework.slingshot.persister.remote"
    class=”org.alfresco.web.site.ClusterAwarePathStoreObjectPersister”
    parent=”webframework.sitedata.persister.abstract”>
    
    
        alfresco/site-data/${objectTypeIds}
    
    
    
        slingshot-topic
    

<bean id="webframework.factory.requestcontext.servlet" class="org.alfresco.web.site.ClusterAwareRequestContextFactory"
    parent=”webframework.factory.base”>
    
    
    
    

Centro de gestión Hazelcast (mancenter):

El centro de gestión Hazelcast (mancenter) permite monitorizar y administrar los servidores que ejecutan Hazelcast. Además, mancenter permite supervisar el estado general de los clústeres, y analizar y examinar las estructuras de datos en detalle.

Para instalarlo, se puede instalar tanto en un tomcat distinto como en el mismo de Alfresco. Solo hay que bajar una versión de Hazelcast (mancenter) y copiar el fichero mancenter-x.x.x.war al directorio de aplicaciones de tomcat.

Por ejemplo:

cp mancenter-2.4.1.war /opt/Alfresco422/tomcat/webapps/mancenter.war

Establecer la propiedad hazelcast.mancenter.home con el directorio donde se almacenan los datos, aquí se puede poner en la misma línea de opciones de Java (JAVA_OPTS), por ejemplo:

-Dhazelcast.mancenter.home=/opt/Alfresco422/tomcat/mancenter_data

Acordarse de activarlo en alfresco-global.properties:

alfresco.hazelcast.mancenter.enabled=true

Establecer la url de acceso, por ejemplo:

alfresco.hazelcast.mancenter.url=http://192.168.1.101:8080/mancenter

Por último, si se produce un error de serialización en el arranque, descomentar la siguiente línea en el contex.xml del servidor Tomcat:


 

Monitorización del funcionamiento de Hazelcast en Alfresco

La mejor forma como siempre es usando Log4j y para esto se puede usar la siguiente propiedad:

log4j.logger.org.alfresco.enterprise.repo.cluster=info

Para monitorizar la caché también se usan las siguientes propiedades:

log4j.logger.org.alfresco.enterprise.repo.cluster.cache=DEBUG
log4j.logger.org.alfresco.repo.cache=DEBUG

A nivel del propio Hazelcast:

log4j.logger.com.hazelcast=info

Y para aumentar el registro de seguimiento también se puede usar:

log4j.logger.com.hazelcast.impl.TcpIpJoiner=debug

Para finalizar

Las pruebas realizadas con un cluster de Alfresco One 4.2.2 usando Hazelcast han resultado ser muy satisfactorias, he realizado pruebas de subida de documentos, cambios de propiedades, etc. y eran instantáneas en ambos nodos.

Hay que tener en cuenta además que hay que configurar Solr (si se usa esta opción de indexado) correctamente para que se use de forma compartida, siempre y cuando no se utilize protocolo NFS para estos recursos compartidos de red ya que no está aconsejado. En este cado (uso de NFS) también se puede seguir usando una configuración similar a la que se utilizaba con Lucene, es decir, mantener índices locales por cada nodo.

Más información

http://docs.alfresco.com/4.2/concepts/ha-intro.html
http://hazelcast.org
http://unpocodejava.wordpress.com/2013/01/21/que-es-hazelcast/