package com.delcyon.capo.webapp.widgets; import java.io.File; import java.io.FileInputStream; import java.io.OutputStream; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; import java.util.logging.Level; import org.w3c.dom.Element; import com.delcyon.capo.CapoApplication; import com.delcyon.capo.datastream.StreamUtil; import com.delcyon.capo.datastream.stream_attribute_filter.MimeTypeFilterInputStream; import com.delcyon.capo.resourcemanager.ContentFormatType; import com.delcyon.capo.resourcemanager.ResourceDescriptor; import com.delcyon.capo.resourcemanager.ResourceDescriptor.Action; import com.delcyon.capo.resourcemanager.ResourceDescriptor.State; import com.delcyon.capo.resourcemanager.types.ContentMetaData; import com.delcyon.capo.resourcemanager.types.Versionable; import com.delcyon.capo.util.HexUtil; import com.delcyon.capo.util.diff.Diff; import com.delcyon.capo.util.diff.InputStreamTokenizer.TokenList; import com.delcyon.capo.webapp.models.DomItemModel; import com.delcyon.capo.webapp.models.DomItemModel.DomUse; import com.delcyon.capo.webapp.models.ResourceDescriptorItemModel; import com.delcyon.capo.webapp.models.WContentMetaDataItemModel; import com.delcyon.capo.webapp.servlets.CapoWebApplication; import com.delcyon.capo.webapp.servlets.resource.WResourceDescriptor; import com.delcyon.capo.webapp.widgets.WAceEditor.Theme; import com.delcyon.capo.webapp.widgets.WDiffWidget.DiffFormat; import com.delcyon.capo.xml.XMLDiff; import com.delcyon.capo.xml.XPath; import eu.webtoolkit.jwt.AlignmentFlag; import eu.webtoolkit.jwt.AnchorTarget; import eu.webtoolkit.jwt.SelectionMode; import eu.webtoolkit.jwt.Signal; import eu.webtoolkit.jwt.WAbstractItemView; import eu.webtoolkit.jwt.WAnchor; import eu.webtoolkit.jwt.WContainerWidget; import eu.webtoolkit.jwt.WFileUpload; import eu.webtoolkit.jwt.WImage; import eu.webtoolkit.jwt.WLength; import eu.webtoolkit.jwt.WLink; import eu.webtoolkit.jwt.WModelIndex; import eu.webtoolkit.jwt.WMouseEvent; import eu.webtoolkit.jwt.WProgressBar; import eu.webtoolkit.jwt.WPushButton; import eu.webtoolkit.jwt.WTabWidget; import eu.webtoolkit.jwt.WTableView; import eu.webtoolkit.jwt.WWidget; import eu.webtoolkit.jwt.servlet.UploadedFile; public class WCapoResourceEditor extends WTabWidget { private static final String XML_CONTENT_TYPE = "xml"; private static final String HEX_CONTENT_TYPE = "hex"; private static final String SHELL_CONTENT_TYPE = "sh"; private static final String MIMETYPE_IMAGE_PREFIX = "image/"; private static final String APPLICATION_X_SHELLSCRIPT = "application/x-shellscript"; private static final String EXTENTION_DELIMITER = "."; private static final String EMPTY_STRING = ""; private ResourceDescriptor model = null; private WTableView attributeTableView; private WContainerWidget detailsContainerWidget; private String content = null; private String contentType = null; private ContentFormatType contentFormatType = null; private String mimeType = null; private long length = 0l; private WLink downloadLink; private WPushButton createContentPushButton; private WAceEditor aceEditor; private WAceEditor formattedContentDisplay; private WFileUpload upload; private WContainerWidget historyContainerWidget; private WTableView historyTableView; private Signal modelChanged = new Signal(); private WDiffWidget diffWidget; private String currentVersionName = ""; private WXMLEditor xmlEditor; /** * Actually loads the data into the editor. This is the only place tabs and what not should be added since it's the main controller method for this class * * @param content * - actual data to display * @param contentType * - ace mode to use when displaying data * @param contentFormatType * - Capo Content format type * @param mimeType * - standard mime type * @param length * - length of data */ public void setContent(String content, String contentType, ContentFormatType contentFormatType, String mimeType, Long length) { this.content = content; // empty out all of the existing tabs since were going to add some random number back in while (this.getCount() > 0) { this.removeTab(this.getWidget(0)); } // null content is ok if (content != null) { // we can also work with empty content, we just don't want to show it if there's nothing there if (length != null && length > 0l) { getFormattedContentDisplay().setText(content); getFormattedContentDisplay().setMode(contentType); this.addTab(getFormattedContentDisplay(), "Content"); } if(contentFormatType == ContentFormatType.XML) { try { getXmlEditor().setXml(content); this.addTab(getXmlEditor(), "Edit"); } catch (Exception exception) { CapoWebApplication.exception(Level.SEVERE, "Error adding xml editor", exception); } } // don't allow binary content to be edited else if (contentFormatType != ContentFormatType.BINARY) { getAceEditor().setText(content); getAceEditor().setMode(contentType); this.addTab(getAceEditor(), "Edit"); } }// treat images a little differently, since we know what to do with them to showup else if (mimeType != null && mimeType.startsWith(MIMETYPE_IMAGE_PREFIX)) { WResourceDescriptor wResourceDescriptor = new WResourceDescriptor((ResourceDescriptor) this.model); this.addTab(new WImage(wResourceDescriptor, "Content"), "Content"); } // update the download link with the new mode data getDownloadLink().setResource(new WResourceDescriptor((ResourceDescriptor) this.model)); // always add the details tab last this.addTab(getDetailsContainerWidget(), "Details"); // add version history tab if (this.model instanceof Versionable) { try { // add these tabs, but hide them if the model isn't versioned this.addTab(getHistoryContainerWidget(), "History").setHidden(((Versionable) this.model).isVersioned() == false); this.addTab(getDiffWidget(), "Diff").setHidden(true); } catch (Exception e) { CapoWebApplication.exception(Level.SEVERE, "Error adding history tabs", e); } } // this might be able to be removed, but was put there when we we're using a background image this.getWidget(0).setAttributeValue("style", "background-color: rgba(255, 255, 255, 0.55);"); } /** * this widget is here to simply add comcolor codeing to the content to make it more readable, but is ALWAYS readonly. * * @return */ private WAceEditor getFormattedContentDisplay() { if (formattedContentDisplay == null) { formattedContentDisplay = new WAceEditor(); formattedContentDisplay.setTheme(Theme.eclipse); // TODO set via user preference formattedContentDisplay.setReadOnly(true); } return formattedContentDisplay; } /** * This is the widget which actually edit's the content * * @return */ private WAceEditor getAceEditor() { if (aceEditor == null) { aceEditor = new WAceEditor(); aceEditor.setTheme(Theme.tomorrow); // add a save listen to the editor aceEditor.save().addListener(this, this::save); } return aceEditor; } private WXMLEditor getXmlEditor() { if(xmlEditor == null) { xmlEditor = new WXMLEditor(); } return xmlEditor; } /** * this will save the 'content' to the 'model', then call refresh */ private void save(String content) { try { // update content WCapoResourceEditor.this.content = content; Versionable versionableModel = null; if (model instanceof Versionable && ((Versionable) model).isVersioned()) { versionableModel = (Versionable) model; } // checkout if we need too if (versionableModel != null) { versionableModel.checkout(); } // then save ((ResourceDescriptor) model).writeBlock(null, content.getBytes()); // checkin if we need too if (versionableModel != null) { versionableModel.checkin(); } // refresh the view refresh(); } catch (Exception exception) { CapoWebApplication.exception(Level.SEVERE, "Error saving", exception); } }; /** * @return the text content of this detail pane */ public String getContent() { return content; } /** * @param model * , can be either a resource descriptor or DOM element. */ public void setModel(ResourceDescriptor model) { this.model = model; // keep all of our variable as nulls, so we don't get weird on each model reset String content = null; String contentType = null; ContentFormatType contentFormatType = null; String mimeType = null; getCreateContentButton().setHidden(true); long length = 0l; if (this.model instanceof Element) { getAttributeTableView().setModel(new DomItemModel((Element) this.model, DomUse.ATTRIBUTES)); // set some decent defaults for xml data content = ((Element) this.model).getTextContent(); contentFormatType = ContentFormatType.XML; contentType = XML_CONTENT_TYPE; } else if (this.model instanceof ResourceDescriptor) { // go ahead a cast here so we can have some cleaner looking code ResourceDescriptor resourceDescriptor = (ResourceDescriptor) this.model; getAttributeTableView().setModel(new ResourceDescriptorItemModel(resourceDescriptor, DomUse.ATTRIBUTES)); try { if (resourceDescriptor instanceof Versionable) { List<ContentMetaData> versionHistory = ((Versionable) resourceDescriptor).getVersionHistory(); for (ContentMetaData contentMetaData : versionHistory) { if(contentMetaData.getValue("isBaseVersion").equals("true")) { this.currentVersionName = contentMetaData.getValue("versionName"); } } getHistoryTableView().setModel(new WContentMetaDataItemModel(versionHistory, "isBaseVersion,Current", "versionTimeStamp,Date", "versionName,Version", "mimeType", "MD5", "size", "contentFormatType,Type")); } } catch (Exception e1) { CapoWebApplication.exception(Level.SEVERE, "Error getting base version", e1); } try { // used to be a container check here, but doesn't make since when backing is JCR // start figuring out what kind of data we're dealing with contentFormatType = resourceDescriptor.getResourceMetaData(null).getContentFormatType(); mimeType = resourceDescriptor.getResourceMetaData(null).getValue(MimeTypeFilterInputStream.MIME_TYPE_ATTRIBUTE); // default mimetype to empty string so we don't have to litter null checks every where if (mimeType == null) { mimeType = EMPTY_STRING; } length = resourceDescriptor.getResourceMetaData(null).getLength(); // figure out the contentType. Content type basically matches up to any available ACE mode in the ace editor if (contentFormatType == ContentFormatType.TEXT || contentFormatType == ContentFormatType.NO_CONTENT) { if (contentFormatType == ContentFormatType.NO_CONTENT) { // no content? enable the create button getCreateContentButton().setHidden(false); } else {// other wise use the data we have in the model resourceDescriptor.getResourceState(); content = new String(resourceDescriptor.readBlock(null)); resourceDescriptor.reset(State.OPEN); } // see if the node name has some sort of clue as to the editor mode to use String localName = resourceDescriptor.getLocalName(); if (localName.indexOf(EXTENTION_DELIMITER) <= 0) // check for extension { // ok, well what about the mime type if (mimeType.equalsIgnoreCase(APPLICATION_X_SHELLSCRIPT)) { // to bad we only know one type.. :-( contentType = SHELL_CONTENT_TYPE; } } else // use extension { // sure hope this matches up to some available ACE editor mode contentType = localName.substring(localName.indexOf(EXTENTION_DELIMITER) + 1); } } // ok, this is XML not text, so put the editor in XML mode // TODO once we become namespace aware, we should use somesort of scheme aware xml editor, for known XML schemas else if (contentFormatType == ContentFormatType.XML) { resourceDescriptor.getResourceState(); content = new String(resourceDescriptor.readBlock(null)); resourceDescriptor.reset(State.OPEN); contentType = XML_CONTENT_TYPE; } // looks like we have some binary content here, and it's not an image, lets dump it to a nice hex output // TODO this should probably not default to showing the binary, but present the user with the option to view binary content // unless it's a registered mimetype, that the user, or admin has set in their preferences or something. else if (contentFormatType == ContentFormatType.BINARY && length < 70000l && mimeType.startsWith(MIMETYPE_IMAGE_PREFIX) == false) { byte[] bytes = resourceDescriptor.readBlock(null); resourceDescriptor.reset(State.OPEN); content = HexUtil.dump(bytes); contentType = HEX_CONTENT_TYPE; } } catch (Exception e) { CapoWebApplication.exception(Level.SEVERE, "Error setting model", e); } } setContent(content, contentType, contentFormatType, mimeType, length); modelChanged.trigger(); } /** * This just empty out any content from the model, then refreshes the widget */ public void clearContent() { try { ((ResourceDescriptor) WCapoResourceEditor.this.model).writeBlock(null, EMPTY_STRING.getBytes()); refresh(); } catch (Exception e) { CapoWebApplication.exception(Level.SEVERE, "Error clearing content", e); } } private WContainerWidget getDetailsContainerWidget() { if (detailsContainerWidget == null) { detailsContainerWidget = new WContainerWidget(); WAnchor anchor = new WAnchor(getDownloadLink(), "Download"); // this is a link so "save as" will work anchor.setTarget(AnchorTarget.TargetNewWindow); upload = new WFileUpload(); upload.setFileTextSize(10000); // needed to get a basic starting point on the progress apparently upload.setProgressBar(new WProgressBar()); // fired when the user selects a file to upload, which we use to indicate that we'd like to start uploading upload.changed().addListener(this, upload::upload); // trigger that gets called once a file is done uploading upload.uploaded().addListener(this, this::fileUploaded); // TODO This upload error has to be processed. We really need an error dialog upload.fileTooLarge().addListener(this, () -> System.err.println("error, too large")); // clean content button. WPushButton clearContentPushButton = new WPushButton("Clear Content"); clearContentPushButton.clicked().addListener(this, this::clearContent); // checkin version button WPushButton checkinVersionPushButton = new WPushButton("Track Versions"); checkinVersionPushButton.clicked().addListener(this, this::makeVersionable); // //checkout version button // WPushButton checkoutVersionPushButton = new WPushButton("Check Out"); // checkoutVersionPushButton.clicked().addListener(this, this::checkoutContent); detailsContainerWidget.addWidget(upload); detailsContainerWidget.addWidget(anchor); detailsContainerWidget.addWidget(clearContentPushButton); detailsContainerWidget.addWidget(getCreateContentButton()); detailsContainerWidget.addWidget(checkinVersionPushButton); // detailsContainerWidget.addWidget(checkoutVersionPushButton); detailsContainerWidget.addWidget(getAttributeTableView()); } return detailsContainerWidget; } private WContainerWidget getHistoryContainerWidget() { if (historyContainerWidget == null) { historyContainerWidget = new WContainerWidget(); // checkin version button WPushButton checkinVersionPushButton = new WPushButton("Check In"); checkinVersionPushButton.clicked().addListener(this, this::checkinContent); // checkout version button WPushButton checkoutVersionPushButton = new WPushButton("Check Out"); checkoutVersionPushButton.clicked().addListener(this, this::checkoutContent); // checkout version button WPushButton restoreSelectedVersionPushButton = new WPushButton("Restore Selected"); restoreSelectedVersionPushButton.disable(); restoreSelectedVersionPushButton.clicked().addListener(this, this::restoreSelected); getHistoryTableView().selectionChanged().addListener(this, () -> processSelectionEvent(getHistoryTableView(), restoreSelectedVersionPushButton)); modelChanged.addListener(this, restoreSelectedVersionPushButton::disable); // checkout version button WPushButton deleteSelectedVersionPushButton = new WPushButton("Delete Selected"); deleteSelectedVersionPushButton.clicked().addListener(this, this::deleteSelected); getHistoryTableView().selectionChanged().addListener(this, () -> processSelectionEvent(getHistoryTableView(), deleteSelectedVersionPushButton)); modelChanged.addListener(this, deleteSelectedVersionPushButton::disable); // diff version button WPushButton diffSelectedVersionPushButton = new WPushButton("Diff Selected"); diffSelectedVersionPushButton.clicked().addListener(this, this::diffSelected); getHistoryTableView().selectionChanged().addListener(this, () -> processSelectionEvent(getHistoryTableView(), diffSelectedVersionPushButton)); modelChanged.addListener(this, diffSelectedVersionPushButton::disable); historyContainerWidget.addWidget(checkinVersionPushButton); historyContainerWidget.addWidget(checkoutVersionPushButton); historyContainerWidget.addWidget(restoreSelectedVersionPushButton); historyContainerWidget.addWidget(deleteSelectedVersionPushButton); historyContainerWidget.addWidget(diffSelectedVersionPushButton); historyContainerWidget.addWidget(getHistoryTableView()); } return historyContainerWidget; } private void processSelectionEvent(WAbstractItemView abstractItemView, WWidget widget) { if (abstractItemView.getSelectedIndexes() == null || abstractItemView.getSelectedIndexes().isEmpty()) { widget.disable(); } else { widget.enable(); } } private void deleteSelected() { SortedSet<WModelIndex> selectedIndexes = getHistoryTableView().getSelectedIndexes(); if (selectedIndexes != null && selectedIndexes.isEmpty() == false) { WModelIndex selectedIndex = selectedIndexes.first(); try { ((Versionable) this.model).remove(((ContentMetaData) selectedIndex.getInternalPointer()).getResourceURI().getResourceURIString()); refresh(); } catch (Exception exception) { CapoWebApplication.exception(Level.SEVERE, "Error deleting selected", exception); } } } private void restoreSelected() { SortedSet<WModelIndex> selectedIndexes = getHistoryTableView().getSelectedIndexes(); if (selectedIndexes != null && selectedIndexes.isEmpty() == false) { WModelIndex selectedIndex = selectedIndexes.first(); try { ((Versionable) this.model).restore(((ContentMetaData) selectedIndex.getInternalPointer()).getResourceURI().getResourceURIString()); refresh(); } catch (Exception exception) { CapoWebApplication.exception(Level.SEVERE, "error restoring selected", exception); } } } /** * Generate a diff between the current version, and a selected version */ private void diffSelected() { SortedSet<WModelIndex> selectedIndexes = getHistoryTableView().getSelectedIndexes(); if (selectedIndexes != null && selectedIndexes.isEmpty() == false) { WModelIndex selectedIndex = selectedIndexes.first(); try { String versionURI = ((Versionable) this.model).getVersion((((ContentMetaData) selectedIndex.getInternalPointer()).getResourceURI().getResourceURIString())); ResourceDescriptor versionResourceDescriptor = CapoApplication.getDataManager().getResourceDescriptor(null, versionURI); ContentFormatType contentFormatType = ((ResourceDescriptor) this.model).getResourceMetaData(null).getContentFormatType(); ContentFormatType versionContentFormatType = versionResourceDescriptor.getResourceMetaData(null).getContentFormatType(); if (versionContentFormatType.ordinal() > contentFormatType.ordinal()) { contentFormatType = versionContentFormatType; } if (contentFormatType == ContentFormatType.XML) { XMLDiff xmlDiff = new XMLDiff(); Element diffelement = xmlDiff.getDifferences(versionResourceDescriptor.readXML(null), ((ResourceDescriptor) this.model).readXML(null)); XPath.dumpNode(diffelement, System.out); } else if (contentFormatType == ContentFormatType.TEXT) { Diff diff = new Diff(versionResourceDescriptor.getInputStream(null), ((ResourceDescriptor) this.model).getInputStream(null), TokenList.NEW_LINE); getDiffWidget().setHeaders(((ContentMetaData) selectedIndex.getInternalPointer()).getValue("versionName"),"Current ("+currentVersionName+")"); getDiffWidget().setDiff(diff.getDifferences(), DiffFormat.CAPO); } //TODO do we really want to DISPLAY BINARY DIFFS, seems like a lot of work for not a lot of benefit. // { // ByteArrayOutputStream baseByteArrayOutputStream = new ByteArrayOutputStream(); // StreamUtil.readInputStreamIntoOutputStream(versionResourceDescriptor.getInputStream(null), baseByteArrayOutputStream); // ByteArrayOutputStream modByteArrayOutputStream = new ByteArrayOutputStream(); // StreamUtil.readInputStreamIntoOutputStream( ((ResourceDescriptor) this.model).getInputStream(null), modByteArrayOutputStream); // Diff diff = new Diff(HexUtil.bytesToHex(baseByteArrayOutputStream.toByteArray()),HexUtil.bytesToHex(modByteArrayOutputStream.toByteArray())); // //byte[] diffs = diff.getDifferencesAsBytes(); // getDiffWidget().setDiff(diff.getDifferences(), DiffFormat.CAPO); // //System.out.println(new String(diffs)); // } this.setTabHidden(this.getIndexOf(getDiffWidget()), false); this.setCurrentIndex(this.getIndexOf(getDiffWidget())); } catch (Exception e) { CapoWebApplication.exception(Level.SEVERE, "Error diffing selected", e); } } } private WDiffWidget getDiffWidget() { if (diffWidget == null) { diffWidget = new WDiffWidget(); } return diffWidget; } /** * This just create the attribute table for the model * * @return */ private WTableView getHistoryTableView() { if (historyTableView == null) { historyTableView = new WTableView(); historyTableView.addStyleClass("bg-transparent"); historyTableView.setItemDelegateForColumn(0, new WCSSItemDelegate("font-weight: bold;")); historyTableView.setSortingEnabled(true); historyTableView.setSelectable(true); historyTableView.setAlternatingRowColors(true); historyTableView.setColumnResizeEnabled(true); historyTableView.setColumnAlignment(0, AlignmentFlag.AlignRight); historyTableView.setColumnWidth(1, new WLength(500)); historyTableView.setSelectionMode(SelectionMode.SingleSelection); historyTableView.doubleClicked().addListener(this,this::historyDoubleClicked); } return historyTableView; } private void historyDoubleClicked(WModelIndex index,WMouseEvent event) { SortedSet<WModelIndex> indexes = new TreeSet<WModelIndex>(); indexes.add(index); getHistoryTableView().setSelectedIndexes(indexes); diffSelected(); } private void makeVersionable() { try { int currentIndex = getCurrentIndex(); ((ResourceDescriptor) model).performAction(null, Action.CHECKOUT); ((ResourceDescriptor) model).performAction(null, Action.CHECKIN); setTabHidden(getIndexOf(getHistoryContainerWidget()), false); refresh(); setCurrentIndex(currentIndex); } catch (Exception exception) { CapoWebApplication.exception(Level.SEVERE, "Error making versionale", exception); } } private void checkinContent() { try { if (model != null) { ((ResourceDescriptor) model).performAction(null, Action.CHECKIN); refresh(); } } catch (Exception e) { CapoWebApplication.exception(Level.SEVERE, "Error checkin content", e); } } private void checkoutContent() { try { if (model != null) { ((ResourceDescriptor) model).performAction(null, Action.CHECKOUT); } } catch (Exception e) { CapoWebApplication.exception(Level.SEVERE, "Error checking out content", e); } } /** * Handles file upload event */ private void fileUploaded() { try { Versionable versionableModel = null; if (model instanceof Versionable && ((Versionable) model).isVersioned()) { versionableModel = (Versionable) model; } // checkout if we need too if (versionableModel != null) { versionableModel.checkout(); } // we always get the first uploaded file, as we don't allow multiple files here List<UploadedFile> uploadedFiles = upload.getUploadedFiles(); String tempFileName = uploadedFiles.get(0).getSpoolFileName(); File tempFile = new File(tempFileName); // once we have a handle on the file, stream it into our resource descriptor // This is a little crazy, i'd rather pass a pointer around, but we're dealing with streams and jcr and all sorts of stuff OutputStream outputStream = ((ResourceDescriptor) WCapoResourceEditor.this.model).getOutputStream(null); StreamUtil.readInputStreamIntoOutputStream(new FileInputStream(tempFile), outputStream); outputStream.close(); // checkin if we need too if (versionableModel != null) { versionableModel.checkin(); } refresh(); } catch (Exception e) { CapoWebApplication.exception(Level.SEVERE, "Uploaded file couldn't be saved ", e); } } /** * Exposed download link so that we can change the backing model when needed. * * @return */ private WLink getDownloadLink() { if (downloadLink == null) { downloadLink = new WLink(); } return downloadLink; } /** * just creates the create content button, privately exposed, so it can be enabled and disabled accordingly * * @return */ private WWidget getCreateContentButton() { if (createContentPushButton == null) { createContentPushButton = new WPushButton("Create Content"); createContentPushButton.clicked().addListener(this, this::createContent); } return createContentPushButton; } /** * This will create an empty place holder for some text content on an empty node */ private void createContent() { setContent(EMPTY_STRING, contentType, contentFormatType, mimeType, length); } /** * This just create the attribute table for the model * * @return */ private WTableView getAttributeTableView() { if (attributeTableView == null) { attributeTableView = new WTableView(); attributeTableView.addStyleClass("bg-transparent"); attributeTableView.setItemDelegateForColumn(0, new WCSSItemDelegate("font-weight: bold;")); attributeTableView.setSortingEnabled(true); attributeTableView.setSelectable(true); attributeTableView.setAlternatingRowColors(true); attributeTableView.setColumnResizeEnabled(true); attributeTableView.setColumnAlignment(0, AlignmentFlag.AlignRight); attributeTableView.setColumnWidth(1, new WLength(500)); attributeTableView.setSelectionMode(SelectionMode.SingleSelection); } return attributeTableView; } /** * Will refresh all of the data from the model. */ @Override public void refresh() { try { if(model != null) { model.getResourceMetaData(null).refresh(); model.advanceState(State.CLOSED, null); model.reset(State.OPEN); ((ResourceDescriptorItemModel) getAttributeTableView().getModel()).reload(); setModel(model); } } catch (Exception e) { CapoWebApplication.exception(Level.SEVERE, "Error refreshing", e); } super.refresh(); } }