четверг, 25 марта 2010 г.

Struts 2 на Google App Engine. Рецепты настройки

image

Для использования одного из самых удобных веб фреймоврков для java на популярной облачной платформе Google AppEngine необходимо совершить ряд действий. Данная статья появилась в результате моих продолжительных экспериментов.

Используемые библиотеки

Я использую обычно следующий набор библиотек:

  • commons-fileupload-1.2.1.jar

  • commons-io-1.3.2.jar

  • commons-logging-1.0.4.jar

  • commons-logging-api-1.1.jar

  • freemarker-2.3.13.jar

  • ognl-2.6.11.jar

  • struts2-core-2.1.6.jar

  • xwork-2.1.2.jar


Я пользуюсь IntelliJ Idea 9 и она подкачивает зависимости автоматически после того, как я указываю использование Struts 2 при создании проекта. Также существует возможность развертывать приложения на облачной платформе через специальное меню IDE, и вообще поддержка Appengine на очень хорошем уровне.

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

Настройка Ognl SecurityManager-а

Необходимо создать листнер, настраивающий Ognl SecurityManager при старте приложения:
  1. package su.mvc.util;
  2.  
  3. import ognl.OgnlRuntime;
  4.  
  5. import javax.servlet.ServletContextEvent;
  6. import javax.servlet.ServletContextListener;
  7.  
  8. public class GaeContextListener implements ServletContextListener {
  9.   public void contextInitialized(ServletContextEvent servletContextEvent) {
  10.     OgnlRuntime.setSecurityManager(null);
  11.   }
  12.  
  13.   public void contextDestroyed(ServletContextEvent servletContextEvent) {
  14.     //
  15.   }
  16. }


А далее подключить его в дескрипторе развертывания приложения web.xml:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <web-app xmlns="http://java.sun.com/xml/ns/javaee"
  3.       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4.       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
  5.      http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
  6.       version="2.5">
  7.  
  8.   <!-- Listeners -->
  9.   <listener>
  10.     <listener-class>su.mvc.util.GaeContextListener</listener-class>
  11.   </listener>
  12.   <!-- ... -->
  13.  
  14. </web-app>


Преобразование типов Test и Key

В Appengine широко используются следующие типы:

  • com.google.appengine.api.datastore.Text для управления большими текстовыми данными

  • com.google.appengine.api.datastore.Key используется в качестве @Id для сущностей. К сожалению, AppEngine позволяет корректно работать с отношениями сущностей только в случае использования Key в качестве @Id


Для использования этих типов на веб формах необходимо определить прямое и обратное преобразование этих типов к строке:
  1. package su.mvc.util.converters;
  2.  
  3. import com.google.appengine.api.datastore.Text;
  4. import ognl.DefaultTypeConverter;
  5.  
  6. import java.util.Map;
  7.  
  8. public class TextConverter extends DefaultTypeConverter {
  9.   @Override
  10.   public Object convertValue(Map map, Object o, Class toType) {
  11.     if (toType == Text.class) {
  12.       String value = ((String[]) o)[0];
  13.       return new Text(value);
  14.     } else if (toType == String.class) {
  15.       Text text = (Text) o;
  16.       return text.getValue();
  17.     }
  18.  
  19.     return null;
  20.   }
  21. }


  1. package su.mvc.util.converters;
  2.  
  3. import com.google.appengine.api.datastore.Key;
  4. import com.google.appengine.api.datastore.KeyFactory;
  5. import ognl.DefaultTypeConverter;
  6.  
  7. import java.util.Map;
  8.  
  9. public class KeyConverter extends DefaultTypeConverter {
  10.   @Override
  11.   public Object convertValue(Map map, Object o, Class toType) {
  12.     if (toType == Key.class) {
  13.       String s = ((String[]) o)[0];
  14.       return KeyFactory.stringToKey(s);
  15.     } else if (toType == String.class) {
  16.       Key k = (Key) o;
  17.       return KeyFactory.keyToString(k);
  18.     }
  19.  
  20.     return null;
  21.   }
  22. }


Осталось задекларировать созданные конверторы в файле xwork-conversion.properties:

  1. сom.google.appengine.api.datastore.Text=su.mvc.util.converters.TextConverter
  2. com.google.appengine.api.datastore.Key=su.mvc.util.converters.KeyConverter


