XPages: WebContent Files (3) – Create a Minimizer Servlet

Because of Stefano Fois comment I decided to write an example about how to create a minimizer servlet for Domino which compresses JavaScript resources on the fly. This is, again, a simple Proof-Of-Concept, nothing more and nothing less.

First, I downloaded the YUICompressor, a Java based minimizer for JavaScript code from the project page. There are other compressors outside, I decided to use this one because it was the first result in my StartPage.com search.

The project is a single jar file and can be easily imported into an existing Domino database, in my case to my demonstration NAPI.nsf.

The next step is to create a servlet and a servlet factory, the basics are described here. To enable the servlet, an additional service file is required.

The file com.ibm.xsp-adapter.servletFactory contains the name of the factory class:

ch.hasselba.jsf.servlet.MinimizerServletFactory

The servlet factory is simple, it just defines the servlet class to use and the path which the servlet listens to requests:

package ch.hasselba.jsf.servlet;

import javax.servlet.Servlet;
import javax.servlet.ServletException;

import com.ibm.designer.runtime.domino.adapter.ComponentModule;
import com.ibm.designer.runtime.domino.adapter.IServletFactory;
import com.ibm.designer.runtime.domino.adapter.ServletMatch;

public class MinimizerServletFactory implements IServletFactory {

    private static final String SERVLET_WIDGET_CLASS = "ch.hasselba.jsf.servlet.MinimizerServlet";
    private static final String SERVLET_WIDGET_NAME = "JS Minimizer";

    private ComponentModule module;

    public void init(ComponentModule module) {
        this.module = module;
    }

    public ServletMatch getServletMatch(String contextPath, String path)
            throws ServletException {

        String servletPath = "";

        if (path.contains("/minimizer")) {
            String pathInfo = path;
            return new ServletMatch(getWidgetServlet(), servletPath, pathInfo);
        }

        return null;
    }

    public Servlet getWidgetServlet() throws ServletException {
        return module.createServlet(SERVLET_WIDGET_CLASS, SERVLET_WIDGET_NAME, null);
    }

}

The servlet is now reachable by the following URL

http://example.com/path/to/db.nsf/xsp/minimizer/

How does the minimizer servlet work? It appends all required files into a single string and compresses the string before sending the result to the browser. So the first information needed is which files should be used. This can be done with the parameter „files„, the list of files is concatenated with „+„:

http://example.com/path/to/db.nsf/xsp/minimizer/?files=file1.js+file2.js+ ...

The given files are then loaded via NAPI into a large StringBuffer and then compressed and mimimized by YUI and GZIP.

package ch.hasselba.jsf.servlet;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.Serializable;
import java.io.StringWriter;
import java.io.Writer;
import java.util.zip.GZIPOutputStream;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.mozilla.javascript.ErrorReporter;
import org.mozilla.javascript.EvaluatorException;

import ch.hasselba.napi.NAPIUtils;

import com.ibm.xsp.webapp.DesignerFacesServlet;
import com.yahoo.platform.yui.compressor.JavaScriptCompressor;

