/* * Copyright 2009 Rodrigo Reyes reyes.rr at gmail dot com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package net.kornr.swit.button; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.Map; import java.util.Map.Entry; import javax.imageio.ImageIO; import net.kornr.swit.util.LRUMap; import org.apache.wicket.Resource; import org.apache.wicket.ResourceReference; import org.apache.wicket.markup.html.WebResource; import org.apache.wicket.markup.html.form.ImageButton; import org.apache.wicket.protocol.http.WebApplication; import org.apache.wicket.protocol.http.WebResponse; import org.apache.wicket.util.resource.FileResourceStream; import org.apache.wicket.util.resource.IResourceStream; import org.apache.wicket.util.value.ValueMap; /** * WebResource that manages the button creation requests. It is responsible for the button cache * and delegates the creation to the ButtonTemplate object registered. * <p> * To optimize the performance of the button generation, this object uses 2 cache. The first cache is the normal cache, * used for buttons that are statically defined in your application. This cache provides optimal performances, as the button * image is only generated once, the first time it's requested, but it stays forever in memory. Another cache, the temporary cache * is used to store button objects that are likely to change during the lifetime of your application, either because the text * is dynamically modified (for instance because it's typed by the user, or because it's time-related), or because the button template * itself is changing. * <p> * Note that both cache are implemented using an LRU cache with limited slots. This is to prevent excessive memory consumption, even for the normal cache. * * When an object is not anymore in the cache, it is generated again. You should take care of defining a "normal cache" size that always exceeds the number of static * pairs [template, text] that your application uses. * <p> * If you're not sure, don't modify the cache sizes: the default normal cache size should be enough for most web applications, and you probably won't need * to use the temporary cache. * <p> * Both caches store the button generated on the filesystem, in the directory specified in "javax.servlet.context.tempdir". You can force the generated files * clean up by calling the static method cleanUpFiles(). */ public class ButtonResource extends WebResource { static private long s_counter = System.currentTimeMillis(); static String s_buttonNameId = "buttons"; static private File s_tempDir = null; static class LRUFileMap extends LRUMap<Long, File> { public LRUFileMap(int maxsize) { super(maxsize); } @Override protected void onRemove(Entry<Long, File> entry) { entry.getValue().delete(); } } static private Map<Long, ButtonResourceKey> s_cache = new LRUMap<Long,ButtonResourceKey>(1000); static private Map<ButtonResourceKey, Long> s_keyCache = new LRUMap<ButtonResourceKey, Long>(1000); static private LRUFileMap s_fileCache = new LRUFileMap(5000); static private Map<Long, ButtonResourceKey> s_tempCache = new LRUMap<Long,ButtonResourceKey>(1000); static private Map<ButtonResourceKey, Long> s_tempKeyCache = new LRUMap<ButtonResourceKey, Long>(1000); static private LRUFileMap s_tempFileCache = new LRUFileMap(1000); public ButtonResource() { } synchronized static private Long getUniqueId(ButtonResourceKey key) { Long keyid = s_keyCache.get(key); if (keyid == null) { keyid = s_counter++; key.setId(keyid); s_keyCache.put(key, keyid); s_cache.put(keyid, key); } return keyid; } /** * Creates a Wicket Image object that is bound to the given button generator and text. * This method registers the template and the text in the normal cache, and should only be used for * buttons which template and text are not dynamically created by your application (because it stays in cache for * all the life of your application). * <p> * This is the normal method to call if you're using it in a web application with a static button. * * @param id the wicket:id of the image * @param template the button generator to use * @param text the text of the button * @return an Image */ static public org.apache.wicket.markup.html.image.Image getImage(String id, ButtonTemplate template, String text) { return new org.apache.wicket.markup.html.image.Image(id, ButtonResource.getReference(), ButtonResource.getValueMap(template, text)); } /** * Creates a Wicket image button in the normal cache. Like getImage(), but returns an ImageButton. * @param id * @param template * @param text * @return */ static ImageButton getImageButton(String id, ButtonTemplate template, String text) { return new ImageButton(id, ButtonResource.getReference(), ButtonResource.getValueMap(template, text)); } /** * Creates a Wicket Image object that is bound to the given button generator and text. * This method registers the template and the text in the temporary cache. This method should be used if either your template or * text is dynamically changed at runtime: it is stored in a temporary LRU cache, and will be discarded from memory when the cache limit is * reached. * * @param id the wicket:id of the image * @param template the button generator to use * @param text the text of the button * @return an Image */ static public org.apache.wicket.markup.html.image.Image getTemporaryImage(String id, ButtonTemplate template, String text) { return new org.apache.wicket.markup.html.image.Image(id, ButtonResource.getReference(), ButtonResource.getTemporaryValueMap(template, text, false)); } synchronized static private Long getTemporaryId(ButtonResourceKey key) { Long keyid = s_tempKeyCache.get(key); if (keyid == null) { keyid = s_counter++; key.setId(keyid); s_tempKeyCache.put(key, keyid); s_tempCache.put(keyid, key); } return keyid; } /** * Provides a ResourceReference for a ButtonResource Object. Note that this reference is not enough to reference a * button, you must also use a ValueMap parameter, as provided by getValueMap(). * * @return a ResourceReference for a button */ static public ResourceReference getReference() { return new ResourceReference(ButtonResource.class, "buttons") { @Override protected Resource newResource() { return new ButtonResource(); } }; } /** * Return a ValueMap to associate to a ButtonResource ResourceReference. * This methods registers the pair [template, text] if it it not already in the normal cache. * @param template * @param text * @return */ static public ValueMap getValueMap(ButtonTemplate template, String text) { Long id = getUniqueId(new ButtonResourceKey(template, text)); ValueMap map = new ValueMap(); map.put("id", id.toString()); return map; } /** * Return a ValueMap for a pair [template, text]. The pair is stored in the temporary cache. * * @param template the ButtonTemplate object to use * @param text the text of the button * @param download true if the resource should be sent to the browser as an attachment, false (the default) to send it normally as an inline image. * @return A ValueMap to use with a ButtonResource ResourceReference */ static public ValueMap getTemporaryValueMap(ButtonTemplate template, String text, boolean download) { return getTemporaryValueMap(template, text, download, null); } /** * Return a ValueMap for a pair [template, text]. The pair is stored in the temporary cache. * * @param template the ButtonTemplate object to use * @param text the text of the button * @param download true if the resource should be sent to the browser as an attachment, false (the default) to send it normally as an inline image. * @param filename if download is true, this specified the filename under which the button image should be sent to the web browser. Can be null, for a default value. * @return A ValueMap to use with a ButtonResource ResourceReference */ static public ValueMap getTemporaryValueMap(ButtonTemplate template, String text, boolean download, String filename) { Long id = getTemporaryId(new ButtonResourceKey(template, text)); ValueMap map = new ValueMap(); map.put("id", id.toString()); if (download) map.put("download", "please"); if (download && filename!=null) map.put("filename", filename); return map; } @Override public IResourceStream getResourceStream() { ValueMap map = this.getParameters(); long id = map.getLong("id", -1); if (id>-1) { File cachedfile = s_fileCache.get(id); if (cachedfile == null) cachedfile = s_tempFileCache.get(id); if (cachedfile != null && cachedfile.exists()) { return new FileResourceStream(cachedfile); } boolean temporary = false; ButtonResourceKey k = s_cache.get(id); if (k == null) { k = s_tempCache.get(id); temporary = true; } File f = createImageFile(k); if (temporary) s_tempFileCache.put(id,f); else s_fileCache.put(id, f); return new FileResourceStream(f); } // TODO Auto-generated method stub return null; } private File createImageFile(ButtonResourceKey k) { BufferedImage img = k.getTemplate().getImage(k.getText()); File dir = getTempDir(); File file = new File(dir, Long.toHexString(k.getId())+".png"); try { ImageIO.write(img, "png", file); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return file; } static private File getTempDir() { if (s_tempDir == null) { try { File tempdir = (File)WebApplication.get().getServletContext().getAttribute("javax.servlet.context.tempdir"); if (tempdir == null) { tempdir = new File(System.getProperty("java.io.tmpdir")); } File dir = new File(tempdir, ButtonResource.class.getCanonicalName()); dir.mkdirs(); // We don't care for a race condition happening here, because even if it happens, it won't hurt. s_tempDir = dir; } catch (Exception exc) { exc.printStackTrace(); } } return s_tempDir; } static public void cleanUpFiles() { File dir = getTempDir(); if (dir != null) { for (File f: dir.listFiles()) { f.delete(); } } } @Override protected void setHeaders(WebResponse response) { ValueMap map = this.getParameters(); String download = map.getString("download", null); if (download != null) { String name = map.getString("filename", "image"); response.getHttpServletResponse().addHeader("content-disposition","attachment; filename="+name+".png"); response.setContentType("application/octet-stream"); } super.setHeaders(response); } }