Использование Convention плагина

Горячо любимый мною Convention плагин используется для реализации концепции Convention-Over-Configuration и существенно сокращает время на конфигурирование приложение. К сожалению при работе со Struts 2 придется забыть об этом плагине.

Патч Freemarker на TextBlock

Даже если Вы не используете Freemarker для реализации UI, при использовании стратсовых JSP тегов Freemarker необходимо пропатчить. Для этого необходимо создать в своем приложении класс freemarker.core.TextBlock следующего содержания:
//
//Workaround for http://groups.google.com/group/google-appengine-java/browse_thread/thread/186c3fc515e6f6ae
//

/*
* Copyright (c) 2003 The Visigoth Software Society. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
*  notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above
copyright
*  notice, this list of conditions and the following disclaimer in
*  the documentation and/or other materials provided with the
*  distribution.
*
* 3. The end-user documentation included with the redistribution, if
*  any, must include the following acknowledgement:
*    "This product includes software developed by the
*    Visigoth Software Society (http://www.visigoths.org/)."
*  Alternately, this acknowledgement may appear in the software
itself,
*  if and wherever such third-party acknowledgements normally
appear.
*
* 4. Neither the name "FreeMarker", "Visigoth", nor any of the names
of the
*  project contributors may be used to endorse or promote products
derived
*  from this software without prior written permission. For written
*  permission, please contact visigo...@visigoths.org.
*
* 5. Products derived from this software may not be called
"FreeMarker" or "Visigoth"
*  nor may "FreeMarker" or "Visigoth" appear in their names
*  without prior written permission of the Visigoth Software
Society.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Visigoth Software Society. For more
* information on the Visigoth Software Society, please see
* http://www.visigoths.org/
*/

package freemarker.core;

import java.io.IOException;

/**
* A TemplateElement representing a block of plain text.
* @version $Id: TextBlock.java,v 1.17 2004/01/06 17:06:42 szegedia
Exp $
*/
public final class TextBlock extends TemplateElement {
  private static final char[] EMPTY_CHAR_ARRAY = new char[0];
  static final TextBlock EMPTY_BLOCK = new TextBlock
(EMPTY_CHAR_ARRAY, false);
  // We're using char[] instead of String for storing the text block because
  // Writer.write(String) involves copying the String contents to a char[]
  // using String.getChars(), and then calling Writer.write(char[]). By
  // using Writer.write(char[]) directly, we avoid array copying on each
  // write.
  private char[] text;
  private final boolean unparsed;

  public TextBlock(String text) {
    this(text, false);
  }

  public TextBlock(String text, boolean unparsed) {
    this(text.toCharArray(), unparsed);
  }

  private TextBlock(char[] text, boolean unparsed) {
    this.text = text;
    this.unparsed = unparsed;
  }

  /**
   * Simply outputs the text.
   */
  public void accept(Environment env)
  throws IOException
  {
    env.getOut().write(text);
  }

  public String getCanonicalForm() {
    String text = new String(this.text);
    if (unparsed) {
      return "<#noparse>" + text + "</#noparse>";
    }
    return text;
  }

  public String getDescription() {
    String s = new String(text).trim();
    if (s.length() == 0) {
      return "whitespace";
    }
    if (s.length() > 20) {
      s = s.substring(0,20) + "...";
      s = s.replace('\n', ' ');
      s = s.replace('\r', ' ');
    }
    return "text block (" + s + ")";
  }

  TemplateElement postParseCleanup(boolean stripWhitespace) {
    if (text.length == 0) return this;
    int openingCharsToStrip = 0, trailingCharsToStrip=0;
    boolean deliberateLeftTrim = deliberateLeftTrim();
    boolean deliberateRightTrim = deliberateRightTrim();
    if (!stripWhitespace || text.length == 0 ) {
      return this;
    }
    if (parent.parent == null && previousSibling() == null) return
this;
    if (!deliberateLeftTrim) {
      trailingCharsToStrip = trailingCharsToStrip();
    }
    if (!deliberateRightTrim) {
      openingCharsToStrip = openingCharsToStrip();
    }
    if (openingCharsToStrip == 0 && trailingCharsToStrip == 0) {
      return this;
    }
    this.text = substring(text, openingCharsToStrip, text.length - trailingCharsToStrip);
    if (openingCharsToStrip > 0) {
      this.beginLine++;
      this.beginColumn = 1;
    }
    if (trailingCharsToStrip >0) {
      this.endColumn = 0;
    }
    return this;
  }