public class MinimizerServlet extends DesignerFacesServlet implements
        Serializable {

    private static final long serialVersionUID = -1L;

    @Override
    public void service(ServletRequest servletRequest,
            ServletResponse servletResponse) throws ServletException,
            IOException {

        HttpServletRequest req = (HttpServletRequest) servletRequest;
        HttpServletResponse res = (HttpServletResponse) servletResponse;
        ServletOutputStream out = servletResponse.getOutputStream();

        try {
            res.setContentType("application/x-javascript");
            res.setCharacterEncoding("utf-8");
            res.addHeader("Content-Encoding", "gzip");

            // load the js requested files
            StringBuffer fileData = new StringBuffer();
            String tmpFile = "";
            String paramFiles = req.getParameter("files");
            String[] files = paramFiles.split(" ");

            // and add them to a buffer
            for (String file : files) {

                try {
                    tmpFile = NAPIUtils.loadFile("DEV01", "NAPI.nsf", file);
                    fileData.append(tmpFile);
                } catch (Exception e) {
                    // ignore errors
                    e.printStackTrace();
                }

            }

            // Compress the JS Code with compressor
            StringWriter sWriter = new StringWriter();
            compress(stringBufferToInputStreamReader(fileData), sWriter);

            // and GZIP it
            ByteArrayOutputStream obj = new ByteArrayOutputStream();
            GZIPOutputStream gzip = new GZIPOutputStream(obj);
            gzip.write(sWriter.toString().getBytes("UTF-8"));
            gzip.close();

            // send it to the client
            out.write(obj.toByteArray());

        } catch (Exception e) {
            e.printStackTrace(new PrintStream(out));
        } finally {
            out.close();
        }
    }

    /**
     * Helper to convert a StringBuffer to an InputStreamReader
     * 
     * @param strBuffer
     *            the StringBuffer to convert
     * @return the converted InputStreamReader
     */
    public static InputStreamReader stringBufferToInputStreamReader(
            final StringBuffer strBuffer) {
        return new InputStreamReader(new ByteArrayInputStream(strBuffer
                .toString().getBytes()));
    }

    /**
     * compresses the JS code using YUI
     * 
     * @param in
     *            the InputStreamReader containing the JS
     * @param out
     *            the Writer Object
     * @throws EvaluatorException
     * @throws IOException
     */
    public static void compress(final InputStreamReader in, Writer out)
            throws EvaluatorException, IOException {
        JavaScriptCompressor compressor = new JavaScriptCompressor(in,
                new ErrorReporter() {
                    public void warning(String message, String sourceName,
                            int line, String lineSource, int lineOffset) {

                        System.out.println("\n[WARNING]");
                        if (line < 0) {
                            System.out.println(" " + message);
                        } else {
                            System.out.println(" " + line + ':' + lineOffset
                                    + ':' + message);
                        }
                    }

                    public void error(String message, String sourceName,
                            int line, String lineSource, int lineOffset) {
                        System.out.println("[ERROR] ");
                        if (line < 0) {
                            System.out.println(" " + message);
                        } else {
                            System.out.println(" " + line + ':' + lineOffset
                                    + ':' + message);
                        }
                    }

                    public EvaluatorException runtimeError(String message,
                            String sourceName, int line, String lineSource,
                            int lineOffset) {
                        error(message, sourceName, line, lineSource, lineOffset);
                        return new EvaluatorException(message);
                    }
                });

        // call YUI
        compressor.compress(out, 0, true, false, false, false);
    }
}

For testing purposes I imported an uncompressed version of jQuery and created a file named helloWorld.js. The helloWorld.js contains a single function only, just to test and verify the outcome of the servlet.

Then, I created a test.html, a simple HTML page which loads the mimimized JavaScript files:

<html>
 <body>
   <h1>Test</h1>
   <script src="./xsp/minimizer/?files=jquery-1.11.1.js+helloWorld.js">
   </script>
 </body>
</html>

This is how the WebContent folder looks like in package explorer:

When opening the test.html page, you can see that only a single request is made to load the data from the servlet:

The size of the response is only 30 % of the original jQuery source request (which is GZIPed too):

When testing the code in the Firebug console, everything is working as expected. The „HelloWorld“ function shows an alert…

and jQuery is defined:

The performance on my test server is very good, without code optimization or caching of the generated JavaScript elements.

Veröffentlicht unter Java, Java Script, Web, XPages | Verschlagwortet mit , , , , , | 6 Kommentare

XPages: WebContent Files (2) – Manipulate exitsting files using the Java NAPI

In this article, I will shortly give an overview how you can edit existing file from the WebContent folder (Don’t miss the first article on this topic).

First, let’s create a view to display the design elements of the WebContent folder. To do this, I have an old school LotusScript Agent which updates the selection formula of a view (Some details about this technique can be found here).

