Quark Engine and JT400 - Part 2

As announced in the previous part, here we will show a skeleton code exposing IBM i IFS file system to the browser JavaScript allowing you to easily create web based IFS explorer.

Last time we learned how to handle the most important JT400 library class, As400.class. From now on it should be easy :)

Let's create our IFSController skeleton first.

@ExtJSDirect(paths = { "/ws", "/api" })
@ExtJSAction(namespace = "io.greenscreens", action = "IFS")
public class IFSController {

	private static final Logger LOG = LoggerFactory.getLogger(IFSController.class);

	@Inject 
	AS400 as400;
	
	@PostConstruct
	public void init() {
		if (!as400.isUsePasswordCache()) {
			throw new RuntimeException("User not verified!");
		}
	}
	
	@ExtJSMethod("list")
	public ExtJSResponseList<Object> list(@Required final String path) {
		
		final ExtJSResponseList.Builder<Object> resp = ExtJSResponseList.Builder.create(Object.class);
		
		return resp.build();
	}
	
	@ExtJSMethod("rename")
	public ExtJSResponse rename(final String path, final String name) {		

		return new ExtJSResponse(true, null);
	}

	@ExtJSMethod("remove")
	public ExtJSResponse remove(final String path) {		

		return new ExtJSResponse(true, null);
	}

	@ExtJSMethod("move")
	public ExtJSResponse move(final String from, final String to) {		

		return new ExtJSResponse(true, null);
	}
	
	@ExtJSMethod("copy")
	public ExtJSResponse copy(final String from, final String to) {		

		return new ExtJSResponse(true, null);
	}
}

Check out @PostConstruct annotated method first. Here we should use something more robust. This is just a dirty trick we are using to signal that session level AS400 class instance are properly authenticated. Look into AS400Controller class when that value is set and how. Here, if value is not set, throw an exception preventing any further calls on this controller if the user is not properly authenticated.

Check out @ExtJSMethod annotation and their names at the rest of the methods. Names which will be used in browser JavaScript. Names are self-descriptive enough, so no further explanation is needed what those methods will do.

In the rest of this blog post we will show list method implementation. Remaining methods will be left empty for you to practice :). As this is basic example, we will not implement advanced stuff as caching etc. Just enough to get you started.

The IFS class itself as any other JT400 classes should not be exposed to the web side, and neither will they work as expected, so we need to create classic Java POJO classes representing exposed data. In this case we will create a WebFile class with basic info.

public class WebFile {

   public enum TYPE {FILE, FOLDER}

     String name;
     String path;
     long created;
     TYPE type;
}

Notice method list inside IFSController class. We defined return type as ExtJSResponseList<T>, response is a list of some type. In skeleton we defined Object as a default, but now we can replace it with our data holder class to make it look like this...

@ExtJSMethod("list")
public ExtJSResponseList<WebFile> login(@Required final String path) {
		
	final ExtJSResponseList.Builder<WebFile> resp = ExtJSResponseList.Builder.create(WebFile.class);


	return resp.build();
}

Now, all we must do is to add some "meat". A code that will pick up the data from IBM i IFS file system with help of JT400 library. For every file or folder in a given target we will create a mapping WebFile POJO instance which will be exposed to the browser.

@ExtJSMethod("list")
public ExtJSResponseList<WebFile> login(@Required final String path) {
		
	final ExtJSResponseList.Builder<WebFile> resp = ExtJSResponseList.Builder.create(WebFile.class);

	try {			
			
		final IFSFile root = new IFSFile(as400, path);
		final IFSFile [] list = root.listFiles(path);
			
		final Collection<WebFile> data = new ArrayList<WebFile>();
		for (IFSFile file : list) {
			WebFile webFile = new WebFile();
			webFile.setCreated(file.created());
			webFile.setName(file.getName());
			webFile.setPath(file.getAbsolutePath());
			webFile.setType(file.isDirectory() ? TYPE.FOLDER : TYPE.FILE);
            data.add(webFile);
		}

		resp.setData(data);
		resp.setStatus(true);

	} catch (Exception e) {
		resp.setMessage(e.getMessage());
		LOG.error(e.getMessage());
		LOG.debug(e.getMessage(), e);
	}
		
	return resp.build();
}