  /**
   * Scans forward the nodes on the same line to see whether there
is a
   * deliberate left trim in effect. Returns true if the left trim
was present.
   */
  private boolean deliberateLeftTrim() {
    boolean result = false;
    for (TemplateElement elem = this.nextTerminalNode();
       elem != null && elem.beginLine == this.endLine;
       elem = elem.nextTerminalNode())
    {
      if (elem instanceof TrimInstruction) {
        TrimInstruction ti = (TrimInstruction) elem;
        if (!ti.left && !ti.right) {
          result = true;
        }
        if (ti.left) {
          result = true;
          int lastNewLineIndex = lastNewLineIndex();
          if (lastNewLineIndex >=0 || beginColumn == 1) {
            char[] firstPart = substring(text, 0, lastNewLineIndex + 1);
            char[] lastLine = substring(text, 1+lastNewLineIndex);
            if (trim(lastLine).length == 0) {
              this.text = firstPart;
              this.endColumn = 0;
            } else {
              int i =0;
              while (Character.isWhitespace(lastLine [i])) {
                i++;
              }
              char[] printablePart = substring(lastLine, i);
              this.text = concat(firstPart, printablePart);
            }
          }
        }
      }
    }
    if (result) {
    }
    return result;
  }

  /**
   * Checks for the presence of a t or rt directive on the
   * same line. Returns true if the right trim directive was
present.
   */
  private boolean deliberateRightTrim() {
    boolean result = false;
    for (TemplateElement elem = this.prevTerminalNode();
       elem != null && elem.endLine == this.beginLine;
       elem = elem.prevTerminalNode())
    {
      if (elem instanceof TrimInstruction) {
        TrimInstruction ti = (TrimInstruction) elem;
        if (!ti.left && !ti.right) {
          result = true;
        }
        if (ti.right) {
          result = true;
          int firstLineIndex = firstNewLineIndex() +1;
          if (firstLineIndex == 0) {
            return false;
          }
          if (text.length > firstLineIndex
            && text[firstLineIndex-1] == '\r'
            && text[firstLineIndex] == '\n')
          {
            firstLineIndex++;
          }
          char[] trailingPart = substring(text, firstLineIndex);
          char[] openingPart = substring(text, 0, firstLineIndex);
          if (trim(openingPart).length ==0) {
            this.text = trailingPart;
            this.beginLine++;
            this.beginColumn=1;
          } else {
            int lastNonWS = openingPart.length -1;
            while (Character.isWhitespace(text[lastNonWS])) {
              lastNonWS--;
            }
            char[] printablePart = substring(text, 0,lastNonWS+1);
            if (trim(trailingPart).length == 0) {
            // THIS BLOCK IS HEINOUS! THERE MUST BE A BETTER WAY! REVISIT (JR)
              boolean trimTrailingPart = true;
              for (TemplateElement te = this.nextTerminalNode();
                 te != null && te.beginLine == this.endLine;
                 te = te.nextTerminalNode())
              {
                if (te.heedsOpeningWhitespace())
                {
                  trimTrailingPart = false;
                }
                if (te instanceof TrimInstruction && ((TrimInstruction) te).left) {
                  trimTrailingPart = true;
                  break;
                }
              }
              if (trimTrailingPart) trailingPart = EMPTY_CHAR_ARRAY;
            }
            this.text = concat(printablePart, trailingPart);
          }
        }
      }
    }
    return result;
  }
/*
  private String leftTrim(String s) {
    int i =0;
    while (i<s.length()) {
      if (!Character.isWhitespace(s.charAt(i)))
        break;
      ++i;
    }
    return s.substring(i);
  }
*/
  private int firstNewLineIndex() {
    String content = new String(text);
    int newlineIndex1 = content.indexOf('\n');
    int newlineIndex2 = content.indexOf('\r');
    int result = newlineIndex1 >=0 ? newlineIndex1 : newlineIndex2;
    if (newlineIndex1 >=0 && newlineIndex2 >=0) {
      result = Math.min(newlineIndex1, newlineIndex2);
    }
    return result;
  }

