NIHVIVO-3772 NIHVIVO-3721 Incorporate the OpenSocial integration from Eric Meeks at CTSI, UCSF. Add instructions on how to install and configure ORNG Shindig.
This commit is contained in:
parent
dfb955f39f
commit
f4e5c31aa8
13 changed files with 1851 additions and 1 deletions
|
@ -2,9 +2,15 @@
|
|||
|
||||
package edu.cornell.mannlib.vitro.webapp.controller.individual;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.json.JSONException;
|
||||
|
||||
import edu.cornell.mannlib.vitro.webapp.auth.permissions.SimplePermission;
|
||||
import edu.cornell.mannlib.vitro.webapp.auth.policy.PolicyHelper;
|
||||
import edu.cornell.mannlib.vitro.webapp.beans.Individual;
|
||||
|
@ -22,6 +28,7 @@ import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory;
|
|||
import edu.cornell.mannlib.vitro.webapp.web.beanswrappers.ReadOnlyBeansWrapper;
|
||||
import edu.cornell.mannlib.vitro.webapp.web.templatemodels.individual.IndividualTemplateModel;
|
||||
import edu.cornell.mannlib.vitro.webapp.web.templatemodels.individuallist.ListedIndividual;
|
||||
import edu.ucsf.vitro.opensocial.OpenSocialManager;
|
||||
import freemarker.ext.beans.BeansWrapper;
|
||||
import freemarker.template.TemplateModel;
|
||||
import freemarker.template.TemplateModelException;
|
||||
|
@ -33,6 +40,9 @@ import freemarker.template.TemplateModelException;
|
|||
* TODO clean this up.
|
||||
*/
|
||||
class IndividualResponseBuilder {
|
||||
private static final Log log = LogFactory
|
||||
.getLog(IndividualResponseBuilder.class);
|
||||
|
||||
private static final Map<String, String> namespaces = new HashMap<String, String>() {{
|
||||
put("display", VitroVocabulary.DISPLAY);
|
||||
put("vitro", VitroVocabulary.vitroURI);
|
||||
|
@ -78,6 +88,24 @@ class IndividualResponseBuilder {
|
|||
//If special values required for individuals like menu, include values in template values
|
||||
body.putAll(getSpecialEditingValues());
|
||||
|
||||
// VIVO OpenSocial Extension by UCSF
|
||||
try {
|
||||
OpenSocialManager openSocialManager = new OpenSocialManager(vreq,
|
||||
itm.isEditable() ? "individual-EDIT-MODE" : "individual", itm.isEditable());
|
||||
openSocialManager.setPubsubData(OpenSocialManager.JSON_PERSONID_CHANNEL,
|
||||
OpenSocialManager.buildJSONPersonIds(individual, "1 person found"));
|
||||
body.put(OpenSocialManager.TAG_NAME, openSocialManager);
|
||||
if (openSocialManager.isVisible()) {
|
||||
body.put("bodyOnload", "my.init();");
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
log.error("JSONException in doTemplate()", e);
|
||||
} catch (IOException e) {
|
||||
log.error("IOException in doTemplate()", e);
|
||||
} catch (SQLException e) {
|
||||
log.error("SQLException in doTemplate()", e);
|
||||
}
|
||||
|
||||
String template = new IndividualTemplateLocator(vreq, individual).findTemplate();
|
||||
|
||||
return new TemplateResponseValues(template, body);
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
package edu.cornell.mannlib.vitro.webapp.search.controller;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
@ -51,6 +52,7 @@ import edu.cornell.mannlib.vitro.webapp.search.beans.VitroQueryFactory;
|
|||
import edu.cornell.mannlib.vitro.webapp.search.solr.SolrSetup;
|
||||
import edu.cornell.mannlib.vitro.webapp.web.templatemodels.LinkTemplateModel;
|
||||
import edu.cornell.mannlib.vitro.webapp.web.templatemodels.searchresult.IndividualSearchResult;
|
||||
import edu.ucsf.vitro.opensocial.OpenSocialManager;
|
||||
|
||||
/**
|
||||
* Paged search controller that uses Solr
|
||||
|
@ -268,7 +270,24 @@ public class PagedSearchController extends FreemarkerHttpServlet {
|
|||
vreq.getServletPath(), pagingLinkParams));
|
||||
}
|
||||
|
||||
String template = templateTable.get(format).get(Result.PAGED);
|
||||
// VIVO OpenSocial Extension by UCSF
|
||||
try {
|
||||
OpenSocialManager openSocialManager = new OpenSocialManager(vreq, "search");
|
||||
// put list of people found onto pubsub channel
|
||||
List<String> ids = OpenSocialManager.getOpenSocialId(individuals);
|
||||
openSocialManager.setPubsubData(OpenSocialManager.JSON_PERSONID_CHANNEL,
|
||||
OpenSocialManager.buildJSONPersonIds(ids, "" + ids.size() + " people found"));
|
||||
body.put("openSocial", openSocialManager);
|
||||
if (openSocialManager.isVisible()) {
|
||||
body.put("bodyOnload", "my.init();");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("IOException in doTemplate()", e);
|
||||
} catch (SQLException e) {
|
||||
log.error("SQLException in doTemplate()", e);
|
||||
}
|
||||
|
||||
String template = templateTable.get(format).get(Result.PAGED);
|
||||
|
||||
return new TemplateResponseValues(template, body);
|
||||
} catch (Throwable e) {
|
||||
|
|
100
webapp/src/edu/ucsf/vitro/opensocial/GadgetController.java
Normal file
100
webapp/src/edu/ucsf/vitro/opensocial/GadgetController.java
Normal file
|
@ -0,0 +1,100 @@
|
|||
package edu.ucsf.vitro.opensocial;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.FreemarkerHttpServlet;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ExceptionResponseValues;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.RedirectResponseValues;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues;
|
||||
|
||||
public class GadgetController extends FreemarkerHttpServlet {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Log log = LogFactory.getLog(GadgetController.class);
|
||||
|
||||
@Override
|
||||
protected ResponseValues processRequest(VitroRequest vreq) {
|
||||
if (vreq.getServletPath().endsWith("/sandbox")) {
|
||||
boolean sandbox = "True".equalsIgnoreCase(ConfigurationProperties.getBean(vreq.getSession()
|
||||
.getServletContext()).getProperty("OpenSocial.sandbox"));
|
||||
if (!sandbox) {
|
||||
return new ExceptionResponseValues( new Exception("Sandbox not available"));
|
||||
}
|
||||
return processGadgetSandbox(vreq);
|
||||
}
|
||||
else {
|
||||
return processGadgetDetails(vreq);
|
||||
}
|
||||
}
|
||||
|
||||
protected ResponseValues processGadgetDetails(VitroRequest vreq) {
|
||||
try {
|
||||
Map<String, Object> body = new HashMap<String, Object>();
|
||||
|
||||
body.put("title", "Gadget Details");
|
||||
// VIVO OpenSocial Extension by UCSF
|
||||
try {
|
||||
OpenSocialManager openSocialManager = new OpenSocialManager(vreq, "gadgetDetails");
|
||||
body.put(OpenSocialManager.TAG_NAME, openSocialManager);
|
||||
if (openSocialManager.isVisible()) {
|
||||
body.put("bodyOnload", "my.init();");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("IOException in doTemplate()", e);
|
||||
} catch (SQLException e) {
|
||||
log.error("SQLException in doTemplate()", e);
|
||||
}
|
||||
|
||||
return new TemplateResponseValues("gadgetDetails.ftl", body);
|
||||
|
||||
} catch (Throwable e) {
|
||||
log.error(e, e);
|
||||
return new ExceptionResponseValues(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTitle(String siteName, VitroRequest vreq) {
|
||||
return "Gadget Details";
|
||||
}
|
||||
|
||||
protected ResponseValues processGadgetSandbox(VitroRequest vreq) {
|
||||
if ("POST".equalsIgnoreCase(vreq.getMethod())) {
|
||||
vreq.getSession().setAttribute(OpenSocialManager.OPENSOCIAL_GADGETS, vreq.getParameter("gadgetURLS"));
|
||||
vreq.getSession().setAttribute(OpenSocialManager.OPENSOCIAL_DEBUG, vreq.getParameter("debug") != null);
|
||||
vreq.getSession().setAttribute(OpenSocialManager.OPENSOCIAL_NOCACHE, vreq.getParameter("useCache") == null);
|
||||
return new RedirectResponseValues("/");
|
||||
}
|
||||
|
||||
Map<String, Object> body = new HashMap<String, Object>();
|
||||
body.put("title", "Gadget Sandbox");
|
||||
|
||||
try {
|
||||
OpenSocialManager openSocialManager = new OpenSocialManager(vreq, "gadgetSandbox");
|
||||
String gadgetURLS = "";
|
||||
for (PreparedGadget gadget : openSocialManager.getVisibleGadgets())
|
||||
{
|
||||
gadgetURLS += gadget.getGadgetURL() + System.getProperty("line.separator");
|
||||
}
|
||||
body.put("gadgetURLS", gadgetURLS);
|
||||
body.put(OpenSocialManager.TAG_NAME, openSocialManager);
|
||||
} catch (IOException e) {
|
||||
log.error("IOException in doTemplate()", e);
|
||||
} catch (SQLException e) {
|
||||
log.error("SQLException in doTemplate()", e);
|
||||
}
|
||||
|
||||
|
||||
return new TemplateResponseValues("gadgetLogin.ftl", body);
|
||||
}
|
||||
|
||||
}
|
204
webapp/src/edu/ucsf/vitro/opensocial/GadgetSpec.java
Normal file
204
webapp/src/edu/ucsf/vitro/opensocial/GadgetSpec.java
Normal file
|
@ -0,0 +1,204 @@
|
|||
package edu.ucsf.vitro.opensocial;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.dbcp.BasicDataSource;
|
||||
|
||||
public class GadgetSpec {
|
||||
private String openSocialGadgetURL;
|
||||
private String name;
|
||||
private int appId = 0;
|
||||
private List<String> channels = new ArrayList<String>();
|
||||
private boolean unknownGadget = false;
|
||||
private Map<String, GadgetViewRequirements> viewRequirements = new HashMap<String, GadgetViewRequirements>();
|
||||
|
||||
// For preloading
|
||||
public GadgetSpec(int appId, String name, String openSocialGadgetURL,
|
||||
List<String> channels) {
|
||||
this.appId = appId;
|
||||
this.name = name;
|
||||
this.openSocialGadgetURL = openSocialGadgetURL;
|
||||
this.channels.addAll(channels);
|
||||
}
|
||||
|
||||
public GadgetSpec(int appId, String name, String openSocialGadgetURL,
|
||||
String channelsStr) {
|
||||
this(appId, name, openSocialGadgetURL, Arrays.asList(channelsStr != null
|
||||
&& channelsStr.length() > 0 ? channelsStr.split(" ") : new String[0]));
|
||||
}
|
||||
|
||||
public GadgetSpec(int appId, String name, String openSocialGadgetURL,
|
||||
List<String> channels, boolean unknownGadget, BasicDataSource ds)
|
||||
throws SQLException {
|
||||
this(appId, name, openSocialGadgetURL, channels);
|
||||
this.unknownGadget = unknownGadget;
|
||||
// Load gadgets from the DB first
|
||||
if (!unknownGadget) {
|
||||
Connection conn = null;
|
||||
Statement stmt = null;
|
||||
ResultSet rset = null;
|
||||
|
||||
try {
|
||||
String sqlCommand = "select page, viewer_req, owner_req, view, closed_width, open_width, start_closed, chromeId, display_order from shindig_app_views where appId = "
|
||||
+ appId;
|
||||
conn = ds.getConnection();
|
||||
stmt = conn.createStatement();
|
||||
rset = stmt.executeQuery(sqlCommand);
|
||||
while (rset.next()) {
|
||||
viewRequirements.put(
|
||||
rset.getString(1),
|
||||
new GadgetViewRequirements(rset.getString(1), rset
|
||||
.getString(2), rset.getString(3), rset
|
||||
.getString(4), rset.getInt(5), rset
|
||||
.getInt(6), rset.getBoolean(7), rset
|
||||
.getString(8), rset.getInt(9)));
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
if (rset != null) {
|
||||
rset.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
if (stmt != null) {
|
||||
stmt.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
if (conn != null) {
|
||||
conn.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getAppId() {
|
||||
return appId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getGadgetURL() {
|
||||
return openSocialGadgetURL;
|
||||
}
|
||||
|
||||
public List<String> getChannels() {
|
||||
return channels;
|
||||
}
|
||||
|
||||
public boolean listensTo(String channel) { // if an unknown gadget just say yes,
|
||||
// we don't care about
|
||||
// performance in this situation
|
||||
return unknownGadget || channels.contains(channel);
|
||||
}
|
||||
|
||||
public GadgetViewRequirements getGadgetViewRequirements(String page) {
|
||||
if (viewRequirements.containsKey(page)) {
|
||||
return viewRequirements.get(page);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean show(String viewerId, String ownerId, String page,
|
||||
BasicDataSource ds) throws SQLException {
|
||||
boolean show = true;
|
||||
// if there are no view requirements, go ahead and show it. We are
|
||||
// likely testing out a new gadget
|
||||
// if there are some, turn it off unless this page is
|
||||
if (viewRequirements.size() > 0) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
if (viewRequirements.containsKey(page)) {
|
||||
show = true;
|
||||
GadgetViewRequirements req = getGadgetViewRequirements(page);
|
||||
if ('U' == req.getViewerReq() && viewerId != null) {
|
||||
show = false;
|
||||
} else if ('R' == req.getViewerReq()) {
|
||||
show &= isRegisteredTo(viewerId, ds);
|
||||
}
|
||||
if ('R' == req.getOwnerReq()) {
|
||||
show &= isRegisteredTo(ownerId, ds);
|
||||
} else if ('S' == req.getOwnerReq()) {
|
||||
show &= (viewerId == ownerId);
|
||||
}
|
||||
}
|
||||
return show;
|
||||
}
|
||||
|
||||
public boolean isRegisteredTo(String personId, BasicDataSource ds)
|
||||
throws SQLException {
|
||||
int count = 0;
|
||||
|
||||
Connection conn = null;
|
||||
Statement stmt = null;
|
||||
ResultSet rset = null;
|
||||
|
||||
try {
|
||||
String sqlCommand = "select count(*) from shindig_app_registry where appId = "
|
||||
+ getAppId() + " and personId = '" + personId + "';";
|
||||
conn = ds.getConnection();
|
||||
stmt = conn.createStatement();
|
||||
rset = stmt.executeQuery(sqlCommand);
|
||||
while (rset.next()) {
|
||||
count = rset.getInt(1);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
if (rset != null) {
|
||||
rset.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
if (stmt != null) {
|
||||
stmt.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
if (conn != null) {
|
||||
conn.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return (count == 1);
|
||||
}
|
||||
|
||||
public boolean fromSandbox() {
|
||||
return unknownGadget;
|
||||
}
|
||||
|
||||
// who sees it? Return the viewerReq for the ProfileDetails page
|
||||
public char getVisibleScope() {
|
||||
GadgetViewRequirements req = getGadgetViewRequirements("/display");
|
||||
return req != null ? req.getViewerReq() : ' ';
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "" + this.appId + ":" + this.name + ":" + this.openSocialGadgetURL;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package edu.ucsf.vitro.opensocial;
|
||||
|
||||
public class GadgetViewRequirements {
|
||||
private String page;
|
||||
private char viewerReq; // U for User or null for no requirement
|
||||
private char ownerReq; // R for Registered or null for no requirement
|
||||
private String view;
|
||||
private int closedWidth;
|
||||
private int openWidth;
|
||||
private boolean startClosed;
|
||||
private String chromeId;
|
||||
private int display_order;
|
||||
|
||||
public GadgetViewRequirements(String page, char viewerReq, char ownerReq,
|
||||
String view, int closedWidth, int openWidth, boolean startClosed,
|
||||
String chromeId, int display_order) {
|
||||
this.page = page;
|
||||
this.viewerReq = viewerReq;
|
||||
this.ownerReq = ownerReq;
|
||||
this.view = view;
|
||||
this.closedWidth = closedWidth;
|
||||
this.openWidth = openWidth;
|
||||
this.startClosed = startClosed;
|
||||
this.chromeId = chromeId;
|
||||
this.display_order = display_order;
|
||||
}
|
||||
|
||||
public GadgetViewRequirements(String page, String viewerReq,
|
||||
String ownerReq, String view, int closedWidth, int openWidth,
|
||||
boolean startClosed, String chromeId, int display_order) {
|
||||
this(page, viewerReq != null ? viewerReq.charAt(0) : ' ',
|
||||
ownerReq != null ? ownerReq.charAt(0) : ' ', view, closedWidth,
|
||||
openWidth, startClosed, chromeId, display_order);
|
||||
}
|
||||
|
||||
public char getViewerReq() {
|
||||
return viewerReq;
|
||||
}
|
||||
|
||||
public char getOwnerReq() {
|
||||
return ownerReq;
|
||||
}
|
||||
|
||||
public String getView() {
|
||||
return view;
|
||||
}
|
||||
|
||||
public int getClosedWidth() {
|
||||
return closedWidth;
|
||||
}
|
||||
|
||||
public int getOpenWidth() {
|
||||
return openWidth;
|
||||
}
|
||||
|
||||
public boolean getStartClosed() {
|
||||
return startClosed;
|
||||
}
|
||||
|
||||
public String getChromeId() {
|
||||
return chromeId;
|
||||
}
|
||||
|
||||
int getDisplayOrder() {
|
||||
return display_order;
|
||||
}
|
||||
}
|
483
webapp/src/edu/ucsf/vitro/opensocial/OpenSocialManager.java
Normal file
483
webapp/src/edu/ucsf/vitro/opensocial/OpenSocialManager.java
Normal file
|
@ -0,0 +1,483 @@
|
|||
package edu.ucsf.vitro.opensocial;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.net.URLEncoder;
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.dbcp.BasicDataSource;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import edu.cornell.mannlib.vedit.beans.LoginStatusBean;
|
||||
import edu.cornell.mannlib.vitro.webapp.beans.Individual;
|
||||
import edu.cornell.mannlib.vitro.webapp.beans.UserAccount;
|
||||
import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.individual.IndividualRequestAnalysisContextImpl;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.individual.IndividualRequestAnalyzer;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.individual.IndividualRequestInfo;
|
||||
|
||||
public class OpenSocialManager {
|
||||
public static final String SHINDIG_URL_PROP = "OpenSocial.shindigURL";
|
||||
|
||||
public static final String OPENSOCIAL_DEBUG = "OPENSOCIAL_DEBUG";
|
||||
public static final String OPENSOCIAL_NOCACHE = "OPENSOCIAL_NOCACHE";
|
||||
public static final String OPENSOCIAL_GADGETS = "OPENSOCIAL_GADGETS";
|
||||
|
||||
public static final String JSON_PERSONID_CHANNEL = "JSONPersonIds";
|
||||
public static final String JSON_PMID_CHANNEL = "JSONPubMedIds";
|
||||
public static final String TAG_NAME = "openSocial";
|
||||
|
||||
private static final String DEFAULT_DRIVER = "com.mysql.jdbc.Driver";
|
||||
|
||||
private List<PreparedGadget> gadgets = new ArrayList<PreparedGadget>();
|
||||
private Map<String, String> pubsubdata = new HashMap<String, String>();
|
||||
private String viewerId = null;
|
||||
private String ownerId = null;
|
||||
private boolean isDebug = false;
|
||||
private boolean noCache = false;
|
||||
private String pageName;
|
||||
private ConfigurationProperties configuration;
|
||||
|
||||
private BasicDataSource dataSource;
|
||||
|
||||
public OpenSocialManager(VitroRequest vreq, String pageName) throws SQLException, IOException {
|
||||
this(vreq, pageName, false);
|
||||
}
|
||||
|
||||
public OpenSocialManager(VitroRequest vreq, String pageName, boolean editMode) throws SQLException, IOException {
|
||||
this.isDebug = vreq.getSession() != null
|
||||
&& Boolean.TRUE.equals(vreq.getSession().getAttribute(OPENSOCIAL_DEBUG));
|
||||
this.noCache = vreq.getSession() != null
|
||||
&& Boolean.TRUE.equals(vreq.getSession().getAttribute(OPENSOCIAL_NOCACHE));
|
||||
this.pageName = pageName;
|
||||
|
||||
configuration = ConfigurationProperties.getBean(vreq.getSession()
|
||||
.getServletContext());
|
||||
|
||||
if (configuration.getProperty(SHINDIG_URL_PROP) == null) {
|
||||
// do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
// Analyze the request to figure out whose page we are viewing.
|
||||
this.ownerId = figureOwnerId(vreq);
|
||||
|
||||
// in editMode we need to set the viewer to be the same as the owner
|
||||
// otherwise, the gadget will not be able to save appData correctly
|
||||
if (editMode) {
|
||||
this.viewerId = ownerId;
|
||||
}
|
||||
else {
|
||||
UserAccount viewer = LoginStatusBean.getCurrentUser(vreq);
|
||||
this.viewerId = viewer != null ? viewer.getUri() : null;
|
||||
}
|
||||
|
||||
boolean gadgetSandbox = "gadgetSandbox".equals(pageName);
|
||||
String requestAppId = vreq.getParameter("appId");
|
||||
|
||||
Map<String, GadgetSpec> dbApps = new HashMap<String, GadgetSpec>();
|
||||
Map<String, GadgetSpec> officialApps = new HashMap<String, GadgetSpec>();
|
||||
|
||||
dataSource = new BasicDataSource();
|
||||
dataSource.setDriverClassName(DEFAULT_DRIVER);
|
||||
dataSource.setUsername(configuration
|
||||
.getProperty("VitroConnection.DataSource.username"));
|
||||
dataSource.setPassword(configuration
|
||||
.getProperty("VitroConnection.DataSource.password"));
|
||||
dataSource.setUrl(configuration
|
||||
.getProperty("VitroConnection.DataSource.url"));
|
||||
|
||||
// Load gadgets from the DB first
|
||||
Connection conn = null;
|
||||
Statement stmt = null;
|
||||
ResultSet rset = null;
|
||||
try {
|
||||
|
||||
String sqlCommand = "select appId, name, url, channels, enabled from shindig_apps";
|
||||
// if a specific app is requested, only grab it
|
||||
if (requestAppId != null) {
|
||||
sqlCommand += " where appId = " + requestAppId;
|
||||
}
|
||||
conn = dataSource.getConnection();
|
||||
stmt = conn.createStatement();
|
||||
rset = stmt.executeQuery(sqlCommand);
|
||||
|
||||
while (rset.next()) {
|
||||
GadgetSpec spec = new GadgetSpec(rset.getInt(1),
|
||||
rset.getString(2), rset.getString(3), rset.getString(4));
|
||||
String gadgetFileName = getGadgetFileNameFromURL(rset
|
||||
.getString(3));
|
||||
|
||||
dbApps.put(gadgetFileName, spec);
|
||||
if (requestAppId != null || rset.getBoolean(5)) {
|
||||
officialApps.put(gadgetFileName, spec);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
if (rset != null) {
|
||||
rset.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
if (stmt != null) {
|
||||
stmt.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
if (conn != null) {
|
||||
conn.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// Add manual gadgets if there are any
|
||||
// Note that this block of code only gets executed after someone fills in the
|
||||
// gadget/sandbox form!
|
||||
int moduleId = 0;
|
||||
if (vreq.getSession() != null
|
||||
&& vreq.getSession().getAttribute(OPENSOCIAL_GADGETS) != null) {
|
||||
String openSocialGadgetURLS = (String) vreq.getSession()
|
||||
.getAttribute(OPENSOCIAL_GADGETS);
|
||||
String[] urls = openSocialGadgetURLS.split(System.getProperty("line.separator"));
|
||||
for (String openSocialGadgetURL : urls) {
|
||||
if (openSocialGadgetURL.length() == 0)
|
||||
continue;
|
||||
int appId = 0; // if URL matches one in the DB, use DB provided
|
||||
// appId, otherwise generate one
|
||||
String gadgetFileName = getGadgetFileNameFromURL(openSocialGadgetURL);
|
||||
String name = gadgetFileName;
|
||||
List<String> channels = new ArrayList<String>();
|
||||
boolean unknownGadget = true;
|
||||
if (dbApps.containsKey(gadgetFileName)) {
|
||||
appId = dbApps.get(gadgetFileName).getAppId();
|
||||
name = dbApps.get(gadgetFileName).getName();
|
||||
channels = dbApps.get(gadgetFileName).getChannels();
|
||||
unknownGadget = false;
|
||||
} else {
|
||||
appId = openSocialGadgetURL.hashCode();
|
||||
}
|
||||
// if they asked for a specific one, only let it in
|
||||
if (requestAppId != null
|
||||
&& Integer.getInteger(requestAppId) != appId) {
|
||||
continue;
|
||||
}
|
||||
GadgetSpec gadget = new GadgetSpec(appId, name,
|
||||
openSocialGadgetURL, channels, unknownGadget, dataSource);
|
||||
// only add ones that are visible in this context!
|
||||
if (unknownGadget
|
||||
|| gadget.show(viewerId, ownerId, pageName, dataSource)) {
|
||||
String securityToken = socketSendReceive(viewerId, ownerId,
|
||||
"" + gadget.getAppId());
|
||||
gadgets.add(new PreparedGadget(gadget, this, moduleId++,
|
||||
securityToken));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if no manual one were added, use the ones from the DB
|
||||
if (gadgets.size() == 0) {
|
||||
// Load DB gadgets
|
||||
if (gadgetSandbox) {
|
||||
officialApps = dbApps;
|
||||
}
|
||||
for (GadgetSpec spec : officialApps.values()) {
|
||||
GadgetSpec gadget = new GadgetSpec(spec.getAppId(),
|
||||
spec.getName(), spec.getGadgetURL(),
|
||||
spec.getChannels(), false, dataSource);
|
||||
// only add ones that are visible in this context!
|
||||
if (gadgetSandbox
|
||||
|| gadget.show(viewerId, ownerId, pageName, dataSource)) {
|
||||
String securityToken = socketSendReceive(viewerId, ownerId,
|
||||
"" + gadget.getAppId());
|
||||
gadgets.add(new PreparedGadget(gadget, this, moduleId++,
|
||||
securityToken));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort the gadgets
|
||||
Collections.sort(gadgets);
|
||||
}
|
||||
|
||||
private String figureOwnerId(VitroRequest vreq) {
|
||||
IndividualRequestAnalyzer requestAnalyzer = new IndividualRequestAnalyzer(vreq,
|
||||
new IndividualRequestAnalysisContextImpl(vreq));
|
||||
IndividualRequestInfo requestInfo = requestAnalyzer.analyze();
|
||||
Individual owner = requestInfo.getIndividual();
|
||||
return owner != null ? owner.getURI() : null;
|
||||
}
|
||||
|
||||
private String getGadgetFileNameFromURL(String url) {
|
||||
String[] urlbits = url.split("/");
|
||||
return urlbits[urlbits.length - 1];
|
||||
}
|
||||
|
||||
public boolean isDebug() {
|
||||
return isDebug;
|
||||
}
|
||||
|
||||
public boolean noCache() {
|
||||
return noCache;
|
||||
}
|
||||
|
||||
public String getOwnerId() {
|
||||
return ownerId;
|
||||
}
|
||||
|
||||
public boolean hasGadgetListeningTo(String channel) {
|
||||
for (PreparedGadget gadget : getVisibleGadgets()) {
|
||||
if (gadget.getGadgetSpec().listensTo(channel)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static List<String> getOpenSocialId(List<Individual> individuals) {
|
||||
List<String> personIds = new ArrayList<String>();
|
||||
for (Individual ind : individuals) {
|
||||
personIds.add(ind.getURI());
|
||||
}
|
||||
return personIds;
|
||||
}
|
||||
|
||||
// JSON Helper Functions
|
||||
public static String buildJSONPersonIds(List<String> personIds,
|
||||
String message) throws JSONException {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("message", message);
|
||||
json.put("personIds", personIds);
|
||||
return json.toString();
|
||||
}
|
||||
|
||||
public static String buildJSONPersonIds(String personId, String message) throws JSONException {
|
||||
List<String> personIds = new ArrayList<String>();
|
||||
personIds.add(personId);
|
||||
return buildJSONPersonIds(personIds, message);
|
||||
}
|
||||
|
||||
public static String buildJSONPersonIds(Individual ind, String message) throws JSONException {
|
||||
List<String> personIds = new ArrayList<String>();
|
||||
personIds.add(ind.getURI());
|
||||
return buildJSONPersonIds(personIds, message);
|
||||
}
|
||||
/****
|
||||
* public static String BuildJSONPubMedIds(Person person) { List<Int32>
|
||||
* pubIds = new List<Int32>(); foreach (Publication pub in
|
||||
* person.PublicationList) { foreach (PublicationSource pubSource in
|
||||
* pub.PublicationSourceList) { if ("PubMed".Equals(pubSource.Name)) {
|
||||
* pubIds.Add(Int32.Parse(pubSource.ID)); } } } Dictionary<string, Object>
|
||||
* foundPubs = new Dictionary<string, object>(); foundPubs.Add("pubIds",
|
||||
* pubIds); foundPubs.Add("message", "PubMedIDs for " +
|
||||
* person.Name.FullName); JavaScriptSerializer serializer = new
|
||||
* JavaScriptSerializer(); return serializer.Serialize(foundPubs); }
|
||||
***/
|
||||
|
||||
public void setPubsubData(String key, String value) {
|
||||
if (pubsubdata.containsKey(key)) {
|
||||
pubsubdata.remove(key);
|
||||
}
|
||||
if (value != null && !value.isEmpty()) {
|
||||
pubsubdata.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, String> getPubsubData() {
|
||||
return pubsubdata;
|
||||
}
|
||||
|
||||
public void removePubsubGadgetsWithoutData() {
|
||||
// if any visible gadgets depend on pubsub data that isn't present,
|
||||
// throw them out
|
||||
List<PreparedGadget> removedGadgets = new ArrayList<PreparedGadget>();
|
||||
for (PreparedGadget gadget : gadgets) {
|
||||
for (String channel : gadget.getGadgetSpec().getChannels()) {
|
||||
if (!pubsubdata.containsKey(channel)) {
|
||||
removedGadgets.add(gadget);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (PreparedGadget gadget : removedGadgets) {
|
||||
gadgets.remove(gadget);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeGadget(String name) {
|
||||
// if any visible gadgets depend on pubsub data that isn't present,
|
||||
// throw them out
|
||||
PreparedGadget gadgetToRemove = null;
|
||||
for (PreparedGadget gadget : gadgets) {
|
||||
if (name.equals(gadget.getName())) {
|
||||
gadgetToRemove = gadget;
|
||||
break;
|
||||
}
|
||||
}
|
||||
gadgets.remove(gadgetToRemove);
|
||||
}
|
||||
|
||||
public String getPageName() {
|
||||
return pageName;
|
||||
}
|
||||
|
||||
public String getIdToUrlMapJavascript() {
|
||||
String retval = "var idToUrlMap = {";
|
||||
for (PreparedGadget gadget : gadgets) {
|
||||
// retval += gadget.GetAppId() + ":'" + gadget.GetGadgetURL() +
|
||||
// "', ";
|
||||
retval += "'remote_iframe_" + gadget.getAppId() + "':'"
|
||||
+ gadget.getGadgetURL() + "', ";
|
||||
}
|
||||
return retval.substring(0, retval.length() - 2) + "};";
|
||||
}
|
||||
|
||||
public boolean isVisible() {
|
||||
// always have turned on for ProfileDetails.aspx because we want to
|
||||
// generate the "profile was viewed" in Javascript (bot proof)
|
||||
// regardless of any gadgets being visible, and we need this to be True
|
||||
// for the shindig javascript libraries to load
|
||||
return (configuration.getProperty(SHINDIG_URL_PROP) != null
|
||||
&& (getVisibleGadgets().size() > 0) || getPageName().equals(
|
||||
"/display"));
|
||||
}
|
||||
|
||||
public List<PreparedGadget> getVisibleGadgets() {
|
||||
return gadgets;
|
||||
}
|
||||
|
||||
public void postActivity(int userId, String title) throws SQLException {
|
||||
postActivity(userId, title, null, null, null);
|
||||
}
|
||||
|
||||
public void postActivity(int userId, String title, String body) throws SQLException {
|
||||
postActivity(userId, title, body, null, null);
|
||||
}
|
||||
|
||||
public void postActivity(int userId, String title, String body,
|
||||
String xtraId1Type, String xtraId1Value) throws SQLException {
|
||||
Connection conn = null;
|
||||
Statement stmt = null;
|
||||
String sqlCommand = "INSERT INTO shindig_activity (userId, activity, xtraId1Type, xtraId1Value) VALUES ('"
|
||||
+ userId + "','<activity xmlns=\"http://ns.opensocial.org/2008/opensocial\"><postedTime>"
|
||||
+ System.currentTimeMillis() + "</postedTime><title>" + title + "</title>"
|
||||
+ (body != null ? "<body>" + body + "</body>" : "") + "</activity>','"
|
||||
+ xtraId1Type + "','" + xtraId1Value + "');";
|
||||
try {
|
||||
conn = dataSource.getConnection();
|
||||
stmt = conn.createStatement();
|
||||
stmt.executeUpdate(sqlCommand);
|
||||
} finally {
|
||||
try {
|
||||
if (stmt != null) {
|
||||
stmt.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
if (conn != null) {
|
||||
conn.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String socketSendReceive(String viewer, String owner, String gadget)
|
||||
throws IOException {
|
||||
// These keys need to match what you see in
|
||||
// edu.ucsf.orng.shindig.service.SecureTokenGeneratorService in
|
||||
// Shindig
|
||||
String[] tokenService = configuration.getProperty(
|
||||
"OpenSocial.tokenService").split(":");
|
||||
String request = "c=default" + (viewer != null ? "&v=" + URLEncoder.encode(viewer, "UTF-8") : "") +
|
||||
(owner != null ? "&o=" + URLEncoder.encode(owner, "UTF-8") : "") + "&g=" + gadget + "\r\n";
|
||||
|
||||
// Create a socket connection with the specified server and port.
|
||||
Socket s = new Socket(tokenService[0],
|
||||
Integer.parseInt(tokenService[1]));
|
||||
|
||||
// Send request to the server.
|
||||
s.getOutputStream().write(request.getBytes());
|
||||
|
||||
// Receive the encoded content.
|
||||
int bytes = 0;
|
||||
String page = "";
|
||||
byte[] bytesReceived = new byte[256];
|
||||
|
||||
// The following will block until the page is transmitted.
|
||||
while ((bytes = s.getInputStream().read(bytesReceived)) > 0) {
|
||||
page += new String(bytesReceived, 0, bytes);
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
public String getContainerJavascriptSrc() {
|
||||
return configuration.getProperty(SHINDIG_URL_PROP)
|
||||
+ "/gadgets/js/core:dynamic-height:osapi:pubsub:rpc:views:shindig-container.js?c=1"
|
||||
+ (isDebug ? "&debug=1" : "");
|
||||
}
|
||||
|
||||
public String getGadgetJavascript() {
|
||||
String lineSeparator = System.getProperty("line.separator");
|
||||
String gadgetScriptText = lineSeparator
|
||||
+ "var my = {};"
|
||||
+ lineSeparator
|
||||
+ "my.gadgetSpec = function(appId, name, url, secureToken, view, closed_width, open_width, start_closed, chrome_id, visible_scope) {"
|
||||
+ lineSeparator + "this.appId = appId;" + lineSeparator
|
||||
+ "this.name = name;" + lineSeparator + "this.url = url;"
|
||||
+ lineSeparator + "this.secureToken = secureToken;"
|
||||
+ lineSeparator + "this.view = view || 'default';"
|
||||
+ lineSeparator + "this.closed_width = closed_width;"
|
||||
+ lineSeparator + "this.open_width = open_width;"
|
||||
+ lineSeparator + "this.start_closed = start_closed;"
|
||||
+ lineSeparator + "this.chrome_id = chrome_id;" + lineSeparator
|
||||
+ "this.visible_scope = visible_scope;" + lineSeparator + "};"
|
||||
+ lineSeparator + "my.pubsubData = {};" + lineSeparator;
|
||||
for (String key : getPubsubData().keySet()) {
|
||||
gadgetScriptText += "my.pubsubData['" + key + "'] = '"
|
||||
+ getPubsubData().get(key) + "';" + lineSeparator;
|
||||
}
|
||||
gadgetScriptText += "my.openSocialURL = '"
|
||||
+ configuration.getProperty(SHINDIG_URL_PROP) + "';"
|
||||
+ lineSeparator + "my.debug = " + (isDebug() ? "1" : "0") + ";"
|
||||
+ lineSeparator + "my.noCache = " + (noCache() ? "1" : "0")
|
||||
+ ";" + lineSeparator + "my.gadgets = [";
|
||||
for (PreparedGadget gadget : getVisibleGadgets()) {
|
||||
gadgetScriptText += "new my.gadgetSpec(" + gadget.getAppId() + ",'"
|
||||
+ gadget.getName() + "','" + gadget.getGadgetURL() + "','"
|
||||
+ gadget.getSecurityToken() + "','" + gadget.getView()
|
||||
+ "'," + gadget.getClosedWidth() + ","
|
||||
+ gadget.getOpenWidth() + ","
|
||||
+ (gadget.getStartClosed() ? "1" : "0") + ",'"
|
||||
+ gadget.getChromeId() + "','"
|
||||
+ gadget.getGadgetSpec().getVisibleScope() + "'), ";
|
||||
}
|
||||
gadgetScriptText = gadgetScriptText.substring(0,
|
||||
gadgetScriptText.length() - 2)
|
||||
+ "];"
|
||||
+ lineSeparator;
|
||||
|
||||
return gadgetScriptText;
|
||||
}
|
||||
}
|
123
webapp/src/edu/ucsf/vitro/opensocial/PreparedGadget.java
Normal file
123
webapp/src/edu/ucsf/vitro/opensocial/PreparedGadget.java
Normal file
|
@ -0,0 +1,123 @@
|
|||
package edu.ucsf.vitro.opensocial;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
|
||||
public class PreparedGadget implements Comparable<PreparedGadget> {
|
||||
private GadgetSpec gadgetSpec;
|
||||
private OpenSocialManager helper;
|
||||
private int moduleId;
|
||||
private String securityToken;
|
||||
|
||||
public PreparedGadget(GadgetSpec gadgetSpec, OpenSocialManager helper,
|
||||
int moduleId, String securityToken) {
|
||||
this.gadgetSpec = gadgetSpec;
|
||||
this.helper = helper;
|
||||
this.moduleId = moduleId;
|
||||
this.securityToken = securityToken;
|
||||
}
|
||||
|
||||
public int compareTo(PreparedGadget other) {
|
||||
GadgetViewRequirements gvr1 = this.getGadgetViewRequirements();
|
||||
GadgetViewRequirements gvr2 = other.getGadgetViewRequirements();
|
||||
return ("" + this.getView() + (gvr1 != null ? gvr1.getDisplayOrder()
|
||||
: Integer.MAX_VALUE)).compareTo("" + other.getView()
|
||||
+ (gvr2 != null ? gvr2.getDisplayOrder() : Integer.MAX_VALUE));
|
||||
}
|
||||
|
||||
public GadgetSpec getGadgetSpec() {
|
||||
return gadgetSpec;
|
||||
}
|
||||
|
||||
public String getSecurityToken() {
|
||||
return securityToken;
|
||||
}
|
||||
|
||||
public int getAppId() {
|
||||
return gadgetSpec.getAppId();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return gadgetSpec.getName();
|
||||
}
|
||||
|
||||
public int getModuleId() {
|
||||
return moduleId;
|
||||
}
|
||||
|
||||
public String getGadgetURL() {
|
||||
return gadgetSpec.getGadgetURL();
|
||||
}
|
||||
|
||||
GadgetViewRequirements getGadgetViewRequirements() {
|
||||
return gadgetSpec.getGadgetViewRequirements(helper.getPageName());
|
||||
}
|
||||
|
||||
public String getView() {
|
||||
GadgetViewRequirements reqs = getGadgetViewRequirements();
|
||||
if (reqs != null) {
|
||||
return reqs.getView();
|
||||
}
|
||||
// default behavior that will get invoked when there is no reqs. Useful
|
||||
// for sandbox gadgets
|
||||
else if (helper.getPageName().equals("individual-EDIT-MODE")) {
|
||||
return "home";
|
||||
} else if (helper.getPageName().equals("individual")) {
|
||||
return "profile";
|
||||
} else if (helper.getPageName().equals("gadgetDetails")) {
|
||||
return "canvas";
|
||||
} else if (gadgetSpec.getGadgetURL().contains("Tool")) {
|
||||
return "small";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public int getOpenWidth() {
|
||||
GadgetViewRequirements reqs = getGadgetViewRequirements();
|
||||
return reqs != null ? reqs.getOpenWidth() : 0;
|
||||
}
|
||||
|
||||
public int getClosedWidth() {
|
||||
GadgetViewRequirements reqs = getGadgetViewRequirements();
|
||||
return reqs != null ? reqs.getClosedWidth() : 0;
|
||||
}
|
||||
|
||||
public boolean getStartClosed() {
|
||||
GadgetViewRequirements reqs = getGadgetViewRequirements();
|
||||
// if the page specific reqs are present, honor those. Otherwise defaut
|
||||
// to true for regular gadgets, false for sandbox gadgets
|
||||
return reqs != null ? reqs.getStartClosed() : !gadgetSpec.fromSandbox();
|
||||
}
|
||||
|
||||
public String getChromeId() {
|
||||
GadgetViewRequirements reqs = getGadgetViewRequirements();
|
||||
if (reqs != null) {
|
||||
return reqs.getChromeId();
|
||||
}
|
||||
// default behavior that will get invoked when there is no reqs. Useful
|
||||
// for sandbox gadgets
|
||||
else if (gadgetSpec.getGadgetURL().contains("Tool")) {
|
||||
return "gadgets-tools";
|
||||
} else if (helper.getPageName().equals("individual-EDIT-MODE")) {
|
||||
return "gadgets-edit";
|
||||
} else if (helper.getPageName().equals("individual")) {
|
||||
return "gadgets-view";
|
||||
} else if (helper.getPageName().equals("gadgetDetails")) {
|
||||
return "gadgets-detail";
|
||||
} else if (helper.getPageName().equals("search")) {
|
||||
return "gadgets-search";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String getCanvasURL() throws UnsupportedEncodingException {
|
||||
return "~/gadget?appId=" + getAppId() + "&Person="
|
||||
+ URLEncoder.encode(helper.getOwnerId(), "UTF-8");
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "" + this.moduleId + ", (" + this.gadgetSpec.toString() + ")";
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue