Java tutorial
/******************************************************************************* * Copyright 2009 Time Space Map, LLC * * 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 org.tsm.concharto.web.eventsearch; import info.bliki.wiki.model.WikiModel; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.lang.reflect.InvocationTargetException; import java.net.URLDecoder; import java.text.ParseException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletRequest; import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.propertyeditors.CustomBooleanEditor; import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.ServletRequestDataBinder; import org.springframework.web.bind.ServletRequestUtils; import org.springframework.web.util.WebUtils; import org.tsm.concharto.auth.AuthHelper; import org.tsm.concharto.dao.EventDao; import org.tsm.concharto.geocode.GAddress; import org.tsm.concharto.geocode.GGcoder; import org.tsm.concharto.model.Event; import org.tsm.concharto.model.geometry.TsGeometry; import org.tsm.concharto.model.time.TimeRange; import org.tsm.concharto.service.EventSearchService; import org.tsm.concharto.service.SearchParams; import org.tsm.concharto.service.Visibility; import org.tsm.concharto.util.JSONFormat; import org.tsm.concharto.util.LatLngBounds; import org.tsm.concharto.util.ProximityHelper; import org.tsm.concharto.util.SensibleMapDefaults; import org.tsm.concharto.util.TimeRangeFormat; import org.tsm.concharto.web.util.CatalogUtil; import org.tsm.concharto.web.util.DisplayTagHelper; import org.tsm.concharto.web.util.GeometryPropertyEditor; import org.tsm.concharto.web.util.TimeRangePropertyEditor; import org.tsm.concharto.web.wiki.SubstitutionMacro; import org.tsm.concharto.web.wiki.WikiModelFactory; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.Point; /** * Utility class used by search web controllers (e.g EventSearch, EmbeddedMap) * @author frank * */ public class SearchHelper { private static final String MODEL_TITLE = "title"; public static final String QUERY_ZOOM = "_zoom"; public static final String QUERY_WHAT = "_what"; public static final String QUERY_WHEN = "_when"; public static final String QUERY_WHERE = "_where"; public static final String QUERY_MAPTYPE = "_maptype"; public static final String QUERY_USERTAG = "_tag"; public static final String QUERY_LAT_LNG = "_ll"; public static final String QUERY_SW = "_sw"; public static final String QUERY_NE = "_ne"; public static final String QUERY_ID = "_id"; public static final String QUERY_WITHIN_MAP_BOUNDS = "_withinMap"; public static final String QUERY_EXCLUDE_TIMERANGE_OVERLAPS = "_timeoverlaps"; public static final String QUERY_EMBED = "_embed"; public static final String QUERY_KML = "_kml"; /** if _style=explicit we render wiki styles right in the html rather than using a style sheet * This is useful for HTML shown in other containers such as google earth or google maplets */ public static final String QUERY_STYLE = "_style"; public static final String QUERY_STYLE_EXPLICIT = "explicit"; public static final String MODEL_EVENTS = "events"; public static final String MODEL_TOTAL_RESULTS = "totalResults"; public static final String MODEL_CURRENT_RECORD = "currentRecord"; public static final String SESSION_DO_SEARCH_ON_SHOW = "doSearch"; public static final String SESSION_EVENT_SEARCH_FORM = "eventSearchForm"; public static final String SESSION_EVENT_SEARCH_RESULTS = "searchResults"; public static final String DISPLAYTAG_TABLE_ID = "event"; public static final int DISPLAYTAG_PAGESIZE = 25; private static final Object MODEL_POSITIONAL_ACCURACIES = "positionalAccuracies"; private static final Log log = LogFactory.getLog(SearchHelper.class); private EventSearchService eventSearchService; private EventDao eventDao; public void setEventSearchService(EventSearchService eventSearchService) { this.eventSearchService = eventSearchService; } public void setEventDao(EventDao eventDao) { this.eventDao = eventDao; } public void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception { binder.registerCustomEditor(TimeRange.class, new TimeRangePropertyEditor()); binder.registerCustomEditor(Geometry.class, new GeometryPropertyEditor()); binder.registerCustomEditor(Boolean.class, new CustomBooleanEditor("true", "false", true)); } /** * Populate the form with params from the URL query string * @param request servlet request * @param eventSearchForm the form to populate * @throws ServletRequestBindingException e * @throws UnsupportedEncodingException */ public void bindGetParameters(HttpServletRequest request, EventSearchForm eventSearchForm) throws ServletRequestBindingException { eventSearchForm.setWhere(ServletRequestUtils.getStringParameter(request, QUERY_WHERE)); String whenStr = ServletRequestUtils.getStringParameter(request, QUERY_WHEN); try { eventSearchForm.setWhen(TimeRangeFormat.parse(whenStr)); } catch (ParseException e) { //no error handling for URL string searches TODO add error handling } eventSearchForm.setWhat(ServletRequestUtils.getStringParameter(request, QUERY_WHAT)); Integer zoom = ServletRequestUtils.getIntParameter(request, QUERY_ZOOM); //check for incorrect zoom if ((zoom != null) && (zoom > 0) && (zoom < SensibleMapDefaults.NUM_ZOOM_LEVELS)) { eventSearchForm.setMapZoom(zoom); eventSearchForm.setZoomOverride(true); } //TSM-257 problem with googlebot. This is a kludge. It is difficult to figure out //why google isn't following the maptype String mapType = request.getParameter(QUERY_MAPTYPE); try { eventSearchForm.setMapType(new Integer(mapType)); } catch (NumberFormatException e) { eventSearchForm.setMapType(SensibleMapDefaults.DEFAULT_MAP_TYPE); } eventSearchForm.setUserTag(getUtf8QueryStringParameter(request, QUERY_USERTAG)); eventSearchForm.setLimitWithinMapBounds( (ServletRequestUtils.getBooleanParameter(request, QUERY_WITHIN_MAP_BOUNDS))); eventSearchForm.setExcludeTimeRangeOverlaps( (ServletRequestUtils.getBooleanParameter(request, QUERY_EXCLUDE_TIMERANGE_OVERLAPS))); eventSearchForm.setEmbed((ServletRequestUtils.getBooleanParameter(request, QUERY_EMBED))); eventSearchForm.setKml((ServletRequestUtils.getBooleanParameter(request, QUERY_KML))); Long eventId = ServletRequestUtils.getLongParameter(request, QUERY_ID); if (null != eventId) { eventSearchForm.setDisplayEventId(eventId); } Point ll = getLatLng(request, QUERY_LAT_LNG); if (null != ll) { eventSearchForm.setMapCenter(ll); //tells the javascript client side code not to "fit" the map to the search results eventSearchForm.setMapCenterOverride(true); } eventSearchForm.setBoundingBoxNE(getLatLng(request, QUERY_NE)); eventSearchForm.setBoundingBoxSW(getLatLng(request, QUERY_SW)); WebUtils.setSessionAttribute(request, SESSION_DO_SEARCH_ON_SHOW, true); } /** * TODO - There is a wierd bug with Get string character encoding that results in improper * decoding of the query string - when you call request.getCharacterEncoding() it returns UTF-8, * but the parameter is not decoded as UTF-8. So we have to do it by hand here. * * @param request * @param paramName */ private String getUtf8QueryStringParameter(HttpServletRequest request, String paramName) { String queryString = request.getQueryString(); String before = paramName + "="; String tag = StringUtils.substringBetween(queryString, before, "&"); if (tag == null) { tag = StringUtils.substringAfter(queryString, before); } try { tag = URLDecoder.decode(tag, "UTF-8"); } catch (UnsupportedEncodingException e) { log.error("Couldn't decode query paramter " + tag, e); } return tag; } private Point getLatLng(HttpServletRequest request, String query) throws ServletRequestBindingException { String ll = ServletRequestUtils.getStringParameter(request, query); String[] lls = StringUtils.split(ll, ','); if ((null != lls) && (lls.length == 2)) { try { Double lat_y = new Double(lls[0]); Double lng_x = new Double(lls[1]); GeometryFactory gf = new GeometryFactory(); return gf.createPoint(new Coordinate(lng_x, lat_y)); } catch (NumberFormatException e) { //do nothing } } return null; } /** * Geocode and update the eventSearchForm accordingly * @param mapKey google maps api key * @param request servlet request * @param eventSearchForm form to be updated * @throws IOException e */ public void geocode(String mapKey, HttpServletRequest request, EventSearchForm eventSearchForm) throws IOException { if (!StringUtils.isEmpty(eventSearchForm.getWhere())) { //String mapKey = formController.getMessageSourceAccessor().getMessage(makeMapKeyCode(request)); GAddress address = GGcoder.geocode(eventSearchForm.getWhere(), mapKey); Point point = address.getPoint(); eventSearchForm.setMapCenter(point); if (point == null) { eventSearchForm.setIsGeocodeSuccess(false); } if (null == eventSearchForm.getMapZoom()) { //if they didn't supply the zoom level, we will get it from the address accuracy if (address.getAccuracy() < SensibleMapDefaults.ACCURACY_TO_ZOOM.length) { eventSearchForm.setMapZoom(SensibleMapDefaults.ACCURACY_TO_ZOOM[address.getAccuracy()]); } else { eventSearchForm.setMapZoom(SensibleMapDefaults.ZOOM_COUNTRY); } } } else { //no location was specified, we will use the default if (eventSearchForm.getMapZoom() == null) { eventSearchForm.setMapZoom(SensibleMapDefaults.ZOOM_WORLD); } if (eventSearchForm.getMapCenter() == null) { eventSearchForm.setMapCenter(SensibleMapDefaults.NORTH_ATLANTIC); } } } /** * Search for events and return results in the model. Some search results are also put in the form * so that the javascript functions may use them. * * @param mapKey google api key * @param request request * @param model model containing search results * @param eventSearchForm form containing search parameters and search results * @return a list of Event objects */ @SuppressWarnings("unchecked") public List<Event> doSearch(String mapKey, HttpServletRequest request, Map model, EventSearchForm eventSearchForm) { /* * 1. Get the lat-long bounding box of whatever zoom * level we are at. 2. Parse the time field to extract a time range 3. * Do a search to find the count of all events within that text filter, * time range and bounding box * TODO cache the total results if nothing has changed (e.g. pagination) */ List<Event> events; Long totalResults; Integer firstRecord; if (eventSearchForm.getDisplayEventId() != null) { Event event = eventSearchService.findById(eventSearchForm.getDisplayEventId()); events = new ArrayList<Event>(); events.add(event); eventSearchForm.setMapCenter(event.getTsGeometry().getGeometry().getCentroid()); //Don't override the zoom for polylines or polygons no matter what. Otherwise, there is too //much opportunity for confusion. For example, the centroid of the polyline may be nowhere //near the border in which case you won't see the line at all. if (event.getTsGeometry().getGeometry() instanceof Point) { eventSearchForm.setMapCenterOverride(true); eventSearchForm.setZoomOverride(true); } else { eventSearchForm.setMapCenterOverride(false); eventSearchForm.setZoomOverride(false); } eventSearchForm.setMapZoom(event.getZoomLevel()); eventSearchForm.setMapType(event.getMapType()); totalResults = 1L; firstRecord = 0; //now remove the id from the form - we don't want to get stuck forever showing this event //the browser javascript still needs the event id so it can crate a linkHere url, so we make //a copy of the id eventSearchForm.setLinkHereEventId(eventSearchForm.getDisplayEventId()); eventSearchForm.setDisplayEventId(null); } else { eventSearchForm.setLinkHereEventId(null); eventSearchForm.setDisplayEventId(null); if (eventSearchForm.getMapCenter() == null) { //geocode try { geocode(mapKey, request, eventSearchForm); } catch (IOException e) { //TODO, perhaps some better error handling here?? log.info("Exception geocoding location " + eventSearchForm.getWhere() + e); } } //we are ok to go if IsGeocodeSuccess is null or True if (!BooleanUtils.isFalse(eventSearchForm.getIsGeocodeSuccess())) { firstRecord = DisplayTagHelper.getFirstRecord(request, DISPLAYTAG_TABLE_ID, DISPLAYTAG_PAGESIZE); LatLngBounds bounds = getBounds(eventSearchForm); SearchParams params = new SearchParams(); params.setTextFilter(eventSearchForm.getWhat()); params.setUserTag(eventSearchForm.getUserTag()); params.setTimeRange(eventSearchForm.getWhen()); params.setVisibility(getVisibility(eventSearchForm)); params.setCatalog(CatalogUtil.getCatalog(request)); //note these are opposites.. a value of null or false = false, true=true params.setIncludeTimeRangeOverlaps( !BooleanUtils.isTrue(eventSearchForm.getExcludeTimeRangeOverlaps())); events = eventSearchService.search(DISPLAYTAG_PAGESIZE, firstRecord, bounds, params); //for debugging //addDebugBoundingBox(events, bounds); totalResults = eventSearchService.getCount(bounds, params); } else { //failed geocode, no points events = new ArrayList<Event>(); totalResults = 0L; firstRecord = 0; } } prepareModel(model, events, totalResults, firstRecord); //NOTE: we are putting the events into the command so that the page javascript //functions can properly display them using google's mapping API List<Event> renderedEvents = renderWiki(events, request); eventSearchForm.setSearchResults(JSONFormat.toJSON(renderedEvents)); setupPageTitle(request, eventSearchForm, model, events); return events; } /** * Creates a string to be displayed in the browser title based on * search terms or event id * @param request * @param eventSearchForm * @param model * @param events */ @SuppressWarnings("unchecked") private void setupPageTitle(HttpServletRequest request, EventSearchForm eventSearchForm, Map model, List<Event> events) { // if the request contains an id= parameter, it means this is a one-at-a-time listing. // Both users and search engines will see this so we need to show the event summary in // the title String title; if (null != request.getParameter("_id")) { title = events.get(0).getSummary(); } else { //summary of the search terms List<String> terms = new ArrayList<String>(); addIfNotEmpty(terms, "where: ", eventSearchForm.getWhere()); if (null != eventSearchForm.getWhen()) { addIfNotEmpty(terms, "when: ", eventSearchForm.getWhen().getAsText()); } addIfNotEmpty(terms, "what: ", eventSearchForm.getWhat()); addIfNotEmpty(terms, "tag: ", eventSearchForm.getUserTag()); title = StringUtils.join(terms.toArray(), ", "); } model.put(MODEL_TITLE, title); } /** * Utility for natural language processing - adding commas between a list of search terms * @param list * @param prefix * @param str */ private void addIfNotEmpty(List<String> list, String prefix, String str) { if (!StringUtils.isEmpty(str)) { list.add(prefix + str); } } private List<Event> renderWiki(List<Event> events, HttpServletRequest request) { List<Event> renderedEvents = new ArrayList<Event>(); WikiModel wikiModel = WikiModelFactory.newWikiModel(request); for (Event event : events) { try { Event renderedEvent = (Event) BeanUtils.cloneBean(event); boolean explicitStyle = false; //use css style sheets if (QUERY_STYLE_EXPLICIT.equals(request.getParameter(QUERY_STYLE))) { explicitStyle = true; } renderedEvent.setDescription(renderWiki(wikiModel, event.getDescription(), explicitStyle)); renderedEvent.setSource(renderWiki(wikiModel, event.getSource(), explicitStyle)); renderedEvents.add(renderedEvent); } catch (IllegalAccessException e) { log.error(e); } catch (InstantiationException e) { log.error(e); } catch (InvocationTargetException e) { log.error(e); } catch (NoSuchMethodException e) { log.error(e); } } return renderedEvents; } /** * Render the raw wiki text. If explicitStyle is set, then add explicit styles to the html tags. * @param wikiModel * @param rawWikiText * @param explicitStyle * @return */ private String renderWiki(WikiModel wikiModel, String rawWikiText, boolean explicitStyle) { String rendered = wikiModel.render(rawWikiText); if (explicitStyle) { rendered = SubstitutionMacro.explicitStyles(rendered); } return rendered; } @SuppressWarnings("unchecked") public void prepareModel(Map model, List<Event> events, Long totalResults, Integer currentRecord) { model.put(MODEL_CURRENT_RECORD, currentRecord); model.put(MODEL_TOTAL_RESULTS, Math.round(totalResults)); model.put(MODEL_EVENTS, events); model.put(MODEL_POSITIONAL_ACCURACIES, eventDao.getPositionalAccuracies()); } /** * Find the search bounding box. It depends on the map center and zoom level. * @param eventSearchForm search form * @return LatLngBounds search bounding box */ private LatLngBounds getBounds(EventSearchForm eventSearchForm) { //if we are below a certain zoom level, we will still search a wider area LatLngBounds bounds = null; if ((StringUtils.isEmpty(eventSearchForm.getWhere())) && BooleanUtils.isFalse(eventSearchForm.getLimitWithinMapBounds())) { //when they specify fit view to all results, we don't want a bounding box, unless //they also specify a place, in which case the geocode takes precedence return null; } //if we already have bounds if (hasBounds(eventSearchForm)) { //if there are no bounds or the user explicitly said to use those bounds if ((!zoomedInTooLow(eventSearchForm) || (BooleanUtils.isTrue(eventSearchForm.getLimitWithinMapBounds())))) { bounds = new LatLngBounds(eventSearchForm.getBoundingBoxSW(), eventSearchForm.getBoundingBoxNE()); } //when we are zoomed in real low we want to search more than just the visible map, instead //use the a wider search radius else { bounds = searchBoxBounds(eventSearchForm); } } else if (null != eventSearchForm.getWhere()) { //there is a location, but no bounds, so we have to make them bounds = searchBoxBounds(eventSearchForm); } return bounds; } private LatLngBounds searchBoxBounds(EventSearchForm eventSearchForm) { return ProximityHelper.getBounds(SensibleMapDefaults.SEARCH_BOX_DIMENTSIONS[eventSearchForm.getMapZoom()], eventSearchForm.getMapCenter()); } private boolean zoomedInTooLow(EventSearchForm eventSearchForm) { return eventSearchForm.getMapZoom() >= SensibleMapDefaults.ZOOM_BOX_THRESHOLD; } private boolean hasBounds(EventSearchForm eventSearchForm) { return ((null != eventSearchForm.getBoundingBoxSW()) && (null != eventSearchForm.getBoundingBoxSW())); } /** * Useful for debugging bounding box problems. * @param events list of Event objects * @param bounds bounding box */ public void addDebugBoundingBox(List<Event> events, LatLngBounds bounds) { Set<Geometry> boxes = ProximityHelper.getBoundingBoxes(bounds.getSouthWest(), bounds.getNorthEast()); for (Geometry box : boxes) { Event event = new Event(); event.setSummary("box"); event.setTsGeometry(new TsGeometry(box)); try { event.setWhen(TimeRangeFormat.parse("2000")); } catch (ParseException e) { //no action } event.setId(2L); event.setHasUnresolvedFlag(false); event.setWhere(""); event.setDescription(""); event.setUserTagsAsString(""); event.setSource(""); events.add(event); } } /** * Only administrators are allowed to see removed events. We need to check for proper authorization * because the user could have hacked the HTML to add the showInvisible parameter * @param eventSearchForm EventSearchForm * @return true if we are supposed to show visible events. false if we are supposed to show invisible events. */ private Visibility getVisibility(EventSearchForm eventSearchForm) { if (AuthHelper.isUserAnAdmin()) { //TODO probably a better way than this if (StringUtils.equals(EventSearchForm.SHOW_HIDDEN, eventSearchForm.getShow())) { return Visibility.HIDDEN; } else if (StringUtils.equals(EventSearchForm.SHOW_NORMAL, eventSearchForm.getShow())) { return Visibility.NORMAL; } else if (StringUtils.equals(EventSearchForm.SHOW_FLAGGED, eventSearchForm.getShow())) { return Visibility.FLAGGED; } } return Visibility.NORMAL; } /** * Utility to get the google maps API key from a properties file * @param request used to find the url and port * @return google maps API key */ public String makeMapKeyCode(HttpServletRequest request) { //map.${pageContext.request.serverName}.${pageContext.request.serverPort}.key return new StringBuffer("map.").append(request.getServerName()).append('.').append(request.getServerPort()) .append('.').append("key").toString(); } }