  private int lastNewLineIndex() {
    String content = new String(text);
    return Math.max(content.lastIndexOf('\r'), content.lastIndexOf
('\n'));
  }

  /**
   * figures out how many opening whitespace characters to strip
   * in the post-parse cleanup phase.
   */
  private int openingCharsToStrip() {
    int newlineIndex = firstNewLineIndex();
    if (newlineIndex == -1 && beginColumn != 1) {
      return 0;
    }
    ++newlineIndex;
    if (text.length > newlineIndex) {
      if (newlineIndex >0 && text[newlineIndex-1] == '\r' && text[newlineIndex] == '\n') {
        ++newlineIndex;
      }
    }
    if (new String(text).substring(0, newlineIndex).trim().length() >0) {
      return 0;
    }
// We look at the preceding elements on the line to see if we should
// strip the opening newline and any whitespace preceding it.
    for (TemplateElement elem = this.prevTerminalNode();
       elem != null && elem.endLine == this.beginLine;
       elem = elem.prevTerminalNode())
    {
      if (elem.heedsOpeningWhitespace())
      {
        return 0;
      }
    }
    return newlineIndex;
  }

  /**
   * figures out how many trailing whitespace characters to strip
   * in the post-parse cleanup phase.
   */
  private int trailingCharsToStrip() {
    String content = new String(text);
    int lastNewlineIndex = lastNewLineIndex();
    if (lastNewlineIndex == -1 && beginColumn != 1) {
      return 0;
    }
    String substring = content.substring(lastNewlineIndex +1);
    if (substring.trim().length() >0) {
      return 0;
    }
// We look at the elements afterward on the same line to see if weshould
// strip any whitespace after the last newline
    for (TemplateElement elem = this.nextTerminalNode();
       elem != null && elem.beginLine == this.endLine;
       elem = elem.nextTerminalNode())
    {
      if (elem.heedsTrailingWhitespace())
      {
        return 0;
      }
    }
    return substring.length();
  }

  boolean heedsTrailingWhitespace() {
    if (isIgnorable()) {
      return false;
    }
    for (int i=0; i<text.length; i++) {
      char c = text[i];
      if (c=='\n' || c=='\r') {
        return false;
      }
      if (!Character.isWhitespace(c)) {
        return true;
      }
    }
    return true;
  }

  boolean heedsOpeningWhitespace() {
    if (isIgnorable()) {
      return false;
    }
    for (int i = text.length -1; i>=0; i--) {
      char c = text[i];
      if (c == '\n' || c == '\r') {
        return false;
      }
      if (!Character.isWhitespace(c)) {
        return true;
      }
    }
    return true;
  }

  boolean isIgnorable() {
    if (text == null || text.length == 0) {
      return true;
    }
    if (!isWhitespace()) {
      return false;
    }
// trick here
    boolean atTopLevel = true;
    TemplateElement prevSibling = previousSibling();
    TemplateElement nextSibling = nextSibling();
    return ((prevSibling == null && atTopLevel) || nonOutputtingType(prevSibling))
       && ((nextSibling == null && atTopLevel) || nonOutputtingType(nextSibling));
  }

  private boolean nonOutputtingType(TemplateElement element) {
    return (element instanceof Macro ||
        element instanceof Assignment ||
        element instanceof AssignmentInstruction ||
        element instanceof PropertySetting ||
        element instanceof LibraryLoad ||
        element instanceof Comment);
  }

  private static char[] substring(char[] c, int from, int to) {
    char[] c2 = new char[to - from];
    System.arraycopy(c, from, c2, 0, c2.length);
    return c2;
  }

  private static char[] substring(char[] c, int from) {
    return substring(c, from, c.length);
  }

  private static char[] trim(char[] c) {
    if (c.length == 0) {
      return c;
    }
    return new String(c).trim().toCharArray();
  }

  private static char[] concat(char[] c1, char[] c2) {
    char[] c = new char[c1.length + c2.length];
    System.arraycopy(c1, 0, c, 0, c1.length);
    System.arraycopy(c2, 0, c, c1.length, c2.length);
    return c;
  }

  boolean isWhitespace() {
    return text == null || trim(text).length == 0;
  }

}



Буду рад, если Вы поделитесь своим опытом работы с вашими любимыми фреймворками на платформе GAE в коментариях :-)

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

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