Sub Initialize

    Dim session As New NotesSession
    Dim doc As NotesDocument
    Dim db As NotesDatabase
    Dim view As NotesView
      
    Set db = session.Currentdatabase
    Set view = db.Getview("DesignView")
    
    view.SelectionFormula = |@Contains($FlagsExt; "w")|
    
    Set doc = db.GetDocumentByUNID(view.UniversalID)
    Delete view
    
    doc.ReplaceItemValue "$FormulaClass", "7FFF"
    doc.Sign
    doc.Save True, False

End Sub

The agent has to run once to change the view’s selection criteria. In this example the view has the name „DesignView“. After that, we can add a single column to the view to display the files and their names:

Now lets build a simple XPage named Files.xsp to select the file you want to edit:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

    <xp:viewPanel
        rows="30"
        id="viewPanel1"
        var="rowEntry">
        <xp:this.facets>
            <xp:pager
                partialRefresh="true"
                layout="Previous Group Next"
                xp:key="headerPager"
                id="pager1">
            </xp:pager>
        </xp:this.facets>
        <xp:this.data>
            <xp:dominoView
                var="viewDesign"
                viewName="DesignView">
            </xp:dominoView>
        </xp:this.data>
        <xp:viewColumn
            columnName="$FileNames"
            id="viewColumnFileNames"
            displayAs="link"
            >
            <xp:this.pageUrl>
                <![CDATA[#{javascript:"/Editor.xsp?filename=" +
                   rowEntry.getColumnValues().get(0)}]]>
        </xp:this.pageUrl>
        </xp:viewColumn>
    </xp:viewPanel>
    <xp:br />

</xp:view>

The Files.xsp lists all files as links and opens them via the Editor.xsp XPage:

This is the Editor.xsp:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

    FileName:
    <xp:inputText
        id="inputTextFileName"
        value="#{fileBean.fileName}"
        defaultValue="#{param.filename}"
        disabled="true" />
    <xp:br />
    FileData:

    <xp:inputTextarea
        id="inputTextarea1"
        value="#{fileBean.fileData}"
        rows="40"
        cols="80" />
    <xp:br />
    <xp:button
        value="Load"
        id="buttonLoad">
        <xp:eventHandler
            event="onclick"
            submit="true"
            refreshMode="complete"
            action="#{fileBean.loadData}">
        </xp:eventHandler>
    </xp:button>
    <xp:button
        value="Save"
        id="buttonSave">
        <xp:eventHandler
            event="onclick"
            submit="true"
            refreshMode="complete"
            action="#{fileBean.saveData}">
        </xp:eventHandler>
    </xp:button>
    
</xp:view>

It uses a simple managed bean…

package ch.hasselba.napi;

import java.io.Serializable;

public class FileDataBean implements Serializable {

    private static final long serialVersionUID = 1L;
    private String fileName;
    private String fileData;
    private String dbPath;
    private String dbServer;

    public String getDbPath() {
        return dbPath;
    }

    public void setDbPath(String dbPath) {
        this.dbPath = dbPath;
    }

    public String getDbServer() {
        return dbServer;
    }

    public void setDbServer(String dbServer) {
        this.dbServer = dbServer;
    }

    public void setFileData(String fileData) {
        this.fileData = fileData;
    }