Now, our web app is ready. Start it, load the demo page and try following code from the web browser. You should get a nice list of files and folders for a given path target.

// Initialize Quark engine
await Engine.init({api: '/demo/ws', service:'/demo/ws'});

// Call JT400 AS400 class instance method to connect to the IBM i system
await io.greenscreens.AS400.login('as400.acme.com', 'QSECOFR', 'QSECOFR');

// Get list of files and directories from IBM i IFS file system
let files = await io.greenscreens.IFS.list('/');

console.log(files);

If you really investigated the code, then probably you might ask a question about one more missing part here... file upload and download.

As Quark Engine is an engine helping you to create web API's for server-side logic, it is not possible to create file uploads and downloads this way. For that, we need standard Java Servlets. Here, we will show you how to create file download, and will leave it up to you to create upload so you can practice a little bit :).

We can do this in 2 ways. To use one servlet for download and one for upload or a single servlet for both operations. This solely depends on how you want to separate URL links. Only what is important is to use different web methods for download and upload if using the same Java Servlet.

As our AS400 class instance is a session based and stored under As400 full class path name, we can easily reach it from Java Servlet and use it to control / verify if the user is signed-on. Let's see the skeleton first.

@WebServlet("/download")
public class IFSDownloadServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    public IFSDownloadServlet() {
        super();
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        final HttpSession session = request.getSession();
        final AS400 as4oo = (AS400) session.getAttribute(AS400.class.getCanonicalName());
				
        if (as4oo == null || !as4oo.isUsePasswordCache()) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "User not authenticated");
            return;
        }
		
        String path = request.getParameter("p");
        if (path == null) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Requested path invalid");
            return;
        }
		
        try {
            path = new String(Base64.getDecoder().decode(path), "UTF-8");
            final IFSFile file = new IFSFile(as4oo, path);
			
            if (!file.exists()) {
                throw new Exception("File does not exists");
            }
			
            doDownload(response, as4oo, file);
        } catch (Exception e) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
        }
    }

    void doDownload(final HttpServletResponse response, final AS400 as4oo, final IFSFile file ) throws Exception {

		
    }
}

Skeleton prepares Java Servlet with very basic checking. What remains is to add logic into the doDownload method which will read data from IBM i IFS file system and forward it to the browser.

void doDownload(final HttpServletResponse resp, final AS400 as4oo, final IFSFile file ) throws Exception {
		
    resp.setContentType("application/octet-stream");
    resp.setHeader("Content-Disposition", "filename=\""+file.getName()+"\"");
		
    final IFSFileInputStream fis = new IFSFileInputStream(file);
    try {
    	copyStream(fis, resp.getOutputStream());
    } finally {
    	close(fis);			
    }
		
}

void copyStream(final InputStream is, final OutputStream os) throws Exception {

    final byte[] buffer = new byte[1024];
    while(is.read(buffer) > -1) {
         os.write(buffer);   
    }
    os.flush();
}
	
void close(final Closeable obj) {

    if (obj == null) return;
		
    try {
        obj.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Note: Use Base64 encoding to properly send real path name from browser to the server.

That's it. Now use the Quark Engine generated API to get the list of files and folders from IFS and use one of  received paths to download file through the download servlet.

let path = '/temp/demo.txt';
window.open('http://localhost:8080/demo/download?p=' + btoa(path));

Please note, that this is a basic example and does not contain best-practices for handling large files or multiple user requests. For that, we would need much more text to show how to use request queueing, file caching etc. That is not the intention of this article.

Full code can be found on our Github repository here (demo2 package).