/*! ******************************************************************************
*
* Pentaho Data Integration
*
* Copyright (C) 2002-2013 by Pentaho : http://www.pentaho.com
*
*******************************************************************************
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/
package org.pentaho.di.starmodeler;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.WeakHashMap;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CCombo;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;
import org.pentaho.di.core.Const;
import org.pentaho.di.core.EngineMetaInterface;
import org.pentaho.di.core.LastUsedFile;
import org.pentaho.di.core.database.DatabaseMeta;
import org.pentaho.di.core.exception.KettleException;
import org.pentaho.di.core.logging.LogChannel;
import org.pentaho.di.core.logging.LogChannelInterface;
import org.pentaho.di.core.variables.Variables;
import org.pentaho.di.core.vfs.KettleVFS;
import org.pentaho.di.i18n.BaseMessages;
import org.pentaho.di.i18n.LanguageChoice;
import org.pentaho.di.job.JobMeta;
import org.pentaho.di.metastore.DatabaseMetaStoreUtil;
import org.pentaho.di.repository.RepositoryDirectory;
import org.pentaho.di.starmodeler.generator.JobGenerator;
import org.pentaho.di.starmodeler.generator.MetadataGenerator;
import org.pentaho.di.starmodeler.metastore.IdNameDescription;
import org.pentaho.di.starmodeler.metastore.SharedDimensionMetaStoreUtil;
import org.pentaho.di.starmodeler.metastore.StarDomainMetaStoreUtil;
import org.pentaho.di.trans.TransMeta;
import org.pentaho.di.ui.core.PropsUI;
import org.pentaho.di.ui.core.database.dialog.DatabaseDialog;
import org.pentaho.di.ui.core.dialog.EnterSelectionDialog;
import org.pentaho.di.ui.core.dialog.ErrorDialog;
import org.pentaho.di.ui.core.widget.ColumnInfo;
import org.pentaho.di.ui.core.widget.TableView;
import org.pentaho.di.ui.spoon.FileListener;
import org.pentaho.di.ui.spoon.MainSpoonPerspective;
import org.pentaho.di.ui.spoon.Spoon;
import org.pentaho.di.ui.spoon.SpoonPerspective;
import org.pentaho.di.ui.spoon.SpoonPerspectiveListener;
import org.pentaho.di.ui.spoon.SpoonPerspectiveManager;
import org.pentaho.di.ui.spoon.SpoonPerspectiveOpenSaveInterface;
import org.pentaho.di.ui.trans.step.BaseStepDialog;
import org.pentaho.di.ui.xul.KettleXulLoader;
import org.pentaho.metadata.model.Domain;
import org.pentaho.metadata.model.LogicalModel;
import org.pentaho.metadata.model.LogicalTable;
import org.pentaho.metadata.model.concept.types.LocalizedString;
import org.pentaho.metadata.util.SerializationService;
import org.pentaho.metastore.api.IMetaStore;
import org.pentaho.metastore.api.IMetaStoreElement;
import org.pentaho.metastore.api.IMetaStoreElementType;
import org.pentaho.metastore.api.exceptions.MetaStoreElementExistException;
import org.pentaho.metastore.api.exceptions.MetaStoreException;
import org.pentaho.metastore.util.PentahoDefaults;
import org.pentaho.ui.xul.XulComponent;
import org.pentaho.ui.xul.XulDomContainer;
import org.pentaho.ui.xul.XulException;
import org.pentaho.ui.xul.XulOverlay;
import org.pentaho.ui.xul.XulRunner;
import org.pentaho.ui.xul.binding.BindingConvertor;
import org.pentaho.ui.xul.binding.BindingFactory;
import org.pentaho.ui.xul.binding.DefaultBindingFactory;
import org.pentaho.ui.xul.components.XulConfirmBox;
import org.pentaho.ui.xul.components.XulTab;
import org.pentaho.ui.xul.components.XulTabpanel;
import org.pentaho.ui.xul.containers.XulTabbox;
import org.pentaho.ui.xul.containers.XulTabpanels;
import org.pentaho.ui.xul.containers.XulTabs;
import org.pentaho.ui.xul.dom.Document;
import org.pentaho.ui.xul.impl.AbstractXulEventHandler;
import org.pentaho.ui.xul.impl.XulEventHandler;
import org.pentaho.ui.xul.swt.SwtXulRunner;
import org.pentaho.ui.xul.swt.tags.SwtTab;
import org.pentaho.ui.xul.util.XulDialogCallback;
import org.w3c.dom.Node;
public class StarModelerPerspective extends AbstractXulEventHandler implements SpoonPerspective, FileListener, XulEventHandler, SpoonPerspectiveOpenSaveInterface {
private static Class<?> PKG = StarModelerPerspective.class; // for i18n
private LogChannelInterface logger = LogChannel.GENERAL;
protected XulDomContainer container;
protected XulRunner runner;
protected Document document;
protected XulTabs tabs;
protected XulTabpanels panels;
protected XulTabbox tabbox;
protected List<SpoonPerspectiveListener> listeners = new ArrayList<SpoonPerspectiveListener>();
protected EngineMetaInterface selectedMeta;
protected List<StarDomain> models = new ArrayList<StarDomain>();
protected Map<XulTab, EngineMetaInterface> metas = new WeakHashMap<XulTab, EngineMetaInterface>();
private String defaultLocale = LanguageChoice.getInstance().getDefaultLocale().toString();
private String defaultExtension="star";
private static StarModelerPerspective instance;
public class XulTabAndPanel{
public XulTab tab;
public XulTabpanel panel;
public XulTabAndPanel(XulTab tab, XulTabpanel panel){
this.tab = tab;
this.panel = panel;
}
}
public XulTabAndPanel createTab(){
try {
XulTab tab = (XulTab) document.createElement("tab");
if(name != null){
tab.setLabel(name);
}
XulTabpanel panel = (XulTabpanel) document.createElement("tabpanel");
panel.setSpacing(0);
panel.setPadding(0);
tabs.addChild(tab);
panels.addChild(panel);
tabbox.setSelectedIndex(panels.getChildNodes().indexOf(panel));
return new XulTabAndPanel(tab, panel);
} catch (XulException e) {
e.printStackTrace();
}
return null;
}
public void setMetaForTab(XulTab tab, EngineMetaInterface meta){
metas.put(tab, meta);
}
private StarModelerPerspective() {
String perspectiveSrc = "org/pentaho/di/starmodeler/xul/perspective.xul";
try {
KettleXulLoader loader = new KettleXulLoader();
loader.registerClassLoader(getClass().getClassLoader());
Spoon.getInstance().addFileListener(this);
container = loader.loadXul(perspectiveSrc, new PDIMessages(this.getClass()));
runner = new SwtXulRunner();
runner.addContainer(container);
runner.initialize();
document = container.getDocumentRoot();
container.addEventHandler(this);
tabs = (XulTabs) document.getElementById("tabs");
panels = (XulTabpanels) document.getElementById("tabpanels");
tabbox = (XulTabbox) tabs.getParent();
BindingFactory bf = new DefaultBindingFactory();
setDefaultExtension("star");
bf.setDocument(document);
bf.createBinding(tabbox, "selectedIndex", this, "selectedMeta", new BindingConvertor<Integer, EngineMetaInterface>() {
public EngineMetaInterface sourceToTarget(Integer value) {
return metas.get(tabs.getTabByIndex(value));
}
public Integer targetToSource(EngineMetaInterface value) {
for (XulTab tab : metas.keySet()) {
if (metas.get(tab) == value) {
return tab.getParent().getChildNodes().indexOf(tab);
}
}
return -1;
}
});
} catch (Exception e) {
logger.logError("Error initializing perspective", e);
}
}
public static StarModelerPerspective getInstance() {
if (instance==null) {
instance=new StarModelerPerspective();
}
return instance;
}
@Override
public String getDisplayName(Locale l) {
return BaseMessages.getString(PKG, "StarModelerPerspective.Perspective.Name");
}
@Override
public InputStream getPerspectiveIcon() {
ClassLoader loader = getClass().getClassLoader();
return loader.getResourceAsStream("org/pentaho/di/starmodeler/images/starmodeler.png");
}
@Override
public String getId() {
return "020-StarModeler";
}
public boolean acceptsXml(String nodeName) {
return false;
}
public String[] getFileTypeDisplayNames(Locale locale) {
return new String[]{ "Star models" };
}
public String[] getSupportedExtensions() {
return new String[]{ "star" };
}
public final Composite getUI() {
return (Composite) container.getDocumentRoot().getRootElement().getFirstChild().getManagedObject();
}
@Override
public boolean open(Node transNode, String fname, boolean importfile) {
try {
String xml = KettleVFS.getTextFileContent(fname, Const.XML_ENCODING);
Domain domain = new SerializationService().deserializeDomain(xml);
StarDomain starDomain = new StarDomain();
starDomain.setDomain(domain);
starDomain.setFilename(fname);
createTabForDomain(starDomain);
PropsUI.getInstance().addLastFile(LastUsedFile.FILE_TYPE_SCHEMA, fname, null, false, null);
Spoon.getInstance().addMenuLast();
return true;
} catch(Exception e) {
new ErrorDialog(Spoon.getInstance().getShell(), "Error", "There was an error opening model from file '"+fname+"'", e);
}
return false;
}
public void importFile(String filename) {
open(null, filename, true);
}
public boolean exportFile(EngineMetaInterface meta, String filename) {
try {
String xml = meta.getXML();
OutputStream outputStream = KettleVFS.getOutputStream(filename, false);
outputStream.write(xml.getBytes(Const.XML_ENCODING));
outputStream.close();
meta.setFilename(filename);
return true;
} catch(Exception e) {
new ErrorDialog(Spoon.getInstance().getShell(), "Error", "Error export star domain to XML", e);
return false;
}
}
@Override
public boolean save(EngineMetaInterface meta, String fname, boolean isExport) {
try {
// We only expect a start domain here. How else would we end up here?
//
if (meta instanceof StarDomain) {
StarDomain starDomain = (StarDomain) meta;
// Make sure we pick the active MetaStore to save to, otherwise it's hard to verify
//
IMetaStore metaStore = Spoon.getInstance().metaStore.getActiveMetaStore();
LogChannel.GENERAL.logBasic("Saving star domain to meta store: "+metaStore.getName());
// Save the name and description of the shared dimension in the metastore
//
StarDomainMetaStoreUtil.saveStarDomain(metaStore, starDomain);
// Save the shared dimensions in the Spoon IMetaStore (update or create)
//
for (LogicalTable sharedDimension : starDomain.getSharedDimensions()) {
SharedDimensionMetaStoreUtil.saveSharedDimension(metaStore, sharedDimension, defaultLocale);
}
meta.clearChanged();
Spoon.getInstance().enableMenus();
return true;
}
} catch(Exception e) {
new ErrorDialog(Spoon.getInstance().getShell(), "Error saving model", "There was an error while saving the model:", e);
}
return false;
}
@Override
public void syncMetaName(EngineMetaInterface meta, String name) {
}
@Override
public boolean accepts(String fileName) {
if(fileName == null || fileName.indexOf('.') == -1){
return false;
}
String extension = fileName.substring(fileName.lastIndexOf('.')+1);
return extension.equals(defaultExtension);
}
@Override
public String getRootNodeName() {
return null;
}
@Override
public void setActive(boolean active) {
for(SpoonPerspectiveListener listener : listeners){
if(active){
listener.onActivation();
} else {
listener.onDeactication();
}
}
}
@Override
public List<XulOverlay> getOverlays() {
return null;
}
@Override
public List<XulEventHandler> getEventHandlers() {
return null;
}
@Override
public void addPerspectiveListener(SpoonPerspectiveListener listener) {
if(listeners.contains(listener) == false){
listeners.add(listener);
}
}
@Override
public Object getData() {
return null;
}
@Override
public String getName() {
return "starModelerPerspective";
}
@Override
public XulDomContainer getXulDomContainer() {
return null;
}
@Override
public void setData(Object arg0) {
}
@Override
public void setName(String arg0) {
}
@Override
public void setXulDomContainer(XulDomContainer arg0) {
// TODO Auto-generated method stub
}
public void setNameForTab(XulTab tab, String name){
String tabName = name;
List<String> usedNames = new ArrayList<String>();
for(XulComponent c : tabs.getChildNodes()){
if(c != tab){
usedNames.add(((SwtTab) c).getLabel());
}
}
if(usedNames.contains(name)){
int num = 2;
while(true){
tabName = name +" ("+num+")";
if(usedNames.contains(tabName) == false){
break;
}
num++;
}
}
tab.setLabel(tabName);
}
public void createTabForDomain(final StarDomain starDomain) throws Exception {
SpoonPerspectiveManager.getInstance().activatePerspective(getClass());
final XulTabAndPanel tabAndPanel = createTab();
PropsUI props = PropsUI.getInstance();
final Composite comp = (Composite) tabAndPanel.panel.getManagedObject();
props.setLook(comp);
comp.setLayout(new FillLayout());
final ScrolledComposite scrolledComposite = new ScrolledComposite(comp, SWT.V_SCROLL | SWT.H_SCROLL);
props.setLook(scrolledComposite);
scrolledComposite.setLayout(new FillLayout());
final Composite parentComposite = new Composite(scrolledComposite, SWT.NONE);
props.setLook(parentComposite);
int margin = Const.MARGIN;
FormLayout formLayout = new FormLayout();
formLayout.marginLeft=10;
formLayout.marginRight=10;
formLayout.marginTop=10;
formLayout.marginBottom=10;
formLayout.spacing=margin;
parentComposite.setLayout(formLayout);
Control lastControl = addModelsGroupToDomainTab(starDomain, tabAndPanel, parentComposite);
lastControl = addSharedDimensionsGroupToDomainTab(starDomain, tabAndPanel, parentComposite, lastControl);
lastControl = addPhysicalGroupToDomainTab(starDomain, tabAndPanel, parentComposite, lastControl);
parentComposite.layout(true);
parentComposite.pack();
// What's the size:
Rectangle bounds = parentComposite.getBounds();
scrolledComposite.setContent(parentComposite);
scrolledComposite.setExpandHorizontal(true);
scrolledComposite.setExpandVertical(true);
scrolledComposite.setMinWidth(bounds.width);
scrolledComposite.setMinHeight(bounds.height);
models.add(starDomain);
setNameForTab(tabAndPanel.tab, starDomain.getDomain().getName(defaultLocale));
setMetaForTab(tabAndPanel.tab, starDomain);
setSelectedMeta(starDomain);
setActive(true);
comp.layout();
Spoon.getInstance().enableMenus();
}
private Control addPhysicalGroupToDomainTab(final StarDomain starDomain, final XulTabAndPanel tabAndPanel,
final Composite parentComposite, Control lastControl) {
PropsUI props = PropsUI.getInstance();
int middle = props.getMiddlePct();
int margin = Const.MARGIN;
// And now for the physical hints
//
final Group physicalGroup = new Group(parentComposite, SWT.SHADOW_NONE);
props.setLook(physicalGroup);
physicalGroup.setText(BaseMessages.getString(PKG, "StarModelerPerspective.PhysicalGroup.Label"));
FormLayout phGroupLayout = new FormLayout();
phGroupLayout.marginLeft=10;
phGroupLayout.marginRight=10;
phGroupLayout.marginTop=10;
phGroupLayout.marginBottom=10;
phGroupLayout.spacing=margin;
physicalGroup.setLayout(phGroupLayout);
FormData fdPhysicalGroup = new FormData();
fdPhysicalGroup.top = new FormAttachment(lastControl, 2*margin);
fdPhysicalGroup.left = new FormAttachment(0, 0);
fdPhysicalGroup.right = new FormAttachment(100, 0);
physicalGroup.setLayoutData(fdPhysicalGroup);
lastControl = physicalGroup;
// The target database (optional)
//
final List<DatabaseMeta> sharedDatabases= SharedDatabaseUtil.getDatabaseMetaList(Spoon.getInstance().metaStore);
String[] databaseNames = SharedDatabaseUtil.getSortedDatabaseNames(sharedDatabases);
Label targetDatabaseLabel = new Label(physicalGroup, SWT.RIGHT);
props.setLook(targetDatabaseLabel);
targetDatabaseLabel.setText(BaseMessages.getString(PKG, "StarModelerPerspective.TargetDatabase.Label"));
FormData fdTargetDatabaseLabel = new FormData();
fdTargetDatabaseLabel.left=new FormAttachment(0, 0);
fdTargetDatabaseLabel.right=new FormAttachment(middle, 0);
fdTargetDatabaseLabel.top =new FormAttachment(0, 0);
targetDatabaseLabel.setLayoutData(fdTargetDatabaseLabel);
final Button newDatabaseButton = new Button(physicalGroup, SWT.PUSH);
newDatabaseButton.setText(BaseMessages.getString("System.Button.New"));
FormData fdNewDatabaseButton = new FormData();
fdNewDatabaseButton.right=new FormAttachment(100, 0);
fdNewDatabaseButton.top =new FormAttachment(0, 0);
newDatabaseButton.setLayoutData(fdNewDatabaseButton);
final CCombo targetDatabase = new CCombo(physicalGroup, SWT.BORDER | SWT.SINGLE);
targetDatabase.setItems(databaseNames);
props.setLook(targetDatabase);
String targetDb = ConceptUtil.getString(starDomain.getDomain(), DefaultIDs.DOMAIN_TARGET_DATABASE);
targetDatabase.setText(Const.NVL(targetDb, ""));
FormData fdTargetDatabase = new FormData();
fdTargetDatabase.left=new FormAttachment(middle, 5);
fdTargetDatabase.right=new FormAttachment(newDatabaseButton, -margin);
fdTargetDatabase.top =new FormAttachment(lastControl, margin);
targetDatabase.setLayoutData(fdTargetDatabase);
targetDatabase.addModifyListener(new ModifyListener() { public void modifyText(ModifyEvent event) {
starDomain.getDomain().setProperty(DefaultIDs.DOMAIN_TARGET_DATABASE, targetDatabase.getText()); } });
lastControl = targetDatabase;
newDatabaseButton.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent event) {
createSharedDatabase(targetDatabase);
}
});
// put some utility buttons at the bottom too...
//
Button sqlJobButton = new Button(physicalGroup, SWT.PUSH);
sqlJobButton.setText(BaseMessages.getString(PKG, "StarModelerPerspective.Button.GenerateSQLJob"));
sqlJobButton.addSelectionListener(new SelectionAdapter() { public void widgetSelected(SelectionEvent event) { generateSqlJobButton(starDomain); } });
Button domainJobButton = new Button(physicalGroup, SWT.PUSH);
domainJobButton.setText(BaseMessages.getString(PKG, "StarModelerPerspective.Button.GenerateDomainJob"));
domainJobButton.addSelectionListener(new SelectionAdapter() { public void widgetSelected(SelectionEvent event) { generateDomainJobButton(starDomain); } });
Button physicalModelButton = new Button(physicalGroup, SWT.PUSH);
physicalModelButton.setText(BaseMessages.getString(PKG, "StarModelerPerspective.Button.GeneratePhysicalModel"));
physicalModelButton.addSelectionListener(new SelectionAdapter() { public void widgetSelected(SelectionEvent event) { generatePhysicalModelButton(starDomain); } });
Button mondrianSchemaButton = new Button(physicalGroup, SWT.PUSH);
mondrianSchemaButton.setText(BaseMessages.getString(PKG, "StarModelerPerspective.Button.GenerateMondrianSchema"));
mondrianSchemaButton.addSelectionListener(new SelectionAdapter() { public void widgetSelected(SelectionEvent event) { generateMondrialSchemaButton(starDomain); } });
BaseStepDialog.positionBottomButtons(physicalGroup,
new Button[] { sqlJobButton, domainJobButton, physicalModelButton, mondrianSchemaButton, }, margin, lastControl);
return physicalGroup;
}
private Control addModelsGroupToDomainTab(final StarDomain starDomain, final XulTabAndPanel tabAndPanel, Composite parentComposite) {
PropsUI props = PropsUI.getInstance();
int middle = props.getMiddlePct();
int margin = Const.MARGIN;
// Add a group for the logical stars
//
final Group logicalGroup = new Group(parentComposite, SWT.SHADOW_NONE);
props.setLook(logicalGroup);
logicalGroup.setText(BaseMessages.getString(PKG, "StarModelerPerspective.LogicalGroup.Label"));
FormLayout groupLayout = new FormLayout();
groupLayout.marginLeft=10;
groupLayout.marginRight=10;
groupLayout.marginTop=10;
groupLayout.marginBottom=10;
groupLayout.spacing=margin;
logicalGroup.setLayout(groupLayout);
FormData fdLogicalGroup = new FormData();
fdLogicalGroup.top = new FormAttachment(0, 0);
fdLogicalGroup.left = new FormAttachment(0, 0);
fdLogicalGroup.right = new FormAttachment(100, 0);
logicalGroup.setLayoutData(fdLogicalGroup);
// Add a line to edit the name of the logical domain
//
Label nameLabel = new Label(logicalGroup, SWT.RIGHT);
props.setLook(nameLabel);
nameLabel.setText(BaseMessages.getString(PKG, "StarModelerPerspective.DomainName.Label"));
FormData fdNameLabel = new FormData();
fdNameLabel.left=new FormAttachment(0, 0);
fdNameLabel.right=new FormAttachment(middle, 0);
fdNameLabel.top =new FormAttachment(0, 0);
nameLabel.setLayoutData(fdNameLabel);
final Text nameText = new Text(logicalGroup, SWT.BORDER | SWT.SINGLE);
props.setLook(nameText);
nameText.setText(Const.NVL(starDomain.getDomain().getName(defaultLocale), ""));
FormData fdNameText = new FormData();
fdNameText.left=new FormAttachment(middle, margin);
fdNameText.right=new FormAttachment(100, 0);
fdNameText.top =new FormAttachment(0, 0);
nameText.setLayoutData(fdNameText);
nameText.addModifyListener(new ModifyListener() { public void modifyText(ModifyEvent event) {
starDomain.getDomain().setName(new LocalizedString(defaultLocale, nameText.getText()));
setNameForTab(tabAndPanel.tab, starDomain.getDomain().getName(defaultLocale));
} });
Control lastControl = nameText;
// Add a line to edit the name of the logical domain
//
Label descriptionLabel = new Label(logicalGroup, SWT.RIGHT);
props.setLook(descriptionLabel);
descriptionLabel.setText(BaseMessages.getString(PKG, "StarModelerPerspective.DomainDescription.Label"));
FormData fdDescriptionLabel = new FormData();
fdDescriptionLabel.left=new FormAttachment(0, 0);
fdDescriptionLabel.right=new FormAttachment(middle, 0);
fdDescriptionLabel.top =new FormAttachment(lastControl, margin);
descriptionLabel.setLayoutData(fdDescriptionLabel);
final Text descriptionText = new Text(logicalGroup, SWT.BORDER | SWT.SINGLE);
props.setLook(descriptionText);
descriptionText.setText(Const.NVL(starDomain.getDomain().getDescription(defaultLocale), ""));
FormData fdDescriptionText = new FormData();
fdDescriptionText.left=new FormAttachment(middle, 5);
fdDescriptionText.right=new FormAttachment(100, 0);
fdDescriptionText.top =new FormAttachment(lastControl, margin);
descriptionText.setLayoutData(fdDescriptionText);
descriptionText.addModifyListener(new ModifyListener() { public void modifyText(ModifyEvent event) {
starDomain.getDomain().setDescription(new LocalizedString(defaultLocale, descriptionText.getText())); } });
lastControl = descriptionText;
// Then we'll add a table view of all the models and their descriptions
//
Label modelsLabel = new Label(logicalGroup, SWT.RIGHT);
props.setLook(modelsLabel);
modelsLabel.setText(BaseMessages.getString(PKG, "StarModelerPerspective.DomainModels.Label"));
FormData fdModelsLabel = new FormData();
fdModelsLabel.left=new FormAttachment(0, 0);
fdModelsLabel.right=new FormAttachment(middle, 0);
fdModelsLabel.top =new FormAttachment(lastControl, margin);
modelsLabel.setLayoutData(fdModelsLabel);
ColumnInfo[] colinf=new ColumnInfo[] {
new ColumnInfo(BaseMessages.getString(PKG, "StarModelerPerspective.ModelName.Column"), ColumnInfo.COLUMN_TYPE_TEXT, false, true),
new ColumnInfo(BaseMessages.getString(PKG, "StarModelerPerspective.ModelDescription.Column"), ColumnInfo.COLUMN_TYPE_TEXT, false, true),
};
final TableView modelsList=new TableView(new Variables(), logicalGroup, SWT.BORDER, colinf, 1, null, props);
modelsList.setReadonly(true);
modelsList.table.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetDefaultSelected(SelectionEvent e) {
if (modelsList.getSelectionIndex()<0) return;
TableItem item = modelsList.table.getSelection()[0];
String name = item.getText(1);
if (editModel(logicalGroup.getShell(), starDomain, defaultLocale, name)) {
refreshModelsList(starDomain, modelsList);
}
}
});
refreshModelsList(starDomain, modelsList);
FormData fdModelsList = new FormData();
fdModelsList.top = new FormAttachment(lastControl, margin);
fdModelsList.bottom = new FormAttachment(lastControl, 250);
fdModelsList.left = new FormAttachment(middle, margin);
fdModelsList.right = new FormAttachment(100, 0);
modelsList.setLayoutData(fdModelsList);
lastControl = modelsList;
// A few buttons to edit the list
//
Button newModelButton = new Button(logicalGroup, SWT.PUSH);
newModelButton.setText(BaseMessages.getString(PKG, "StarModelerPerspective.Button.NewModel"));
FormData fdNewModelButton = new FormData();
fdNewModelButton.top = new FormAttachment(lastControl, margin);
fdNewModelButton.left = new FormAttachment(middle, margin);
newModelButton.setLayoutData(fdNewModelButton);
newModelButton.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent event) {
if (newModel(logicalGroup.getShell(), starDomain)) {
refreshModelsList(starDomain, modelsList);
}
}
});
Button editModelButton = new Button(logicalGroup, SWT.PUSH);
editModelButton.setText(BaseMessages.getString(PKG, "StarModelerPerspective.Button.EditModel"));
FormData fdEditModelButton = new FormData();
fdEditModelButton.top = new FormAttachment(lastControl, margin);
fdEditModelButton.left = new FormAttachment(newModelButton, margin);
editModelButton.setLayoutData(fdEditModelButton);
editModelButton.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent event) {
if (modelsList.getSelectionIndex()<0) return;
TableItem item = modelsList.table.getSelection()[0];
String name = item.getText(1);
if (editModel(logicalGroup.getShell(), starDomain, defaultLocale, name)) {
refreshModelsList(starDomain, modelsList);
}
}
});
Button delModelButton = new Button(logicalGroup, SWT.PUSH);
delModelButton.setText(BaseMessages.getString(PKG, "StarModelerPerspective.Button.DeleteModel"));
FormData fdDelModelButton = new FormData();
fdDelModelButton.top = new FormAttachment(lastControl, margin);
fdDelModelButton.left = new FormAttachment(editModelButton, margin);
delModelButton.setLayoutData(fdDelModelButton);
delModelButton.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent event) {
if (modelsList.getSelectionIndex()<0) return;
TableItem item = modelsList.table.getSelection()[0];
String name = item.getText(1);
if (deleteModel(logicalGroup.getShell(), starDomain, defaultLocale, name)) {
refreshModelsList(starDomain, modelsList);
}
}
});
return logicalGroup;
}
private Control addSharedDimensionsGroupToDomainTab(final StarDomain starDomain, final XulTabAndPanel tabAndPanel, Composite parentComposite, Control lastControl) {
PropsUI props = PropsUI.getInstance();
int middle = props.getMiddlePct();
int margin = Const.MARGIN;
// Add a group for the logical stars
//
final Group dimsGroup = new Group(parentComposite, SWT.SHADOW_NONE);
props.setLook(dimsGroup);
dimsGroup.setText(BaseMessages.getString(PKG, "StarModelerPerspective.SharedDimensions.Label"));
FormLayout groupLayout = new FormLayout();
groupLayout.marginLeft=10;
groupLayout.marginRight=10;
groupLayout.marginTop=10;
groupLayout.marginBottom=10;
groupLayout.spacing=margin;
dimsGroup.setLayout(groupLayout);
FormData fdDimsGroup = new FormData();
fdDimsGroup.top = new FormAttachment(lastControl, margin);
fdDimsGroup.left = new FormAttachment(0, 0);
fdDimsGroup.right = new FormAttachment(100, 0);
dimsGroup.setLayoutData(fdDimsGroup);
// Then we'll add a table view for the shared dimensions
//
Label dimensionsLabel = new Label(dimsGroup, SWT.RIGHT);
props.setLook(dimensionsLabel);
dimensionsLabel.setText(BaseMessages.getString(PKG, "StarModelerPerspective.ListOfSharedDimensions.Label"));
FormData fdDimensionsLabel = new FormData();
fdDimensionsLabel.left=new FormAttachment(0, 0);
fdDimensionsLabel.right=new FormAttachment(middle, 0);
fdDimensionsLabel.top =new FormAttachment(lastControl, margin);
dimensionsLabel.setLayoutData(fdDimensionsLabel);
ColumnInfo[] colinf=new ColumnInfo[] {
new ColumnInfo(BaseMessages.getString(PKG, "StarModelerPerspective.DimensionName.Column"), ColumnInfo.COLUMN_TYPE_TEXT, false, true),
new ColumnInfo(BaseMessages.getString(PKG, "StarModelerPerspective.DimensionDescription.Column"), ColumnInfo.COLUMN_TYPE_TEXT, false, true),
};
final TableView dimensionsList=new TableView(new Variables(), dimsGroup, SWT.BORDER, colinf, 1, null, props);
dimensionsList.setReadonly(true);
dimensionsList.table.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetDefaultSelected(SelectionEvent e) {
if (dimensionsList.getSelectionIndex()<0) return;
TableItem item = dimensionsList.table.getSelection()[0];
String name = item.getText(1);
if (editModel(dimsGroup.getShell(), starDomain, defaultLocale, name)) {
refreshDimensionsList(starDomain, dimensionsList);
}
}
});
refreshDimensionsList(starDomain, dimensionsList);
FormData fdDimensionsList = new FormData();
fdDimensionsList.top = new FormAttachment(lastControl, margin);
fdDimensionsList.bottom = new FormAttachment(lastControl, 250);
fdDimensionsList.left = new FormAttachment(middle, margin);
fdDimensionsList.right = new FormAttachment(100, 0);
dimensionsList.setLayoutData(fdDimensionsList);
lastControl = dimensionsList;
// A few buttons to edit the list
//
Button newDimensionButton = new Button(dimsGroup, SWT.PUSH);
newDimensionButton.setText(BaseMessages.getString(PKG, "StarModelerPerspective.Button.NewSharedDimension"));
FormData fdNewModelButton = new FormData();
fdNewModelButton.top = new FormAttachment(lastControl, margin);
fdNewModelButton.left = new FormAttachment(middle, margin);
newDimensionButton.setLayoutData(fdNewModelButton);
newDimensionButton.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent event) {
if (newSharedDimension(dimsGroup.getShell(), starDomain)) {
refreshDimensionsList(starDomain, dimensionsList);
}
}
});
Button editDimensionButton = new Button(dimsGroup, SWT.PUSH);
editDimensionButton.setText(BaseMessages.getString(PKG, "StarModelerPerspective.Button.EditDimension"));
FormData fdEditModelButton = new FormData();
fdEditModelButton.top = new FormAttachment(lastControl, margin);
fdEditModelButton.left = new FormAttachment(newDimensionButton, margin);
editDimensionButton.setLayoutData(fdEditModelButton);
editDimensionButton.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent event) {
if (dimensionsList.getSelectionIndex()<0) return;
TableItem item = dimensionsList.table.getSelection()[0];
String name = item.getText(1);
if (editSharedDimension(dimsGroup.getShell(), starDomain, defaultLocale, name)) {
refreshDimensionsList(starDomain, dimensionsList);
}
}
});
Button delDimensionButton = new Button(dimsGroup, SWT.PUSH);
delDimensionButton.setText(BaseMessages.getString(PKG, "StarModelerPerspective.Button.DeleteDimension"));
FormData fdDelDimensionButton = new FormData();
fdDelDimensionButton.top = new FormAttachment(lastControl, margin);
fdDelDimensionButton.left = new FormAttachment(editDimensionButton, margin);
delDimensionButton.setLayoutData(fdDelDimensionButton);
delDimensionButton.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent event) {
if (dimensionsList.getSelectionIndex()<0) return;
TableItem item = dimensionsList.table.getSelection()[0];
String name = item.getText(1);
if (deleteSharedDimension(dimsGroup.getShell(), starDomain, defaultLocale, name)) {
refreshDimensionsList(starDomain, dimensionsList);
}
}
});
Button testDimensionButton = new Button(dimsGroup, SWT.PUSH);
testDimensionButton.setText("TEST PUR");
FormData fdtestDimensionButton = new FormData();
fdtestDimensionButton.top = new FormAttachment(lastControl, margin);
fdtestDimensionButton.left = new FormAttachment(delDimensionButton, margin);
testDimensionButton.setLayoutData(fdtestDimensionButton);
testDimensionButton.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent event) {
testMetaStore();
}
});
return dimsGroup;
}
protected void testMetaStore() {
try {
// Force repository meta store
IMetaStore metaStore = Spoon.getInstance().getRepository().getMetaStore();
LogChannel.GENERAL.logBasic("Active metastore: "+metaStore.getName());
StarDomainMetaStoreUtil.verifyNamespaceCreated(metaStore, "pentaho");
IMetaStoreElementType elementType = StarDomainMetaStoreUtil.getStarDomainElementType(metaStore);
if (elementType==null) {
throw new KettleException("Unable to find star domain element type");
}
LogChannel.GENERAL.logBasic("Found star domain element type: "+elementType.getName()+" : "+elementType.getDescription());
elementType = metaStore.getElementTypeByName(PentahoDefaults.NAMESPACE, elementType.getName());
if (elementType==null) {
throw new KettleException("Unable to find star domain element type by name");
}
LogChannel.GENERAL.logBasic("Found element type by name");
List<IdNameDescription> list = new ArrayList<IdNameDescription>();
for (IMetaStoreElement element : metaStore.getElements(PentahoDefaults.NAMESPACE, elementType)) {
IdNameDescription nameDescription = new IdNameDescription(element.getId(), element.getName(), null);
list.add(nameDescription);
}
LogChannel.GENERAL.logBasic("Found "+list.size()+" star domain elements.");
StarDomainMetaStoreUtil.getStarDomainList(metaStore);
} catch(Exception e) {
new ErrorDialog(Spoon.getInstance().getShell(), "ERROR", "Error testing meta store: ", e);
}
}
protected void createSharedDatabase(CCombo targetDatabase) {
Shell shell = Spoon.getInstance().getShell();
boolean retry=true;
while (retry) {
try {
DatabaseMeta dbMeta = new DatabaseMeta();
DatabaseDialog databaseDialog = new DatabaseDialog(shell, dbMeta);
if (databaseDialog.open()!=null) {
// Add dbMeta to the shared databases...
//
IMetaStore metaStore = Spoon.getInstance().getMetaStore();
DatabaseMetaStoreUtil.createDatabaseElement(metaStore, dbMeta);
// Refresh the list...
//
final List<DatabaseMeta> sharedDatabases= DatabaseMetaStoreUtil.getDatabaseElements(metaStore);
String[] databaseNames = SharedDatabaseUtil.getSortedDatabaseNames(sharedDatabases);
targetDatabase.setItems(databaseNames);
targetDatabase.setText(dbMeta.getName());
}
retry = false;
} catch(MetaStoreElementExistException e) {
new ErrorDialog(shell,
BaseMessages.getString(PKG, "StarModelerPerspective.Exception.UnableToCreateSharedDB.Title"),
BaseMessages.getString(PKG, "StarModelerPerspective.Exception.UnableToCreateSharedDB.Message"), e);
} catch (MetaStoreException e) {
new ErrorDialog(shell,
BaseMessages.getString(PKG, "StarModelerPerspective.Exception.UnableToCreateSharedDB.Title"),
BaseMessages.getString(PKG, "StarModelerPerspective.Exception.UnableToCreateSharedDB.Message"), e);
retry=false;
}
}
}
protected void generateSqlJobButton(StarDomain starDomain) {
final Spoon spoon = Spoon.getInstance();
List<DatabaseMeta> sharedDatabases = SharedDatabaseUtil.loadSharedDatabases();
// TODO: validate presence of repository, repository directory
//
JobGenerator jobGenerator = new JobGenerator(starDomain, spoon.rep, new RepositoryDirectory(), sharedDatabases, defaultLocale);
try {
JobMeta jobMeta = jobGenerator.generateSqlJob();
spoon.addJobGraph(jobMeta);
SpoonPerspectiveManager.getInstance().activatePerspective(MainSpoonPerspective.class);
} catch(Exception e) {
new ErrorDialog(spoon.getShell(),
BaseMessages.getString(PKG, "StarModelerPerspective.ErrorGeneratingSqlJob.Title"),
BaseMessages.getString(PKG, "StarModelerPerspective.ErrorGeneratingSqlJob.Message"), e);
}
}
protected void generateDomainJobButton(StarDomain starDomain) {
final Spoon spoon = Spoon.getInstance();
List<DatabaseMeta> sharedDatabases = SharedDatabaseUtil.loadSharedDatabases();
JobGenerator jobGenerator = new JobGenerator(starDomain, spoon.rep, new RepositoryDirectory(), sharedDatabases, defaultLocale);
try {
List<TransMeta> transMetas = jobGenerator.generateDimensionTransformations();
for (TransMeta transMeta : transMetas) {
spoon.addTransGraph(transMeta);
}
SpoonPerspectiveManager.getInstance().activatePerspective(MainSpoonPerspective.class);
} catch(Exception e) {
new ErrorDialog(spoon.getShell(),
BaseMessages.getString(PKG, "StarModelerPerspective.ErrorGeneratingSqlJob.Title"),
BaseMessages.getString(PKG, "StarModelerPerspective.ErrorGeneratingSqlJob.Message"), e);
}
}
protected void generatePhysicalModelButton(StarDomain starDomain) {
try {
List<DatabaseMeta> sharedDatabases = SharedDatabaseUtil.loadSharedDatabases();
MetadataGenerator generator = new MetadataGenerator(starDomain.getDomain(), sharedDatabases);
Domain physicalMetadataModel = generator.generatePhysicalMetadataModel();
System.out.println("Generated physical model: "+physicalMetadataModel.getName(defaultLocale));
} catch(Exception e) {
new ErrorDialog(Spoon.getInstance().getShell(),
BaseMessages.getString(PKG, "StarModelerPerspective.ErrorGeneratingPhysicalModel.Title"),
BaseMessages.getString(PKG, "StarModelerPerspective.ErrorGeneratingPhysicalModel.Message"),
e);
}
}
protected void generateMondrialSchemaButton(StarDomain starDomain) {
}
protected void refreshModelsList(StarDomain starDomain, TableView modelsList) {
modelsList.clearAll();
for (LogicalModel model : starDomain.getDomain().getLogicalModels()) {
TableItem item = new TableItem(modelsList.table, SWT.NONE);
item.setText(1, Const.NVL(model.getName(defaultLocale), ""));
item.setText(2, Const.NVL(model.getDescription(defaultLocale), ""));
}
modelsList.removeEmptyRows();
modelsList.setRowNums();
modelsList.optWidth(true);
Spoon.getInstance().enableMenus();
}
protected void refreshDimensionsList(StarDomain starDomain, TableView dimensionsList) {
dimensionsList.clearAll();
for (LogicalTable table : starDomain.getSharedDimensions()) {
TableItem item = new TableItem(dimensionsList.table, SWT.NONE);
item.setText(1, Const.NVL(table.getName(defaultLocale), ""));
item.setText(2, Const.NVL(table.getDescription(defaultLocale), ""));
}
dimensionsList.removeEmptyRows();
dimensionsList.setRowNums();
dimensionsList.optWidth(true);
}
/**
* Create a new shared dimension in the domain
*
* @param domain the domain to create the new model in
*/
private boolean newSharedDimension(Shell shell, StarDomain starDomain) {
LogicalTable dimensionTable = new LogicalTable();
dimensionTable.setName(new LocalizedString(defaultLocale, "Shared dimension"));
DimensionTableDialog dialog = new DimensionTableDialog(shell, dimensionTable, defaultLocale);
if (dialog.open()!=null) {
starDomain.getSharedDimensions().add(dimensionTable);
starDomain.setChanged(true);
return true;
}
return false;
}
/**
* Edit a shared dimension in the domain
* @param shell
* @param LogicstarDomain the domain to edit the model in
* @param dimensionName the name of the model to edit
* @return
*/
private boolean editSharedDimension(Shell shell, StarDomain starDomain, String locale, String dimensionName) {
LogicalTable logicalTable = findSharedDimension(starDomain, locale, dimensionName);
if (logicalTable!=null) {
DimensionTableDialog dialog = new DimensionTableDialog(shell, logicalTable, locale);
if (dialog.open()!=null) {
starDomain.setChanged(true);
return true;
}
}
return false;
}
/**
* Delete a shared dimension in the domain
*
* @param domain the domain to delete the dimension from
* @param modelName the name of the dimension to delete
*/
private boolean deleteSharedDimension(Shell shell, StarDomain starDomain, String locale, String modelName) {
LogicalTable logicalTable = findSharedDimension(starDomain, locale, modelName);
if (logicalTable!=null) {
// TODO: show warning dialog.
//
starDomain.getSharedDimensions().remove(logicalTable);
starDomain.setChanged(true);
return true;
}
return false;
}
/**
* Create a new model in the domain
*
* @param domain the domain to create the new model in
*/
private boolean newModel(Shell shell, StarDomain starDomain) {
LogicalModel model = new LogicalModel();
model.setName(new LocalizedString(defaultLocale, "Model"));
StarModelDialog dialog = new StarModelDialog(shell, model, defaultLocale);
if (dialog.open()!=null) {
starDomain.getDomain().getLogicalModels().add(model);
starDomain.setChanged(true);
return true;
}
return false;
}
/**
* Edit a model in the domain
* @param shell
* @param LogicstarDomain the domain to edit the model in
* @param modelName the name of the model to edit
* @return
*/
private boolean editModel(Shell shell, StarDomain starDomain, String locale, String modelName) {
LogicalModel logicalModel = findLogicalTable(starDomain.getDomain(), locale, modelName);
if (logicalModel!=null) {
StarModelDialog dialog = new StarModelDialog(shell, logicalModel, locale);
if (dialog.open()!=null) {
starDomain.setChanged(true);
return true;
}
}
return false;
}
/**
* Delete a model in the domain
*
* @param domain the domain to delete the model from
* @param modelName the name of the model to delete
*/
private boolean deleteModel(Shell shell, StarDomain starDomain, String locale, String modelName) {
LogicalModel logicalModel = findLogicalTable(starDomain.getDomain(), locale, modelName);
if (logicalModel!=null) {
// TODO: show warning dialog.
//
starDomain.getDomain().getLogicalModels().remove(logicalModel);
starDomain.setChanged(true);
return true;
}
return false;
}
private LogicalTable findSharedDimension(StarDomain starDomain, String locale, String dimensionName) {
for (LogicalTable logicalTable : starDomain.getSharedDimensions()) {
String name = ConceptUtil.getName(logicalTable, locale);
if (name!=null && name.equalsIgnoreCase(dimensionName)) return logicalTable;
}
return null;
}
private LogicalModel findLogicalTable(Domain domain, String locale, String modelName) {
for (LogicalModel logicalModel : domain.getLogicalModels()) {
String name = ConceptUtil.getName(logicalModel, locale);
if (name!=null && name.equalsIgnoreCase(modelName)) return logicalModel;
}
return null;
}
public EngineMetaInterface getActiveMeta() {
int idx = tabbox.getSelectedIndex();
if( idx == -1 || idx >= tabbox.getTabs().getChildNodes().size()) {
return null;
}
return metas.get(tabbox.getTabs().getChildNodes().get( idx ));
}
public void setSelectedMeta(EngineMetaInterface meta){
EngineMetaInterface prevVal = this.selectedMeta;
this.selectedMeta = meta;
Spoon.getInstance().enableMenus();
firePropertyChange("selectedMeta", prevVal, meta);
}
public EngineMetaInterface getSelectedMeta(){
return selectedMeta;
}
public String getDefaultExtension() {
return defaultExtension;
}
public void setDefaultExtension(String defaultExtension) {
this.defaultExtension = defaultExtension;
}
protected static class CloseConfirmXulDialogCallback implements XulDialogCallback<Object>{
public boolean closeIt = false;
public void onClose(XulComponent sender, Status returnCode, Object retVal) {
if(returnCode == Status.ACCEPT){
closeIt = true;
}
}
public void onError(XulComponent sender, Throwable t) {}
}
public boolean onTabClose(final int pos) throws XulException {
if(models.get(pos).hasChanged()){
XulConfirmBox confirm = (XulConfirmBox) document.createElement("confirmbox");
confirm.setTitle(BaseMessages.getString(this.getClass(), "StarModelerPerspective.unsavedChangesTitle"));
confirm.setMessage(BaseMessages.getString(this.getClass(), "StarModelerPerspective.unsavedChangesMessage"));
CloseConfirmXulDialogCallback callback = new CloseConfirmXulDialogCallback();
confirm.addDialogCallback(callback);
confirm.open();
if(callback.closeIt){
models.remove(pos);
metas.remove(tabbox.getTabs().getChildNodes().get(pos));
return true;
} else {
return false;
}
} else {
models.remove(pos);
metas.remove(pos);
}
return true;
}
public boolean onFileClose() {
int idx = tabbox.getSelectedIndex();
if (idx == -1 || idx >= tabbox.getTabs().getChildNodes().size()) {
return false;
}
try {
if (onTabClose(idx)) {
XulComponent panel = panels.getChildNodes().get(idx);
XulComponent tab = tabs.getChildNodes().get(idx);
panels.removeChild(panel);
tabs.removeChild(tab);
return true;
}
} catch (XulException e) {
e.printStackTrace();
}
return false;
}
public void open() {
// List all star domains in the metastore
//
Shell shell = Spoon.getInstance().getShell();
try {
List<IdNameDescription> starDomainList = StarDomainMetaStoreUtil.getStarDomainList(Spoon.getInstance().getMetaStore());
List<String> rows = new ArrayList<String>();
for (IdNameDescription ind : starDomainList) {
rows.add(ind.getName()+" : "+ind.getDescription());
}
EnterSelectionDialog selectionDialog = new EnterSelectionDialog(shell, rows.toArray(new String[rows.size()]), "Select star domain", "Select the star domain to open:");
selectionDialog.setMulti(false);
if (selectionDialog.open()!=null) {
int index = selectionDialog.getSelectionNr();
StarDomain starDomain = StarDomainMetaStoreUtil.loadStarDomain(Spoon.getInstance().getMetaStore(), starDomainList.get(index).getId());
if (starDomain!=null) {
createTabForDomain(starDomain);
}
}
} catch(Exception e) {
new ErrorDialog(shell, "Error", "Error getting list of star domains from the MetaStore:", e);
}
}
@Override
public boolean save(EngineMetaInterface meta) {
return save(meta, null, false);
}
}