    public String getFileData() {
        return fileData;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public String getFileName() {
        return fileName;
    }

    public void loadData() {
        this.fileData = NAPIUtils.loadFile(this.dbServer, this.dbPath, this.fileName);
    }

    public void saveData() {
        NAPIUtils.saveFile(this.dbServer, this.dbPath, this.fileName, this.fileData);
    }
}

… which is initialized with the properties defined in the faces-config.xml. There you can find the database server and the database path:

<?xml version="1.0" encoding="UTF-8"?>
<faces-config>
  <managed-bean>
    <managed-bean-name>fileBean</managed-bean-name>
    <managed-bean-class>ch.hasselba.napi.FileDataBean</managed-bean-class>
    <managed-bean-scope>view</managed-bean-scope>
    <managed-property>
      <property-name>dbPath</property-name>
      <value>NAPI.nsf</value>
    </managed-property>
    <managed-property>
      <property-name>dbServer</property-name>
      <value>DEV01/Hasselba/CH</value>
    </managed-property>
  </managed-bean>
</faces-config>

And last but not least, the required NAPIUtil class:

package ch.hasselba.napi;

import java.io.InputStream;
import com.ibm.designer.domino.napi.NotesAPIException;
import com.ibm.designer.domino.napi.NotesDatabase;
import com.ibm.designer.domino.napi.NotesNote;
import com.ibm.designer.domino.napi.NotesObject;
import com.ibm.designer.domino.napi.NotesSession;
import com.ibm.designer.domino.napi.design.FileAccess;

public class NAPIUtils {

    /**
     * loads a given WebContent file and returns the result as String
     * 
     * @param serverName
     *            the server to use
     * @param dbPath
     *            the database path
     * @param fileName
     *            the file to load
     * @return the file data as String
     */
    static public String loadFile(final String serverName, final String dbPath,
            final String fileName) {

        NotesSession nSession = null;
        NotesDatabase nDatabase = null;
        NotesNote nNote = null;

        try {
            nSession = new NotesSession();

            // open database
            nDatabase = nSession.getDatabaseByPath(serverName + "!!" + dbPath);
            nDatabase.open();

            // load existing data
            nNote = FileAccess.getFileByPath(nDatabase, fileName);

            // get Filedate and return String
            InputStream is = FileAccess.readFileContentAsInputStream(nNote);

            return convertStreamToString(is);
        } catch (NotesAPIException e) {
            e.printStackTrace();
        } finally {
            // recycle NAPI objects
            recycleNAPIObject(nNote, nDatabase, nSession);
        }

        return fileName;
    }

    /**
     * loads a given WebContent file and returns the result as String
     * 
     * @param serverName
     *            the server to use
     * @param dbPath
     *            the database path
     * @param fileName
     *            the file to load
     * @param fileData
     *            the data of the file
     */
    static public void saveFile(final String serverName, final String dbPath,
            final String fileName, final String fileData) {

        NotesSession nSession = null;
        NotesDatabase nDatabase = null;
        NotesNote nNote = null;

        try {
            nSession = new NotesSession();

            // open database
            nDatabase = nSession.getDatabaseByPath(serverName + "!!" + dbPath);
            nDatabase.open();

            // load existing data
            nNote = FileAccess.getFileByPath(nDatabase, fileName);

            // store them to note
            FileAccess.saveData(nNote, fileName, fileData.getBytes());

        } catch (NotesAPIException e) {
            e.printStackTrace();
        } finally {
            // recycle NAPI objects
            recycleNAPIObject(nNote, nDatabase, nSession);
        }
    }

    /**
     * converts an input stream to a string
     * 
     * @param is
     *            the input stream to convert
     * @return String
     */
    static String convertStreamToString(java.io.InputStream is) {
        java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A");
        return s.hasNext() ? s.next() : "";
    }

