+package org.simantics.export.ui;\r
+\r
+import java.net.MalformedURLException;\r
+import java.net.URL;\r
+import java.util.ArrayList;\r
+import java.util.Collection;\r
+import java.util.Collections;\r
+import java.util.Comparator;\r
+import java.util.HashMap;\r
+import java.util.HashSet;\r
+import java.util.List;\r
+import java.util.Map;\r
+import java.util.Set;\r
+import java.util.TreeMap;\r
+\r
+import org.eclipse.jface.resource.ImageDescriptor;\r
+import org.eclipse.jface.resource.JFaceResources;\r
+import org.eclipse.jface.resource.LocalResourceManager;\r
+import org.eclipse.jface.wizard.WizardPage;\r
+import org.eclipse.nebula.widgets.grid.Grid;\r
+import org.eclipse.nebula.widgets.grid.GridColumn;\r
+import org.eclipse.nebula.widgets.grid.GridItem;\r
+import org.eclipse.swt.SWT;\r
+import org.eclipse.swt.events.SelectionAdapter;\r
+import org.eclipse.swt.events.SelectionEvent;\r
+import org.eclipse.swt.events.SelectionListener;\r
+import org.eclipse.swt.graphics.Color;\r
+import org.eclipse.swt.graphics.Image;\r
+import org.eclipse.swt.graphics.RGB;\r
+import org.eclipse.swt.widgets.Composite;\r
+import org.simantics.db.exception.DatabaseException;\r
+import org.simantics.export.core.ExportContext;\r
+import org.simantics.export.core.error.ExportException;\r
+import org.simantics.export.core.intf.ContentType;\r
+import org.simantics.export.core.intf.Discoverer;\r
+import org.simantics.export.core.intf.Exporter;\r
+import org.simantics.export.core.intf.Format;\r
+import org.simantics.export.core.intf.Publisher;\r
+import org.simantics.export.core.manager.Content;\r
+import org.simantics.export.core.manager.ExportWizardResult;\r
+import org.simantics.export.core.util.ExportQueries;\r
+import org.simantics.export.ui.util.ExportUIQueries;\r
+import org.simantics.utils.datastructures.MapList;\r
+import org.simantics.utils.datastructures.ToStringComparator;\r
+import org.simantics.utils.strings.AlphanumComparator;\r
+import org.simantics.utils.ui.workbench.StringMemento;\r
+\r
+/**\r
+ * Show wizard page where content and export format is selected.\r
+ *\r
+ * @author toni.kalajainen@semantum.fi\r
+ */\r
+public class ContentSelectionPage extends WizardPage {\r
+ \r
+ /** Key for preference setting that contains sub-mementos for each content URI. */\r
+ public static final String KEY_FORMAT_SELECTIONS = "org.simantics.modeling.ui.export.wizard.formatSelections";\r
+\r
+ // UI stuff\r
+ LocalResourceManager resourceManager;\r
+ Grid grid;\r
+ \r
+ // Initial data\r
+ ExportContext ctx; \r
+ // Hash-code for the initial data\r
+ String initialSelectionKey;\r
+ Set<Content> initialSelection = new HashSet<Content>();\r
+ \r
+ // Previous selections\r
+ StringMemento formatSelections;\r
+ \r
+ // Initializations\r
+ Collection<String> allModels, selectedModels;\r
+ ToStringComparator toStringComparator = new ToStringComparator();\r
+ MapList<ContentType, String> content; // ContentType -> Content[]\r
+ MapList<ContentType, Format> typeToFormatMap; // ContentType -> Format\r
+ MapList<String, ContentType> contentToTypeMap; // uri -> ContentType\r
+ Map<String, Map<String, String>> labels; // ContentType -> URI -> Label\r
+ MapList<String, String> modelContent; // ModelURI -> ContentURI\r
+ \r
+ // User selections\r
+ List<Content> contentSelection = new ArrayList<Content>();\r
+ \r
+ public ContentSelectionPage(ExportContext ctx) throws ExportException {\r
+ super("Select Content", "Select the PDF Pages and the attachments", null);\r
+ this.ctx = ctx;\r
+ \r
+ init();\r
+ }\r
+ \r
+ void init() throws ExportException {\r
+ try {\r
+ System.out.println("Found Content Types:");\r
+ for ( ContentType ct : ctx.eep.contentTypes() ) {\r
+ System.out.println(" "+ct);\r
+ }\r
+ System.out.println();\r
+ \r
+ System.out.println("Exporters:");\r
+ for ( Exporter ex : ctx.eep.exporters() ) {\r
+ System.out.println(" "+ex);\r
+ }\r
+ System.out.println();\r
+\r
+ System.out.println("Formats:");\r
+ for ( Format format : ctx.eep.formats() ) {\r
+ System.out.println(" "+format);\r
+ }\r
+ System.out.println();\r
+\r
+ System.out.println("Discoverers:");\r
+ for ( Discoverer discoverer : ctx.eep.discoverers() ) {\r
+ System.out.println(" "+discoverer);\r
+ }\r
+ System.out.println();\r
+ \r
+ System.out.println("Publishers:");\r
+ for ( Publisher publisher : ctx.eep.publishers() ) {\r
+ System.out.println(" "+publisher.id());\r
+ }\r
+ System.out.println();\r
+ \r
+ // Organize formats by content types - Filter out ContentTypes that don't have exporter and format.\r
+ System.out.println("Mapped ContentTypes to Exporters:");\r
+ typeToFormatMap = MapList.use( new TreeMap<ContentType, List<Format>>(toStringComparator) );\r
+ for ( ContentType ct : ctx.eep.contentTypes() ) {\r
+ for ( Exporter exp : ctx.eep.getExportersForContentType( ct.id() ) ) {\r
+ Format format = ctx.eep.getFormat( exp.formatId() );\r
+ if ( format==null ) continue;\r
+ System.out.println(" "+ct.id()+" -> "+format.fileext());\r
+ if (!typeToFormatMap.contains(ct, format)) typeToFormatMap.add(ct, format);\r
+ }\r
+ }\r
+ System.out.println();\r
+ \r
+ // Discover the models in the project\r
+ allModels = ctx.session.syncRequest( ExportUIQueries.models(ctx.project) );\r
+\r
+ // Calculate hash for the initial selection\r
+ int initialContentHash = 0x52f3a45;\r
+ for ( String content : ctx.selection ) {\r
+ initialContentHash = 13*initialContentHash + content.hashCode();\r
+ }\r
+ initialSelectionKey = "InitialSelection-"+initialContentHash;\r
+ String sel = ctx.store.get(initialSelectionKey, null);\r
+ if ( sel != null ) {\r
+ initialSelection = ExportWizardResult.parse(sel);\r
+ } else {\r
+ // First time wizard was called with this selection.\r
+ // Check in\r
+ for ( String contentUri : ctx.selection ) {\r
+ initialSelection.add( new Content(contentUri, null, "all", null, null, null) );\r
+ }\r
+ }\r
+ \r
+ // Choose the models from user interface selection\r
+ selectedModels = new ArrayList<String>();\r
+ StringBuilder modelsStr = new StringBuilder(); \r
+ for ( String content : ctx.selection ) {\r
+ for ( String model : allModels ) {\r
+ if ( content.equals(model) || content.startsWith(model + "/") ) {\r
+ if ( !selectedModels.contains( model ) ) {\r
+ selectedModels.add( model );\r
+ if ( modelsStr.length()>0 ) modelsStr.append(", ");\r
+ modelsStr.append( model );\r
+ }\r
+ }\r
+ }\r
+ }\r
+ // If user has nothing selected, choose active models\r
+ if ( selectedModels.isEmpty() ) selectedModels.addAll( ctx.activeModels );\r
+ // If there are no active models, choose all models\r
+ if ( selectedModels.isEmpty() ) selectedModels.addAll( allModels );\r
+ // UI labels\r
+ labels = new HashMap<String, Map<String, String>>(); \r
+ labels.put( "model", ctx.session.syncRequest( ExportQueries.labels( selectedModels ) ) ); // Should Model CT be used for labeling? \r
+ \r
+ // Discover contents\r
+ System.out.println("Discovering content: "+modelsStr);\r
+ content = MapList.use( new TreeMap<ContentType, List<String>>(toStringComparator) );\r
+ contentToTypeMap = MapList.use( new TreeMap<String, List<ContentType>>(toStringComparator) );\r
+ modelContent = MapList.use( new TreeMap<String, List<String>>() );\r
+ \r
+ for ( ContentType ct : typeToFormatMap.getKeys() ) {\r
+ System.out.println(" "+ct.label());\r
+ for ( Discoverer discoverer : ctx.eep.getDiscoverers( ct.id() )) {\r
+ System.out.println(" "+discoverer.toString());\r
+ \r
+ // Get content Uris\r
+ Collection<String> contents = discoverer.discoverContent(ctx, selectedModels);\r
+ List<String> contentUris = new ArrayList<String>( contents );\r
+\r
+ // Get UI Labels\r
+ Map<String, String> lbls = ct.getLabels(ctx, contentUris); \r
+ labels.put( ct.id(), lbls );\r
+ \r
+ // Sort content\r
+ IndirectComparator comp = new IndirectComparator();\r
+ comp.labels = lbls;\r
+ Collections.sort( contentUris, comp );\r
+ \r
+ for ( String contentId : contentUris ) {\r
+ content.add( ct, contentId );\r
+ contentToTypeMap.add(contentId, ct);\r
+ //modelContent.add(key)\r
+ System.out.println(" "+contentId);\r
+ }\r
+ \r
+ }\r
+ }\r
+ System.out.println();\r
+ \r
+ \r
+ } catch (DatabaseException e) {\r
+ throw new ExportException(e);\r
+ }\r
+\r
+ \r
+ }\r
+ \r
+ @Override\r
+ public void createControl(Composite parent) {\r
+ grid = new Grid(parent, SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL | SWT.MULTI);\r
+ grid.setHeaderVisible(true);\r
+ \r
+ resourceManager = new LocalResourceManager(JFaceResources.getResources(), grid);\r
+ Color contentTypeColor = resourceManager.createColor( new RGB(245, 245, 252) );\r
+ GridColumn column = new GridColumn(grid,SWT.NONE);\r
+ column.setTree(true);\r
+ column.setText("Name");\r
+ column.setWidth(200); \r
+\r
+ // "Pagees"\r
+ assertColumnIndex(1);\r
+ \r
+ Format pdfFormat = ctx.eep.getFormat("pdf");\r
+ \r
+ \r
+ ImageDescriptor folderID = null;\r
+ try {\r
+ URL folderUrl = new URL("platform:/plugin/com.famfamfam.silk/icons/folder.png");\r
+ folderID = ImageDescriptor.createFromURL( folderUrl );\r
+ } catch (MalformedURLException e) {\r
+ e.printStackTrace();\r
+ }\r
+ \r
+ List<GridItem> selectedNodes = new ArrayList<GridItem>();\r
+ \r
+ // Iterate all models\r
+ for ( String modelUri : contentToTypeMap.getKeys() ) {\r
+ ContentType modelContentType = null;\r
+ for ( ContentType ct : contentToTypeMap.getValues(modelUri) ) {\r
+ if ( ct.isModel() ) {\r
+ modelContentType = ct;\r
+ break;\r
+ }\r
+ }\r
+ if (modelContentType==null) continue;\r
+ \r
+ // Create Model Node\r
+ String modelLabel = labels.get("model").get(modelUri);\r
+ GridItem modelNode = newLine(grid, modelLabel, modelUri, modelContentType.id()); \r
+ setIcon(modelNode, 0, modelContentType.icon(modelUri));\r
+ modelNode.setToolTipText(0, modelUri);\r
+ \r
+ if ( ctx.selection.contains( modelUri ) ) selectedNodes.add( modelNode );\r
+\r
+ // Columns for formats\r
+ int column1 = 1;\r
+ for ( ContentType ct : contentToTypeMap.getValues(modelUri) ) {\r
+ for ( Format format : typeToFormatMap.getValues(ct) ) {\r
+ column1++;\r
+ assertColumnIndex( column1 );\r
+ modelNode.setText(column1, format.fileext());\r
+ modelNode.setGrayed(column1, false);\r
+ modelNode.setCheckable(column1, true);\r
+ modelNode.setData( Integer.toString(column1), \r
+ new Content(modelUri, ct.id(), format.id(), modelLabel, format.fileext(), modelUri ) );\r
+ modelNode.setChecked(column1, hasInitialSelection(modelUri, format.id()));\r
+ modelNode.setToolTipText(column1, format.label());\r
+\r
+ ImageDescriptor id = format.icon();\r
+ if ( id!=null ) {\r
+ Image icon = resourceManager.createImage(id);\r
+ if ( icon!=null) modelNode.setImage(column1, icon);\r
+ }\r
+ }\r
+ }\r
+ \r
+ for ( ContentType ct : content.getKeys() ) \r
+ {\r
+ if ( ct.isModel() ) continue;\r
+ // ContentType Node\r
+ GridItem ctNode = newLine(modelNode, ct.label(), modelUri, ct.id());\r
+ ctNode.setExpanded( true );\r
+ ctNode.setBackground(0, contentTypeColor);\r
+ ctNode.setBackground(contentTypeColor);\r
+ setIcon(ctNode, 0, folderID);\r
+ int contentCount = 0;\r
+ ArrayList<Format> contentTypesFormats = new ArrayList<Format>();\r
+ \r
+ for ( String contentUri : content.getValues(ct) ) {\r
+ // WORKAROUND: Should not be based on URI\r
+ if ( !contentUri.startsWith(modelUri) ) continue;\r
+ // Content Node\r
+ String nodeLabel = labels.get(ct.id()).get(contentUri);\r
+ GridItem contentNode = newLine(ctNode, nodeLabel, contentUri, ct.id());\r
+ contentCount++;\r
+ contentNode.setToolTipText(0, contentUri);\r
+ setIcon(contentNode, 0, ct.icon(contentUri));\r
+ \r
+ if ( ctx.selection.contains( contentUri) ) selectedNodes.add( contentNode );\r
+ \r
+ int columnNumber = 0;\r
+ \r
+ // PDF Column\r
+ List<Format> formats = typeToFormatMap.getValues(ct);\r
+ if ( formats.contains( pdfFormat )) {\r
+ if ( !contentTypesFormats.contains(pdfFormat) ) contentTypesFormats.add(pdfFormat);\r
+ columnNumber++;\r
+ assertColumnIndex( columnNumber );\r
+ \r
+ contentNode.setText(columnNumber, " "+pdfFormat.fileext());\r
+ contentNode.setGrayed(columnNumber, false);\r
+ contentNode.setCheckable(columnNumber, true);\r
+ contentNode.setChecked(columnNumber, hasInitialSelection(contentUri, pdfFormat.id()) );\r
+ contentNode.setToolTipText(columnNumber, pdfFormat.label());\r
+ setIcon(contentNode, columnNumber, pdfFormat.icon());\r
+ \r
+ contentNode.setData(\r
+ Integer.toString(columnNumber), \r
+ new Content(contentUri, ct.id(), pdfFormat.id(), nodeLabel, pdfFormat.fileext(), modelUri ) );\r
+ \r
+ } else {\r
+ if ( !contentTypesFormats.contains(null) ) contentTypesFormats.add(null);\r
+ columnNumber++;\r
+ assertColumnIndex( columnNumber );\r
+ }\r
+ \r
+ // Attachment Columns\r
+ for (Format format : formats ) {\r
+ if ( format==pdfFormat ) continue;\r
+ if ( !contentTypesFormats.contains(format) ) contentTypesFormats.add(format);\r
+ columnNumber++;\r
+ assertColumnIndex( columnNumber );\r
+ contentNode.setText(columnNumber, " "+format.fileext());\r
+ contentNode.setGrayed(columnNumber, false);\r
+ contentNode.setCheckable(columnNumber, true);\r
+ contentNode.setChecked(columnNumber, hasInitialSelection(contentUri, format.id()) );\r
+ contentNode.setToolTipText(columnNumber, format.label()); \r
+ setIcon(contentNode, columnNumber, format.icon());\r
+\r
+ contentNode.setData(\r
+ Integer.toString(columnNumber), \r
+ new Content(contentUri, ct.id(), format.id(), nodeLabel, format.fileext(), modelUri ) );\r
+ }\r
+ }\r
+ \r
+ // Add the *.pdf buttons\r
+ int columnNumber = 0;\r
+ Set<GridItem> gis = new HashSet<GridItem>();\r
+ for ( Format format : contentTypesFormats ) {\r
+ columnNumber++;\r
+ ctNode.setBackground(columnNumber, contentTypeColor);\r
+ if ( format == null ) continue;\r
+ ctChecks.add( new CTCheck(ctNode, columnNumber, format) );\r
+ ctNode.setCheckable(columnNumber, true);\r
+ ctNode.setGrayed(columnNumber, true);\r
+ //setIcon(ctNode, columnNumber, format.icon());\r
+ gis.add(ctNode);\r
+ //ctNode.setData(Integer.toString(columnNumber), format );\r
+ //ctNode.setText(columnNumber, "*"+format.fileext());\r
+ }\r
+ \r
+ if ( contentCount == 0 ) {\r
+ ctNode.dispose();\r
+ }\r
+ \r
+ }\r
+ modelNode.setExpanded( true );\r
+ }\r
+ grid.setSelection( selectedNodes.toArray( new GridItem[selectedNodes.size()] ) );\r
+ if ( selectedNodes.size()>0 ) {\r
+ GridItem first = selectedNodes.get(0);\r
+ grid.setFocusItem( first ); \r
+ }\r
+\r
+ grid.addSelectionListener(ctChecksListener);\r
+ /*\r
+ grid.addSelectionListener( new SelectionAdapter() {\r
+ public void widgetSelected(SelectionEvent e) {\r
+ if ( e.item == null || e.item instanceof GridItem==false ) return;\r
+ GridItem gi = (GridItem) e.item;\r
+ GridColumn column = grid.getColumn( new Point(e.x, e.y) );\r
+ if ( column == null ) return;\r
+ int columnIndex = -1;\r
+ int columnCount = grid.getColumnCount();\r
+ for ( int i=0; i<columnCount; i++) {\r
+ GridColumn gc = grid.getColumn(i);\r
+ if ( gc == column ) {\r
+ columnIndex = i;\r
+ break;\r
+ }\r
+ }\r
+ System.out.println(e.detail);\r
+ System.out.println(e);\r
+ System.out.println(columnIndex);\r
+ String text = gi.getText(columnIndex);\r
+ System.out.println(text);\r
+ }\r
+ });*/\r
+ \r
+ setControl(grid);\r
+ setPageComplete(true);\r
+ }\r
+ \r
+ void setIcon(GridItem node, int index, ImageDescriptor icon) {\r
+ if ( icon == null ) return;\r
+ Image image = resourceManager.createImage(icon);\r
+ if ( image == null ) return;\r
+ node.setImage(index, image);\r
+ }\r
+ \r
+ /**\r
+ * Creates column index if doesn't exist.\r
+ * Column=2, is "Pages"\r
+ * Column>=3, is "Attachement"\r
+ * \r
+ * @param columnIndex\r
+ */\r
+ void assertColumnIndex(int columnIndex) {\r
+ while ( columnIndex >= grid.getColumnCount() ) {\r
+ int cc = grid.getColumnCount();\r
+ \r
+ GridColumn column = new GridColumn(grid, SWT.CHECK);\r
+ column.setText( cc==1?"Pages":"Attachments");\r
+ column.setWidth( cc==1?150:200 );\r
+ \r
+ for (GridItem gi : grid.getItems()) {\r
+ gi.setGrayed(cc, true);\r
+ gi.setCheckable(cc, false);\r
+ }\r
+ \r
+ } \r
+ }\r
+ \r
+ boolean hasInitialSelection(String uri, String formatId) {\r
+ for (Content c : initialSelection) {\r
+ if ( !c.url.equals( uri ) ) continue;\r
+ if ( c.formatId.equals("all") || c.formatId.equals(formatId) ) return true;\r
+ }\r
+ return false;\r
+ }\r
+ \r
+ GridItem newLine(Object parent, String label, String url, String contentTypeId) {\r
+ GridItem gi = null;\r
+ if (parent instanceof Grid) {\r
+ gi = new GridItem( (Grid)parent, SWT.NONE);\r
+ } else {\r
+ gi = new GridItem( (GridItem)parent, SWT.NONE);\r
+ }\r
+ \r
+ gi.setText( label );\r
+ if ( url!=null || contentTypeId!=null ) gi.setData( new Content(url, contentTypeId, null, label, null, null) );\r
+ for ( int columnIndex=0; columnIndex<grid.getColumnCount(); columnIndex++ ) {\r
+ gi.setGrayed(columnIndex, true);\r
+ gi.setCheckable(columnIndex, false);\r
+ }\r
+ \r
+ return gi; \r
+ }\r
+ \r
+ public void validatePage() {\r
+ List<Content> newContentSelection = new ArrayList<Content>(); \r
+ // Get list of content.. something must be checked\r
+ int columnWidth = grid.getColumnCount();\r
+ Set<String> checkedFormats = new HashSet<String>();\r
+ for (GridItem gi : grid.getItems()) {\r
+ /*\r
+ checkedFormats.clear();\r
+ GridItem parentItem = gi.getParentItem();\r
+ if ( parentItem!=null ) {\r
+ for (int c=0; c<columnWidth; c++) {\r
+ if ( parentItem.getChecked(c) ) {\r
+ Object data = parentItem.getData( Integer.toString(c) );\r
+ if ( data==null || data instanceof Format == false ) continue;\r
+ Format format = (Format) data;\r
+ checkedFormats.add( format.id() );\r
+ }\r
+ }\r
+ }*/\r
+ \r
+ for (int c=0; c<columnWidth; c++) { \r
+ Object data = gi.getData( Integer.toString(c) );\r
+ if ( data==null || data instanceof Content == false ) continue;\r
+ Content content = (Content) data; \r
+ if ( gi.getChecked(c) || checkedFormats.contains(content.formatId) ) {\r
+ newContentSelection.add( content );\r
+ }\r
+ }\r
+ \r
+ }\r
+ \r
+ contentSelection = newContentSelection;\r
+ }\r
+ \r
+ public List<Content> getContentSelection() {\r
+ return contentSelection;\r
+ }\r
+\r
+ public void savePrefs() {\r
+ String str = ExportWizardResult.print( getContentSelection() );\r
+ ctx.store.put(initialSelectionKey, str);\r
+ }\r
+ \r
+ static class IndirectComparator implements Comparator<String> {\r
+ Map<String, String> labels;\r
+\r
+ @Override\r
+ public int compare(String o1, String o2) {\r
+ String l1 = null, l2 = null;\r
+ if ( labels != null ) {\r
+ l1 = labels.get(o1);\r
+ l2 = labels.get(o2);\r
+ } \r
+ if ( l1 == null ) l1 = o1;\r
+ if ( l2 == null ) l2 = o2;\r
+ return AlphanumComparator.CASE_INSENSITIVE_COMPARATOR.compare(l1, l2);\r
+ }\r
+ }\r
+ \r
+ //// Code for following clicks on ContentType format specific checkboxes\r
+ List<CTCheck> ctChecks = new ArrayList<CTCheck>();\r
+ static class CTCheck {\r
+ GridItem gi;\r
+ int columnNumber;\r
+ Format format;\r
+ boolean lastKnownCheckedStatus = false;\r
+ CTCheck(GridItem gi, int columnNumber, Format format) {\r
+ this.gi = gi;\r
+ this.columnNumber = columnNumber;\r
+ this.format = format;\r
+ }\r
+ boolean previousSelection;\r
+ boolean checkStatus() {\r
+ return gi.getChecked(columnNumber);\r
+ }\r
+ void setCheck(boolean checked) {\r
+ gi.setChecked(columnNumber, checked);\r
+ }\r
+ }\r
+ \r
+ SelectionListener ctChecksListener = new SelectionAdapter() {\r
+ public void widgetSelected(SelectionEvent e) {\r
+ if ( e.item == null || e.item instanceof GridItem==false ) return;\r
+ int columnWidth = grid.getColumnCount();\r
+ for ( CTCheck cc : ctChecks ) {\r
+ boolean checked = cc.checkStatus();\r
+ if ( checked == cc.lastKnownCheckedStatus ) continue;\r
+ cc.lastKnownCheckedStatus = checked;\r
+ \r
+ for ( GridItem gi : cc.gi.getItems() ) {\r
+ for ( int columnNumber = 0; columnNumber<columnWidth; columnNumber++ ) {\r
+ Object data = gi.getData( Integer.toString( columnNumber ) );\r
+ if ( data==null || data instanceof Content == false ) continue;\r
+ Content content = (Content) data; \r
+ if ( !content.formatId.equals( cc.format.id() )) continue;\r
+ gi.setChecked(columnNumber, checked);\r
+ }\r
+ } \r
+ }\r
+ \r
+ }\r
+ };\r
+ \r
+\r
+ \r
+}\r