1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.jetspeed.modules.actions.portlets.browser;
18
19
20 import org.apache.velocity.context.Context;
21
22
23 import java.sql.Connection;
24 import java.sql.PreparedStatement;
25 import java.sql.ResultSet;
26 import java.sql.ResultSetMetaData;
27 import java.sql.SQLException;
28 import java.sql.Types;
29 import java.util.Iterator;
30 import java.util.List;
31 import java.util.ArrayList;
32 import java.util.Vector;
33 import java.util.StringTokenizer;
34
35
36 import org.apache.turbine.util.RunData;
37 import org.apache.turbine.util.StringUtils;
38
39 import org.apache.torque.Torque;
40
41
42 import org.apache.jetspeed.services.logging.JetspeedLogFactoryService;
43 import org.apache.jetspeed.services.logging.JetspeedLogger;
44 import org.apache.jetspeed.services.resources.JetspeedResources;
45
46
47 import org.apache.jetspeed.modules.actions.portlets.VelocityPortletAction;
48 import org.apache.jetspeed.modules.actions.portlets.browser.ActionParameter;
49 import org.apache.jetspeed.modules.actions.portlets.browser.BrowserQuery;
50 import org.apache.jetspeed.portal.portlets.VelocityPortlet;
51 import org.apache.jetspeed.portal.portlets.browser.DatabaseBrowserIterator;
52 import org.apache.jetspeed.portal.portlets.browser.BrowserIterator;
53 import org.apache.jetspeed.util.PortletSessionState;
54 import org.apache.jetspeed.util.PortletConfigState;
55
56
57
58 /***
59 * This action sets up the template context for retrieving paged data from the resultSet
60 * according to the quey speciified by the user.
61 *
62 * @author <a href="mailto:taylor@apache.org">David Sean Taylor</a>
63 * @version $Id: DatabaseBrowserAction.java,v 1.34 2004/02/23 02:51:19 jford Exp $
64 */
65 public class DatabaseBrowserAction extends VelocityPortletAction implements BrowserQuery
66 {
67 protected static final String SQL = "sql";
68 protected static final String POOLNAME = "poolname";
69 protected static final String START = "start";
70 protected static final String CUSTOMIZE_TEMPLATE = "customizeTemplate";
71 protected static final String WINDOW_SIZE = "windowSize";
72
73 protected static final String USER_OBJECT_NAMES = "user-object-names";
74 protected static final String USER_OBJECT_TYPES = "user-object-types";
75 protected static final String USER_OBJECTS = "user-objects";
76
77 protected static final String SQL_PARAM_PREFIX = "sqlparam";
78
79 protected static final String LINKS_READ = "linksRead";
80 protected static final String ROW_LINK = "rowLinks";
81 protected static final String TABLE_LINK = "tableLinks";
82 protected static final String ROW_LINK_IDS = "row-link-ids";
83 protected static final String ROW_LINK_TYPES = "row-link-types";
84 protected static final String ROW_LINK_TARGETS = "row-link-targets";
85 protected static final String TABLE_LINK_IDS = "table-link-ids";
86 protected static final String TABLE_LINK_TYPES = "table-link-types";
87 protected static final String TABLE_LINK_TARGETS = "table-link-targets";
88 protected static final String BROWSER_TABLE_SIZE = "tableSize";
89 protected static final String DATABASE_BROWSER_ACTION_KEY = "database_browser_action_key";
90 protected static final String BROWSER_ITERATOR = "table";
91 protected static final String BROWSER_TITLE_ITERATOR = "title";
92 protected static final String NEXT = "next";
93 protected static final String PREVIOUS = "prev";
94 protected static final String VELOCITY_NULL_ENTRY = "-";
95
96 protected static final String PEID = "js_peid";
97 protected static final String SORT_COLUMN_NAME = "js_dbcolumn";
98
99 protected List sqlParameters = new Vector();
100
101 /***
102 * Static initialization of the logger for this class
103 */
104 private static final JetspeedLogger logger = JetspeedLogFactoryService.getLogger(DatabaseBrowserAction.class.getName());
105
106 /***
107 * Build the maximized state content for this portlet. (Same as normal state).
108 *
109 * @param portlet The velocity-based portlet that is being built.
110 * @param context The velocity context for this request.
111 * @param rundata The turbine rundata context for this request.
112 */
113 protected void buildMaximizedContext( VelocityPortlet portlet,
114 Context context,
115 RunData rundata )
116 {
117 buildNormalContext( portlet, context, rundata);
118 }
119
120 /***
121 * Subclasses should override this method if they wish to
122 * provide their own customization behavior.
123 * Default is to use Portal base customizer action
124 */
125 protected void buildConfigureContext( VelocityPortlet portlet,
126 Context context,
127 RunData rundata )
128 {
129 try
130 {
131 super.buildConfigureContext( portlet, context, rundata);
132 }
133 catch (Exception ex)
134 {
135 logger.error("Exception", ex);
136 }
137 context.put(SQL, getQueryString(portlet, rundata, context));
138 context.put(WINDOW_SIZE, getParameterUsingFallback(portlet, rundata, WINDOW_SIZE, null));
139 setTemplate(rundata, getParameterUsingFallback(portlet, rundata, CUSTOMIZE_TEMPLATE, null));
140 }
141
142 /***
143 * Build the normal state content for this portlet.
144 *
145 * @param portlet The velocity-based portlet that is being built.
146 * @param context The velocity context for this request.
147 * @param rundata The turbine rundata context for this request.
148 */
149 protected void buildNormalContext( VelocityPortlet portlet,
150 Context context,
151 RunData rundata )
152 {
153 int resultSetSize, next, prev, windowSize;
154
155 BrowserIterator iterator = getDatabaseBrowserIterator(portlet, rundata);
156 String sortColName = getRequestParameter(portlet, rundata, SORT_COLUMN_NAME);
157 int start = getStartVariable(portlet, rundata, START, sortColName, iterator);
158
159 windowSize = Integer.parseInt((String)getParameterUsingFallback(portlet, rundata, WINDOW_SIZE, "10"));
160 next = start + windowSize;
161 prev = start - windowSize;
162
163 try
164 {
165 if(iterator == null || PortletSessionState.getPortletConfigChanged(portlet, rundata))
166 {
167 String sql = getQueryString(portlet, rundata, context);
168
169 if(sql != null )
170 {
171 readUserParameters(portlet,rundata,context);
172 getRows(portlet, rundata, sql, windowSize);
173 iterator = getDatabaseBrowserIterator(portlet, rundata);
174 }
175 else
176 {
177 logger.info("The sql query is null, hence not generating the result set.");
178 }
179 }
180 else
181 {
182 if(sortColName != null)
183 {
184 iterator.sort(sortColName);
185 }
186 iterator.setTop(start);
187 }
188
189 readLinkParameters(portlet, rundata, context);
190
191 if(iterator != null)
192 {
193 resultSetSize = iterator.getResultSetSize();
194
195 if(next < resultSetSize)
196 {
197 context.put(NEXT, String.valueOf(next));
198 }
199 if(prev <= resultSetSize && prev >=0 )
200 {
201 context.put(PREVIOUS, String.valueOf(prev));
202 }
203
204 context.put(BROWSER_ITERATOR, iterator);
205 context.put(BROWSER_TITLE_ITERATOR, iterator.getResultSetTitleList());
206 context.put(BROWSER_TABLE_SIZE, new Integer(resultSetSize));
207
208
209
210
211
212
213
214
215 }
216
217 }
218 catch (Exception e)
219 {
220
221 logger.error("Exception", e);
222
223 rundata.setMessage("Error in Jetspeed Database Browser: " + e.toString());
224 rundata.setStackTrace(StringUtils.stackTrace(e), e);
225 rundata.setScreenTemplate(JetspeedResources.getString("template.error","Error"));
226 }
227 }
228
229 /***
230 * This method is called when the user configures any of the parameters.
231 * @param data The turbine rundata context for this request.
232 * @param context The velocity context for this request.
233 */
234 public void doUpdate(RunData rundata, Context context)
235 {
236 VelocityPortlet portlet = (VelocityPortlet)context.get("portlet");
237 String sql = getRequestParameter(portlet, rundata, SQL);
238 String pageSize = getRequestParameter(portlet, rundata, WINDOW_SIZE);
239
240 if (sql!=null)
241 {
242 setParameterToPSML( portlet, rundata, SQL, sql);
243 context.put(SQL, sql);
244 clearDatabaseBrowserIterator(portlet, rundata);
245
246 }
247 if(pageSize!=null)
248 {
249 setParameterToPSML( portlet, rundata, WINDOW_SIZE, pageSize);
250 context.put(WINDOW_SIZE, pageSize);
251
252 }
253
254
255
256 buildNormalContext( portlet, context, rundata);
257 }
258
259 /***
260 * This method is called when the user hits refresh to refetch the result set.
261 * @param data The turbine rundata context for this request.
262 * @param context The velocity context for this request.
263 */
264 public void doRefresh(RunData rundata, Context context)
265 {
266 VelocityPortlet portlet = (VelocityPortlet)context.get("portlet");
267 if(isMyRequest(portlet,rundata))
268 {
269 clearDatabaseBrowserIterator(portlet, rundata);
270 }
271 buildNormalContext(portlet, context, rundata);
272 }
273
274
275
276
277 public boolean filter(List row, RunData rundata)
278 {
279 return false;
280 }
281
282 /***
283 * Execute the sql statement as specified by the user or the default, and store the
284 * resultSet in a vector.
285 *
286 * @param sql The sql statement to be executed.
287 * @param data The turbine rundata context for this request.
288 */
289 protected void getRows(VelocityPortlet portlet, RunData rundata, String sql,
290 int windowSize) throws Exception
291 {
292 List resultSetList = new ArrayList();
293 List resultSetTitleList = new ArrayList();
294 List resultSetTypeList = new ArrayList();
295 Connection con = null;
296 PreparedStatement selectStmt = null;
297 ResultSet rs = null;
298 try
299 {
300 String poolname = getParameterUsingFallback(portlet,rundata,POOLNAME,null);
301 if (poolname==null || poolname.length()==0)
302 {
303 con = Torque.getConnection();
304 }
305 else
306 {
307 con = Torque.getConnection(poolname);
308 }
309 selectStmt = con.prepareStatement(sql);
310
311 readSqlParameters(portlet, rundata);
312 Iterator it = sqlParameters.iterator();
313 int ix = 0;
314 while (it.hasNext())
315 {
316 ix++;
317 Object object = it.next();
318 selectStmt.setObject(ix, object);
319 }
320 rs = selectStmt.executeQuery();
321 ResultSetMetaData rsmd = rs.getMetaData();
322 int columnNum = rsmd.getColumnCount();
323
324
325
326
327 List userObjList = (List)getParameterFromTemp(portlet, rundata, USER_OBJECTS);
328 int userObjListSize = 0;
329 if (userObjList != null)
330 {
331 userObjListSize = userObjList.size();
332 }
333
334
335
336
337
338
339 boolean[] columnDisplayed = new boolean [columnNum + userObjListSize];
340
341
342
343
344
345 for(int i = 1; i <= columnNum; i++)
346 {
347 int type = rsmd.getColumnType(i);
348 if( !((type == Types.BLOB) || (type == Types.CLOB) ||
349 (type == Types.BINARY) || (type == Types.LONGVARBINARY) ||
350 (type == Types.VARBINARY)) )
351 {
352 resultSetTitleList.add(rsmd.getColumnName(i));
353 resultSetTypeList.add(String.valueOf(type));
354 columnDisplayed[i-1] = true;
355 }
356 else
357 {
358 columnDisplayed[i-1] = false;
359 }
360 }
361
362 for (int i = columnNum; i < columnNum + userObjListSize; i++)
363 {
364 ActionParameter usrObj = (ActionParameter)userObjList.get(i - columnNum);
365 resultSetTitleList.add(usrObj.getName());
366 resultSetTypeList.add(usrObj.getType());
367 columnDisplayed[i] = true;
368
369 }
370
371
372
373 int index = 0;
374 while(rs.next())
375 {
376 List row = new ArrayList(columnNum);
377
378 for(int i = 1; i <= columnNum; i++)
379 {
380 if( columnDisplayed[i-1] )
381 {
382 Object obj = rs.getObject(i);
383 if (obj == null)
384 {
385 obj = VELOCITY_NULL_ENTRY;
386 }
387 row.add(obj);
388 }
389 }
390 for (int i = columnNum; i < columnNum + userObjListSize; i++)
391 {
392 ActionParameter usrObj = (ActionParameter)userObjList.get(i - columnNum);
393 if( columnDisplayed[i] )
394 {
395 Class c = Class.forName(usrObj.getType());
396 row.add(c.newInstance());
397 populate(index, i, row);
398 }
399 }
400
401 if (filter(row, rundata))
402 {
403 continue;
404 }
405
406 resultSetList.add(row);
407 index++;
408 }
409 BrowserIterator iterator =
410 new DatabaseBrowserIterator( resultSetList, resultSetTitleList,
411 resultSetTypeList, windowSize);
412 setDatabaseBrowserIterator(portlet, rundata, iterator);
413
414 }
415 catch (SQLException e)
416 {
417 throw e;
418 }
419 finally
420 {
421 try
422 {
423 if (null != selectStmt)
424 selectStmt.close();
425 if (null != rs)
426 rs.close();
427 if (null != con)
428 Torque.closeConnection(con);
429
430 }
431 catch (Exception e)
432 {
433 throw e;
434 }
435 }
436
437 }
438
439 /***
440 * Centralizes the calls to runData.getUser.getTemp() - to retrieve
441 * the DatabaseBrowserIterator.
442 *
443 * @param data The turbine rundata context for this request.
444 *
445 */
446 protected BrowserIterator getDatabaseBrowserIterator(VelocityPortlet portlet,
447 RunData rundata)
448 {
449
450
451 BrowserIterator iterator =
452 (BrowserIterator)getParameterFromTemp(portlet, rundata, DATABASE_BROWSER_ACTION_KEY);
453 return iterator;
454 }
455
456 /***
457 * Centralizes the calls to runData.getUser.setTemp() - to set
458 * the DatabaseBrowserIterator.
459 *
460 * @param data The turbine rundata context for this request.
461 * @param iterator.
462 *
463 */
464 protected void setDatabaseBrowserIterator(VelocityPortlet portlet,
465 RunData rundata,
466 BrowserIterator iterator)
467 {
468 setParameterToTemp(portlet, rundata, DATABASE_BROWSER_ACTION_KEY, iterator);
469 }
470
471 /***
472 * Centralizes the calls to runData.getUser.removeTemp() - to clear
473 * the DatabaseBrowserIterator from the temp storage.
474 *
475 * @param data The turbine rundata context for this request.
476 *
477 */
478 protected void clearDatabaseBrowserIterator(VelocityPortlet portlet, RunData rundata)
479 {
480 clearParameterFromTemp(portlet, rundata, DATABASE_BROWSER_ACTION_KEY);
481 }
482
483 /***
484 * This method returns the query to be executed to get the results which will
485 * be opened in the browser.
486 *
487 */
488 public String getQueryString(RunData rundata, Context context)
489 {
490 return null;
491 }
492 /***
493 * This method returns the sql from the getQuery method which can be
494 * overwritten according to the needs of the application. If the getQuery()
495 * returns null, then it gets the value from the psml file. If the psml value is null
496 * then it returns the value from the xreg file.
497 *
498 */
499 protected String getQueryString(VelocityPortlet portlet, RunData rundata, Context context)
500 {
501 String sql = getQueryString(rundata, context);
502 if( sql==null )
503 {
504 sql = getParameterUsingFallback(portlet, rundata, SQL, null);
505 }
506 return sql;
507 }
508 /***
509 *
510 */
511 protected void clearQueryString(VelocityPortlet portlet, RunData rundata)
512 {
513 clearParameterFromPSML(portlet, rundata, SQL);
514 }
515 /***
516 * to be used if sorting behavior to be overwritten
517 */
518 protected int getStartIndex()
519 {
520 return 0;
521 }
522 /***
523 *
524 */
525 protected String getParameterUsingFallback(VelocityPortlet portlet, RunData rundata,
526 String attrName, String attrDefValue)
527 {
528 return PortletConfigState.getParameter(portlet, rundata, attrName, attrDefValue);
529 }
530 /***
531 *
532 */
533 protected void clearParameterFromPSML(VelocityPortlet portlet, RunData rundata,
534 String attributeName)
535 {
536 PortletConfigState.clearInstanceParameter(portlet, rundata, attributeName);
537 }
538 /***
539 *
540 */
541 protected void setParameterToPSML(VelocityPortlet portlet, RunData rundata,
542 String attrName, String attrValue)
543 {
544 PortletConfigState.setInstanceParameter(portlet, rundata, attrName, attrValue);
545 }
546
547 /***
548 *
549 */
550 protected String getParameterFromPSML(VelocityPortlet portlet, RunData rundata,
551 String attrName, String attrDefValue)
552 {
553 return PortletConfigState.getInstanceParameter(portlet, rundata, attrName);
554
555 }
556
557 protected String getParameterFromRegistry(VelocityPortlet portlet,
558 String attrName,
559 String attrDefValue)
560 {
561 return PortletConfigState.getConfigParameter(portlet, attrName, attrDefValue);
562
563 }
564
565 /***
566 *
567 */
568 protected Object getParameterFromTemp(VelocityPortlet portlet, RunData rundata, String attrName)
569 {
570 return PortletSessionState.getAttribute(portlet, rundata, attrName);
571 }
572 /***
573 *
574 */
575 protected void setParameterToTemp(VelocityPortlet portlet, RunData rundata,
576 String attrName, Object attrValue)
577 {
578 PortletSessionState.setAttribute(portlet, rundata, attrName, attrValue);
579 }
580 /***
581 *
582 */
583 protected void clearParameterFromTemp(VelocityPortlet portlet, RunData rundata,
584 String attrName)
585 {
586 PortletSessionState.clearAttribute(portlet, rundata, attrName);
587 }
588
589 /***
590 *
591 */
592 protected boolean isMyRequest(VelocityPortlet portlet, RunData rundata)
593 {
594 String peId = portlet.getID();
595
596 if(peId != null && peId.equals(rundata.getParameters().getString(PEID)))
597 return true;
598 else
599 return false;
600 }
601 /***
602 *
603 */
604 protected String getRequestParameter(VelocityPortlet portlet, RunData rundata,
605 String attrName)
606 {
607 if(isMyRequest(portlet, rundata))
608 return rundata.getParameters().getString(attrName);
609 else
610 return null;
611 }
612 /***
613 *
614 */
615 protected int getStartVariable(VelocityPortlet portlet, RunData rundata,
616 String attrName, String sortColName,
617 BrowserIterator iterator)
618 {
619 int start = -1;
620
621
622 if( sortColName != null ) start = getStartIndex();
623
624 if( start < 0 )
625 {
626
627 String startStr = getRequestParameter(portlet, rundata, attrName);
628 if(startStr != null && startStr.length() > 0)
629 {
630 start = Integer.parseInt(startStr);
631 }
632 else if( start == -1 && iterator != null )
633 {
634 start = iterator.getTop();
635 }
636
637 if( start < 0 ) start = 0;
638 }
639 return start;
640
641 }
642
643 public void setSQLParameters(List parameters)
644 {
645 this.sqlParameters = parameters;
646 }
647
648 public List getSQLParameters()
649 {
650 return sqlParameters;
651 }
652
653 protected void readSqlParameters(VelocityPortlet portlet, RunData rundata)
654 {
655 List sqlParamList = null;
656
657 int i = 1;
658 while (true)
659 {
660 String param = getParameterUsingFallback(portlet, rundata, SQL_PARAM_PREFIX + i, null);
661 if (param == null)
662 {
663 break;
664 }
665 else
666 {
667 if (sqlParamList == null)
668 {
669 sqlParamList = new ArrayList();
670 }
671 sqlParamList.add(param);
672 }
673 i++;
674 }
675
676 if (sqlParamList != null)
677 {
678 setSQLParameters(sqlParamList);
679 }
680
681 }
682
683 protected void readUserParameters(VelocityPortlet portlet, RunData rundata, Context context)
684 {
685 List userObjectList;
686 Object userObjRead = getParameterFromTemp(portlet, rundata, USER_OBJECTS);
687 if ( userObjRead != null)
688 {
689 context.put(USER_OBJECTS, (List)userObjRead);
690
691 }
692 else
693 {
694 String userObjTypes= getParameterFromRegistry(portlet,USER_OBJECT_TYPES,null);
695 String userObjNames= getParameterFromRegistry(portlet,USER_OBJECT_NAMES,null);
696 if( userObjTypes != null && userObjTypes.length() > 0 )
697 {
698 userObjectList = new ArrayList();
699 int userObjectIndex = 0;
700 StringTokenizer tokenizer1 = new StringTokenizer(userObjNames, ",");
701 StringTokenizer tokenizer3 = new StringTokenizer(userObjTypes, ",");
702 while(tokenizer1.hasMoreTokens() && tokenizer3.hasMoreTokens())
703 {
704 userObjectList.add(userObjectIndex,
705 new ActionParameter(tokenizer1.nextToken(), null, tokenizer3.nextToken()));
706 userObjectIndex++;
707 }
708 context.put(USER_OBJECTS, userObjectList);
709 setParameterToTemp(portlet, rundata, USER_OBJECTS, userObjectList);
710
711 }
712 }
713 }
714
715 protected void readLinkParameters(VelocityPortlet portlet, RunData rundata, Context context)
716 {
717 List rowList, tableList;
718 Object linksRead = getParameterFromTemp(portlet, rundata, LINKS_READ);
719 if(linksRead != null && ((String)linksRead).equals(LINKS_READ))
720 {
721 Object tmp = getParameterFromTemp(portlet, rundata, ROW_LINK);
722 if(tmp != null)
723 {
724 context.put(ROW_LINK, (List)tmp);
725
726 }
727 tmp = getParameterFromTemp(portlet, rundata, TABLE_LINK);
728 if(tmp != null)
729 {
730 context.put(TABLE_LINK, (List)tmp);
731
732 }
733 }
734 else
735 {
736 String rowLinkIds= getParameterFromRegistry(portlet,ROW_LINK_IDS,null);
737 String rowLinkClasses= getParameterFromRegistry(portlet,ROW_LINK_TARGETS,null);
738 String rowLinkTypes= getParameterFromRegistry(portlet,ROW_LINK_TYPES,null);
739 if( rowLinkIds != null && rowLinkIds.length() > 0 )
740 {
741 rowList = new ArrayList();
742 int rowIndex = 0;
743 StringTokenizer tokenizer1 = new StringTokenizer(rowLinkIds, ",");
744 StringTokenizer tokenizer2 = new StringTokenizer(rowLinkClasses, ",");
745 StringTokenizer tokenizer3 = new StringTokenizer(rowLinkTypes, ",");
746 while(tokenizer1.hasMoreTokens() && tokenizer2.hasMoreTokens() && tokenizer3.hasMoreTokens())
747 {
748 rowList.add(rowIndex,
749 new ActionParameter(tokenizer1.nextToken(), tokenizer2.nextToken(), tokenizer3.nextToken()));
750 rowIndex++;
751 }
752 context.put(ROW_LINK, rowList);
753 setParameterToTemp(portlet, rundata, ROW_LINK, rowList);
754
755 }
756
757 String tableLinkIds= getParameterFromRegistry(portlet,TABLE_LINK_IDS,null);
758 String tableLinkClasses= getParameterFromRegistry(portlet,TABLE_LINK_TARGETS,null);
759 String tableLinkTypes= getParameterFromRegistry(portlet,TABLE_LINK_TYPES,null);
760 if( tableLinkIds != null && tableLinkIds.length() > 0 )
761 {
762 tableList = new ArrayList();
763 int tableIndex = 0;
764 StringTokenizer tokenizer1 = new StringTokenizer(tableLinkIds, ",");
765 StringTokenizer tokenizer2 = new StringTokenizer(tableLinkClasses, ",");
766 StringTokenizer tokenizer3 = new StringTokenizer(tableLinkTypes, ",");
767 while(tokenizer1.hasMoreTokens() && tokenizer2.hasMoreTokens() && tokenizer3.hasMoreTokens())
768 {
769 tableList.add(tableIndex,
770 new ActionParameter(tokenizer1.nextToken(), tokenizer2.nextToken(), tokenizer3.nextToken()));
771 tableIndex++;
772 }
773 context.put(TABLE_LINK, tableList);
774 setParameterToTemp(portlet, rundata, TABLE_LINK, tableList);
775
776 }
777 setParameterToTemp(portlet, rundata, LINKS_READ, LINKS_READ);
778 }
779
780 }
781
782 /***
783 * This method should be overwritten every time the user object needs to be
784 * populated with some user specific constraints. As an example if the user wanted
785 * to track the parent of an object based on some calculation per row, it could be
786 * done here.
787 *
788 */
789 public void populate(int rowIndex, int columnIndex, List row)
790 {
791 }
792
793
794 }