    /**
     * recycleNAPIObject helper method for recycling NAPI objects
     * 
     * @param nObjects
     *            the NAPI objects to recycle
     */
    static void recycleNAPIObject(NotesObject... nObjects) {
        for (NotesObject nObject : nObjects) {
            if (nObject != null) {
                try {
                    (nObject).recycle();
                } catch (NotesAPIException ne) {
                }
            }
        }
    }
}

If the class looks like this…

…just hover one  of the entries and select „Fix project setup“.

Then, you can choose the missed bundle:

Build the project, and open one of the files by clicking a link in the Files.xsp. Here is an example for the file WEB-INF/faces-config.xml:

Now you can click the „Load“ button to read the content of the file.

You can edit the file now and save it to the NSF.

If you got to package explorer (Hit F9 for refresh) you can see the changes:

Veröffentlicht unter Java, Server, XPages | Verschlagwortet mit , , , , , | 3 Kommentare

XPages: WebContent Files (1) – Create a file using the Java NAPI

The great Marky Roden has written an interesting article about using the WebContent folder instead of standard domino design elements. To create or manipulate these files programmatically, you can use the Java NAPI.

The first example demonstrates the creation of a file using a Java Agent. Before you can compile the code, you have to import the required jars as described here.

import lotus.domino.AgentBase;

import com.ibm.designer.domino.napi.NotesConstants;
import com.ibm.designer.domino.napi.NotesDatabase;
import com.ibm.designer.domino.napi.NotesNote;
import com.ibm.designer.domino.napi.NotesSession;
import com.ibm.designer.domino.napi.design.FileAccess;
import com.ibm.jvm.util.ByteArrayOutputStream;

public class JavaAgent extends AgentBase {

    public void NotesMain() {

        try {
            final String serverName = "Dev01/Hasselba/CH";
            final String dbPath = "NAPI.nsf";
            final String fileName = "dummy.txt";

            NotesSession nSession = new NotesSession();
            NotesDatabase nDatabase = nSession.getDatabaseByPath(serverName
                    + "!!" + dbPath);
            nDatabase.open();

            // create a new note
            NotesNote nNote = nDatabase.createNote();

            // define $Flags item
            StringBuffer flags = new StringBuffer();
            flags.append(NotesConstants.DESIGN_FLAG_HIDEFROMDESIGNLIST);
            flags.append(NotesConstants.DESIGN_FLAG_NO_COMPOSE);
            flags.append(NotesConstants.DESIGN_FLAG_HIDE_FROM_V4);
            flags.append(NotesConstants.DESIGN_FLAG_FILE);
             
            // define $FlagsExt item
            StringBuffer extFlags = new StringBuffer();
            extFlags.append(NotesConstants.DESIGN_FLAGEXT_WEBCONTENTFILE);

            // init the file with the flags
            nNote.initAsFile(flags.toString(), extFlags.toString());

            // add required fields
            nNote.setItemText("$TITLE", fileName);
            nNote.setItemText("$MimeType", "text/plain");

           // generate some random data
           ByteArrayOutputStream bos = new ByteArrayOutputStream();
           for (int i = 0; i < 128000; i++) {
             int hlp = (i % 24) + 60;
             bos.write(hlp);
           }

           // store the data to the Note
           FileAccess.saveData(nNote, fileName, bos.toByteArray());

           // recycle the NAPI objects
           nNote.recycle();
           nDatabase.recycle();
           nSession.recycle();

       } catch (Exception e) {
           e.printStackTrace();
       }
    }
}

As you can see, when using the NAPI you don’t have to save a note. It is directly written to the NSF.

After running the agent and refreshing the project explorer view (hit F9) the newly created file appears in the WebContent folder…

… and is accessible in the browser:

If you want to allow public access to your element, you have to add a single line in the code above:

nNote.setItemText("$PublicAccess", "1");

This allows unauthenticated users to access the file while the rest of the application is protected:

On the other hand, you can use reader fields enhance the security of the file. But this can not be done with the note object directly, you have to use the standard Domino document classes for this. To access the document, you have to convert the note’s note id (an integer value) into his hexadecimal representation:

int noteId = nNote.getNoteId();
Document doc = db.getDocumentByID(Integer.toHexString(noteId));

Then, you can add the reader field to the document and save the result.

Session session = getAgentSession();
Database db = session.getCurrentDatabase();
Document doc = db.getDocumentByID(Integer.toHexString(noteId));

// create the field 
Item item = doc.replaceItemValue("DocReaders", "[ReadAll]");
item.setReaders(true);

// save the document
doc.save();

// recycle the instances
item.recycle();
doc.recycle();
db.recycle();
session.recycle();

Now, the file is protected when opening in the browser:

Keep in mind: As the developer it is required to have the access rights for the file too.

Veröffentlicht unter Agenten, Java, XPages | Verschlagwortet mit , , , , , | 9 Kommentare

MongoDB for Java Developers

Today I received the confirmation for successfully completing the „MongoDB for Java Developers“ course:

Here is the link to verify the certificate.

Veröffentlicht unter MongoDB | Verschlagwortet mit | Schreib einen Kommentar

XPages: Execute Events with HTTP Get

To execute an event on the server, you normally have to send a POST request, because actions will be executed in the Invoke Application phase of the JSF lifecycle. A GET request will only process the Restore View and the Render Response phase, that why you can not execute an event with a GET request.

But with the help of a PhaseListener, the execution can be done earlier in the Restore View phase:

package ch.hasselba.xpages.util;

import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import com.ibm.xsp.component.xp.XspEventHandler;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import com.ibm.xsp.util.FacesUtil;

public class ExecuteOnServerPhaseListener implements PhaseListener {

    private static final long serialVersionUID = 1L;

    public void beforePhase(PhaseEvent event) {}

    public PhaseId getPhaseId() {
        return PhaseId.RESTORE_VIEW;
    }

    public void afterPhase(PhaseEvent event) {
        
        FacesContextExImpl = FacesContextExImpl.getCurrentInstance();
        ExternalContext ec = fc.getExternalContext();
        String url = ec.getRequestPathInfo();
        String[] pathes = url.split( "/" );
        
        try{
            if( pathes.length > 2 ){
                if( "executeOnServer".equals( pathes[pathes.length -2 ] ) ){
                    String[] fullId = pathes[ pathes.length - 1 ].split(":");
                    String actionId = fullId[ fullId.length - 1 ];
                    XspEventHandler eventHandler = (XspEventHandler)
                        FacesUtil.findComponentWithFullId( fc.getViewRoot(), actionId );
                    if( eventHandler != null ){
                        eventHandler.getAction().invoke( fc, null );
                        fc.responseComplete();
                    }
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
        
    }

}

To activate the PhaseListener, it has to be enabled in the faces-config.xml:

<?xml version="1.0" encoding="UTF-8"?>
<faces-config>
   <lifecycle>
      <phase-listener>ch.hasselba.xpages.util.ExecuteOnServerPhaseListener</phase-listener>
   </lifecycle>
</faces-config>

The following Javascript snippet extends the XSP object and adds the new function executeOnServerGet to it. The parameter is the if of the event to invoke.

XSP.executeOnServerGet = function( eventId ){
    var viewId = dojo.query('[name="$$viewid"]')[0].value;
    var url = document.forms[0].action;
    url += "/executeOnServer/" + eventId;
    url += "?$$viewid=" + viewId;
    url += "&$$ajaxid=@none";
    dojo.xhrGet({url: url});
}

When calling the function, it sends a GET request and adds the current view id to the request. With the parameter $$ajaxId set to @none, the XPages Engine is forced to send no HTML code back to the client.

And here is an example XPage:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

    <xp:eventHandler id="helloworld" event="helloworld" submit="false">
        <xp:this.action>
            <![CDATA[#{javascript:
               print("hello world " + java.lang.System.currentTimeMillis() );
            }]]>
        </xp:this.action>
    </xp:eventHandler>


    <xp:scriptBlock id="scriptBlock1">
        <xp:this.value><![CDATA[
            dojo.addOnLoad( function(){
                XSP.executeOnServerGet = function( eventId ){
                      var viewId = dojo.query('[name="$$viewid"]')[0].value;
                    var url = document.forms[0].action;
                    url += "/executeOnServer/" + eventId;
                    url += "?$$viewid=" + viewId;
                    url += "&$$ajaxid=@none";
                    dojo.xhrGet({url: url});
                  }
            });
        ]]></xp:this.value>
    </xp:scriptBlock>
    
    <xp:button value="Execute" id="button1">
        <xp:eventHandler event="onclick" submit="false">
            <xp:this.script>
                <![CDATA[XSP.executeOnServerGet( "#{id:helloworld}" );]]>
            </xp:this.script>
        </xp:eventHandler>
    </xp:button>
</xp:view>

When clicking the button, the following URL is opened in the background:

http://example.com/db.nsf/EventGet.xsp/executeOnServer/view:_id1:helloworld?$$viewid=!dwjldz64w0!&$$ajaxid=@none

A GET request was sent to the server:If you look on the server console, you will see that the Action was invoked:

Veröffentlicht unter Dojo Toolkit, Java, Java Script, JSF, Performance, XPages, XSP | Verschlagwortet mit , , , , , , , , , , | 2 Kommentare

Yii Framework: Security Fix for Version 1.1.14

A security fix for the Yii framework was released on 29th June: http://www.yiiframework.com/news/78/yii-1-1-15-is-released-security-fix/

The issue only affects the CDetailView component of Version 1.1.14, and can be upgraded safely without breaking existing code.

Veröffentlicht unter Bug, PHP, Yii | Verschlagwortet mit , , , | Schreib einen Kommentar

Tschüß RFC 2616! War nett mit Dir!

Wie auf Heise berichtet, ist das RFC 2616 zu seinem 15ten Geburtstag in Rente geschickt worden, und sollte keine Verwendung mehr finden. Bei Fragen rund um die HTTP/1.1 Spezifikation gelten von nun an folgende RFCs:

In jedem der RFCs findet sich ein Abschnitt „Changes from RFC 2616„, indem die grundlegensten Änderungen gegenüber der ursprünglichen Spezifikation erläutert werden.

Veröffentlicht unter Web | Verschlagwortet mit | Schreib einen Kommentar

XPages & Angular.js: AngScope for Firebug

AngScope is a

„Simple Firebug extension that allows you to inspect the AngularJS scope that a DOM element binds to.“

Just do a right click on the DOM element you want to inspect and select „Inspect Angular Scope„:

This gives you a direct access to all elements of the scopes of your Angular.js application:

You can find the extension here.

Veröffentlicht unter Angular.js | Verschlagwortet mit , , , | Ein Kommentar

The Voices Told Me To Do It!

1. „Create a new com.ibm.xsp.context.FacesContextExImpl!“

2. „Add all required classed to the build path!“

3. „Add a useless message to the constructor!“

4. „BUILD IT!“

5. „Open the original Jar!“

6. „In WinRAR!“

7. „Overwrite the existing classes!“

8. „Quick! Start the server!“

9. „And open a XPage!“

10. „Look! It is fulfilled!“

Veröffentlicht unter Java, XPages | Verschlagwortet mit , , , , | 7 Kommentare

XPages & Angular.js: Fileuploads

When using Angular.js you sooner or later want to upload a file to your Domino server. But to do this, you not only need some nice looking frontend, you also need some code in the backend. For the frontend you can use one of the already exsiting modules which are available for Angular.js, for example the angular-file-upload. For a quick start, I have choosen to modify the provided Simple example.

After stripping down the example files (just saved the whole website with Firefox), you can import all the files into a new database. Mark Roden has written a very good tutorial about the details for this step.

The WebContent folder should look like this:

Now you have to modify the index.html and make all pathes relative:

When you open the index.html in the browser you will see some errors in the console, because of some missing fonts, but we will ignore this issue in this quick example.

Instead, we are adding a link for a download of an uploaded file:

&#160;<a download="{{ item.file.name }}" ng-href="{{ item.file.dlPath }}">download</a></td>

The download link will be filled with the URL of the uploaded attachment in the target database after a successfull upload.

Now it’s time to tell the frontend where to store the file in the backend. For this, you have to modify the controller.js and define the target of the upload process. In this example, the target is a XPage named „upload.xsp„. After uploading a file to the server, the XPage returns the URL of the attachment as a JSON string. To update the link in the frontend, we bind a function to the event „success“ which adds the URL to the current file item:

angular.module('app', ['angularFileUpload'])

.controller('TestController', function ($scope, $fileUploader) {
    'use strict';

    // create a uploader with options
    var uploader = $scope.uploader = $fileUploader.create({
        scope: $scope,   
        url: 'upload.xsp'
    });

    uploader.bind('success', function (event, xhr, item, response) {
        // add the response url to the file item 
        item.file.dlPath = response.url;
    });

});

 [This is the complete controler.js which replaces the file from the module’s example.]

The XPage handles the uploaded files them directly without a file upload control. Every uploaded file will be attached to a single document, and embedded to the Richtext item „Body„. If the upload was successfull, the XPages returns the JSON data containing the URL to the newly created attachment.

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core" rendered="false">
    <xp:this.data>
        <xp:dominoDocument var="documentFile" action="editDocument" concurrencyMode="force" />
    </xp:this.data>

    <xp:this.afterRenderResponse>
        <![CDATA[#{javascript:

            /**
             * saves a uploaded attachment to a datasource
             *
             * @param ds
             *        the datasource
             * @param rtItemName
             *        the richtext item to save the file
             * @return
             *        the name of the uploaded file
             * @author Sven Hasselbach
             */
            function saveAttachment( ds:NotesXspDocument, rtItemName:String ):String {

                // get the file data
                var con = facesContext.getExternalContext();
                var request:com.sun.faces.context.MyHttpServletRequestWrapper = con.getRequest();
                var map:java.util.Map = request.getParameterMap();
                var fileData:com.ibm.xsp.http.UploadedFile = map.get( "file" );

                if( fileData == null )
                      return;

                // get the file
                var tempFile:java.io.File = fileData.getServerFile();
                var correctedFile = new java.io.File( tempFile.getParentFile().getAbsolutePath() +
                java.io.File.separator + fileData.getClientFileName() );
                var success = tempFile.renameTo(correctedFile);

                // create or use an existing RT item
                var rtFiles:NotesRichTextItem = null;
                if(!(ds.getDocument().hasItem( rtItemName ))){
                    rtFiles = ds.getDocument().createRichTextItem( rtItemName )
                }else{
                    rtFiles = ds.getDocument().getFirstItem( rtItemName );
                }

                // embed the file
                rtFiles.embedObject(lotus.domino.local.EmbeddedObject.EMBED_ATTACHMENT, "",
                      correctedFile.getAbsolutePath(), null);

                  // rename the file back for server processing
                  correctedFile.renameTo(tempFile);

                  // save the datasource
                ds.save();

                // return the filenam
                return fileData.getClientFileName();

            }

            // save the doc
            var file = saveAttachment( documentFile, "Body" );

            // create the response
            var res = facesContext.getExternalContext().getResponse();
            res.setContentType( "application/json" );
            res.setCharacterEncoding( "UTF-8" );
            var writer = res.getWriter();

            if( file != null ){
                // send a JSON url string back to the client
                var url = documentFile.getDocument().getHttpURL();
                var fileUrl = url.replace("?OpenDocument","/$File/"+file+"?OpenElement");
                writer.write( '{"url": "' + fileUrl + '"}' );    
            }else{
                // otherwise send empty JSON data
                writer.write( '{}' );
            }
            writer.flush();
            facesContext.responseComplete();
        }]]>
    </xp:this.afterRenderResponse>
</xp:view>

After uploading a file, you can see that the file was uploaded successfully in the firebug console:

When clicking the „download“ link, the file is available to download:

And the attachment was uploaded to the database:

That’s it!

Veröffentlicht unter Angular.js | Verschlagwortet mit , , , , , , | 2 Kommentare