/* Copyright (2012) Schibsted ASA * This file is part of Possom. * * Possom is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Possom is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Possom. If not, see <http://www.gnu.org/licenses/>. * */ package no.sesat.search.view.velocity; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.io.IOException; import java.io.Writer; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map; import net.sf.json.JSONSerializer; import net.sf.json.JsonConfig; import net.sf.json.util.CycleDetectionStrategy; import no.sesat.search.datamodel.DataModel; import no.sesat.search.view.config.SearchTab.Layout; import org.apache.log4j.Logger; import org.apache.velocity.context.InternalContextAdapter; import org.apache.velocity.exception.MethodInvocationException; import org.apache.velocity.exception.ParseErrorException; import org.apache.velocity.exception.ResourceNotFoundException; import org.apache.velocity.runtime.parser.node.Node; /** Convert sections (datamodel.includes) of the context's DataModel into a JSON object. * This allows client javascript to use the datamodel exactly like JSP and velocity templates do. * * <br/><br/> * * For example, adding to any tab (in views.xml) the following * <pre> * <layout id="json" main="datamodel-json.vm" > * <property key="datamodel.includes" value="searches,navigation,query"/> * <property key="datamodel.includes" value="configuration"/> * </layout> * </pre> * * allows the javascript to access after a request to this tab with the addition URL parameter "layout=json" * to access datamodel.getSearches() datamodel.getNavigation() datamodel.getQuery() * but will exclude any bean properties, at any level, named "configuration". * * Or what to serialise can be passed into the directive as named argument pairs. * For example: <br/> * #jsonDataModel('yellow' $datamodel.getSearch('yellow').results 'yellow' $datamodel.getSearch('yellow').results) * * <br/><br/> Add prettyJson=true to the url to get the output in pretty format. * * <br/><br/>This class was inspired by Karl Øie's work in MapJSONDirective.java * * @version $Id$ */ public final class JsonDataModelDirective extends AbstractDirective { // Constants ----------------------------------------------------- private static final Logger LOG = Logger.getLogger(JsonDataModelDirective.class); private static final String NAME = "jsonDataModel"; private static final String DATAMODEL_INCLUDES = "datamodel.includes"; private static final String DATAMODEL_EXCLUDES = "datamodel.excludes"; // Attributes ---------------------------------------------------- // Static -------------------------------------------------------- // Constructors -------------------------------------------------- public JsonDataModelDirective() {} // Public -------------------------------------------------------- public String getName() { return NAME; } public int getType() { return LINE; } public boolean render( final InternalContextAdapter context, final Writer writer, final Node node) throws IOException, ResourceNotFoundException, ParseErrorException, MethodInvocationException { if (0 <= node.jjtGetNumChildren()) { final JsonConfig config = new JsonConfig(); // Use NOPROP to indicate any cyclic references config.setCycleDetectionStrategy(CycleDetectionStrategy.NOPROP); // Ignore all transient bean properties config.setIgnoreTransientFields(true); final String[] excludes = null != ((Layout)context.get("layout")).getProperty(DATAMODEL_EXCLUDES) ? ((Layout)context.get("layout")).getProperty(DATAMODEL_EXCLUDES).split(",") : new String[0]; final String[] fullExcludes = new String[excludes.length + 2]; // Ignore all of the junkYard (it's deprecated) fullExcludes[0] = "junkYard"; // and all toString methods. fullExcludes[1] = "string"; System.arraycopy(excludes, 0, fullExcludes, 2, excludes.length); config.setExcludes(fullExcludes); final String[] includes = null != ((Layout)context.get("layout")).getProperty(DATAMODEL_INCLUDES) ? ((Layout)context.get("layout")).getProperty(DATAMODEL_INCLUDES).split(",") : new String[0]; final DataModel datamodel = getDataModel(context); final Map<String,Object> map = new HashMap<String,Object>(); for(String include : includes){ try { map.put(include, getDataModelInclude(datamodel, include)); } catch (IntrospectionException ex) { LOG.error("failed to add include " + include, ex); } catch (IllegalAccessException ex) { LOG.error("failed to add include " + include, ex); } catch (IllegalArgumentException ex) { LOG.error("failed to add include " + include, ex); } catch (InvocationTargetException ex) { LOG.error("failed to add include " + include, ex); } } for(int i = 0; i + 1 < node.jjtGetNumChildren(); i+=2){ map.put(getArgument(context, node, i), getObjectArgument(context, node, i+1)); } assert 0 < map.size() : "No datamodel.includes included"; if(null != datamodel.getParameters().getValue("prettyJson")){ writer.write(JSONSerializer.toJSON(map, config).toString(3)); }else{ JSONSerializer.toJSON(map, config).write(writer); } }else{ LOG.error("#" + getName() + " - wrong number of arguments"); return false; } return true; } // Z implementation ---------------------------------------------- // Y overrides --------------------------------------------------- // Package protected --------------------------------------------- // Protected ----------------------------------------------------- // Private ------------------------------------------------------- private Object getDataModelInclude(final DataModel datamodel, final String include) throws IntrospectionException, IllegalAccessException, IllegalArgumentException, InvocationTargetException{ Object dataObject = datamodel; for(int firstDot = 0 ; -1 != firstDot ; firstDot = include.indexOf('.', firstDot + 1)) { final int secondDot = include.indexOf('.', firstDot + 1); final String beanName = include.substring(firstDot, -1 != secondDot ? secondDot : include.length()); final BeanInfo beanInfo = Introspector.getBeanInfo(dataObject.getClass()); for(PropertyDescriptor prop : beanInfo.getPropertyDescriptors()){ if(beanName.equals(prop.getName())){ dataObject = prop.getReadMethod().invoke(dataObject); break; } } } return dataObject; } // Inner classes ------------------------------------------------- }