View Javadoc

1   package com.eyeq.pivot4j.pentaho.ui;
2   
3   import java.io.IOException;
4   import java.io.InputStream;
5   import java.io.Serializable;
6   import java.util.LinkedList;
7   import java.util.List;
8   import java.util.ResourceBundle;
9   
10  import javax.faces.FacesException;
11  import javax.faces.application.FacesMessage;
12  import javax.faces.bean.ManagedBean;
13  import javax.faces.bean.ManagedProperty;
14  import javax.faces.bean.ViewScoped;
15  import javax.faces.context.FacesContext;
16  import javax.faces.context.Flash;
17  
18  import org.apache.commons.configuration.ConfigurationException;
19  import org.apache.commons.configuration.HierarchicalConfiguration;
20  import org.apache.commons.configuration.XMLConfiguration;
21  import org.apache.commons.io.IOUtils;
22  import org.olap4j.OlapDataSource;
23  import org.primefaces.context.RequestContext;
24  import org.primefaces.model.DefaultTreeNode;
25  import org.primefaces.model.TreeNode;
26  import org.slf4j.Logger;
27  import org.slf4j.LoggerFactory;
28  
29  import com.eyeq.pivot4j.PivotModel;
30  import com.eyeq.pivot4j.analytics.config.Settings;
31  import com.eyeq.pivot4j.analytics.datasource.ConnectionInfo;
32  import com.eyeq.pivot4j.analytics.datasource.CubeInfo;
33  import com.eyeq.pivot4j.analytics.datasource.DataSourceManager;
34  import com.eyeq.pivot4j.analytics.repository.DataSourceNotFoundException;
35  import com.eyeq.pivot4j.analytics.repository.ReportContent;
36  import com.eyeq.pivot4j.analytics.repository.ReportFile;
37  import com.eyeq.pivot4j.analytics.repository.ReportRepository;
38  import com.eyeq.pivot4j.analytics.repository.RepositoryFileFilter;
39  import com.eyeq.pivot4j.analytics.state.ViewState;
40  import com.eyeq.pivot4j.analytics.state.ViewStateHolder;
41  import com.eyeq.pivot4j.analytics.ui.navigator.RepositoryNode;
42  import com.eyeq.pivot4j.impl.PivotModelImpl;
43  import com.eyeq.pivot4j.mdx.MdxParser;
44  import com.eyeq.pivot4j.mdx.MdxStatement;
45  import com.eyeq.pivot4j.mdx.impl.MdxParserImpl;
46  import com.eyeq.pivot4j.ui.table.TableRenderer;
47  
48  @ManagedBean(name = "migrationHandler")
49  @ViewScoped
50  public class MigrationHandler {
51  
52  	private Logger log = LoggerFactory.getLogger(getClass());
53  
54  	@ManagedProperty(value = "#{dataSourceManager}")
55  	private DataSourceManager dataSourceManager;
56  
57  	@ManagedProperty(value = "#{settings}")
58  	private Settings settings;
59  
60  	@ManagedProperty(value = "#{reportRepository}")
61  	private ReportRepository repository;
62  
63  	@ManagedProperty(value = "#{viewStateHolder}")
64  	private ViewStateHolder viewStateHolder;
65  
66  	private TreeNode rootNode;
67  
68  	private TreeNode selection;
69  
70  	private String viewId;
71  
72  	private List<Entry> convertedFiles = new LinkedList<Entry>();
73  
74  	private Entry selectedFile;
75  
76  	private boolean migrationDone = false;
77  
78  	private RepositoryFileFilter fileFilter = new MigrationTargetFilter();
79  
80  	public boolean isOkButtonEnabled() {
81  		if (migrationDone) {
82  			return selectedFile != null && selectedFile.getError() == null;
83  		} else {
84  			return selection != null;
85  		}
86  	}
87  
88  	public String proceed() {
89  		FacesContext context = FacesContext.getCurrentInstance();
90  		ResourceBundle bundle = context.getApplication().getResourceBundle(
91  				context, "plugin_msg");
92  
93  		String navigation = null;
94  
95  		try {
96  			if (migrationDone) {
97  				navigation = open(selectedFile.getResult(), false);
98  			} else {
99  				convertedFiles.clear();
100 
101 				ReportFile target;
102 
103 				if (selection instanceof RepositoryNode) {
104 					target = ((RepositoryNode) selection).getObject();
105 				} else {
106 					target = repository.getRoot();
107 				}
108 
109 				if (target.isDirectory()) {
110 					convertFiles(target);
111 				} else {
112 					navigation = open(target, true);
113 				}
114 
115 				RequestContext.getCurrentInstance().execute(
116 						"if (parent) parent.mantle_refreshRepository()");
117 
118 				this.migrationDone = true;
119 			}
120 		} catch (Exception e) {
121 			String title = bundle.getString("title.migrate.error");
122 			String message = bundle.getString("message.migrate.error") + e;
123 
124 			context.addMessage(null, new FacesMessage(
125 					FacesMessage.SEVERITY_ERROR, title, message));
126 
127 			if (log.isErrorEnabled()) {
128 				log.error(message, e);
129 			}
130 		}
131 
132 		return navigation;
133 	}
134 
135 	public String getSummary() {
136 		FacesContext context = FacesContext.getCurrentInstance();
137 		ResourceBundle bundle = context.getApplication().getResourceBundle(
138 				context, "plugin_msg");
139 
140 		int count = 0;
141 
142 		for (Entry entry : convertedFiles) {
143 			if (entry.getError() == null) {
144 				count++;
145 			}
146 		}
147 
148 		String message = String.format(
149 				bundle.getString("message.migrate.success"), count);
150 
151 		return message;
152 	}
153 
154 	/**
155 	 * @param selectedFile
156 	 * @param convert
157 	 * @return
158 	 * @throws IOException
159 	 * @throws ConfigurationException
160 	 * @throws DataSourceNotFoundException
161 	 * @throws InvalidConnectionInfoException
162 	 */
163 	public String open(ReportFile file, boolean convert) throws IOException,
164 			ConfigurationException, DataSourceNotFoundException,
165 			InvalidConnectionInfoException {
166 		FacesContext context = FacesContext.getCurrentInstance();
167 
168 		ViewState state;
169 
170 		if (convert) {
171 			state = convertFile(file);
172 			state.setDirty(true);
173 		} else {
174 			state = new ViewState(viewId, file.getName());
175 			state.setFile(file);
176 
177 			ReportContent content = repository.getReportContent(file);
178 			content.read(state, dataSourceManager);
179 		}
180 
181 		state.getModel().initialize();
182 
183 		viewStateHolder.registerState(state);
184 
185 		Flash flash = context.getExternalContext().getFlash();
186 
187 		flash.put("connectionInfo", state.getConnectionInfo());
188 		flash.put("viewId", state.getId());
189 
190 		StringBuilder builder = new StringBuilder();
191 		builder.append("view");
192 		builder.append("?faces-redirect=true");
193 		builder.append("&");
194 		builder.append(settings.getViewParameterName());
195 		builder.append("=");
196 		builder.append(state.getId());
197 
198 		return builder.toString();
199 	}
200 
201 	/**
202 	 * @param parent
203 	 * @throws IOException
204 	 * @throws ConfigurationException
205 	 */
206 	protected void convertFiles(ReportFile parent) throws IOException,
207 			ConfigurationException {
208 		List<ReportFile> files = repository.getFiles(parent, fileFilter);
209 
210 		for (ReportFile file : files) {
211 			if (file.isDirectory()) {
212 				convertFiles(file);
213 			} else {
214 				String error = null;
215 
216 				ReportFile result = null;
217 
218 				FacesContext context = FacesContext.getCurrentInstance();
219 				ResourceBundle bundle = context.getApplication()
220 						.getResourceBundle(context, "plugin_msg");
221 
222 				try {
223 					ViewState state = convertFile(file);
224 
225 					String name = file.getName().substring(
226 							0,
227 							file.getName().length()
228 									- file.getExtension().length())
229 							+ "pivot4j";
230 
231 					result = repository.createFile(parent, name,
232 							new ReportContent(state));
233 				} catch (InvalidConnectionInfoException e) {
234 					error = bundle
235 							.getString("message.migrate.error.dataSource");
236 				} catch (Exception e) {
237 					error = bundle.getString("message.migrate.error") + e;
238 
239 					if (log.isErrorEnabled()) {
240 						log.error(error, e);
241 					}
242 				}
243 
244 				convertedFiles.add(new Entry(file, result, error));
245 			}
246 		}
247 	}
248 
249 	/**
250 	 * @param selectedFile
251 	 * @return
252 	 * @throws ConfigurationException
253 	 * @throws IOException
254 	 */
255 	protected ViewState convertFile(ReportFile file)
256 			throws InvalidConnectionInfoException, ConfigurationException,
257 			IOException {
258 		if (log.isDebugEnabled()) {
259 			log.debug("Migrating JPivot report : " + file);
260 		}
261 
262 		InputStream in = null;
263 
264 		try {
265 			in = repository.readContent(file);
266 
267 			XMLConfiguration config = new XMLConfiguration();
268 			config.setRootElementName("action-sequence");
269 			config.setDelimiterParsingDisabled(true);
270 			config.load(in);
271 
272 			return convertFile(config);
273 		} finally {
274 			IOUtils.closeQuietly(in);
275 		}
276 	}
277 
278 	/**
279 	 * @param config
280 	 * @return
281 	 * @throws InvalidConnectionInfoException
282 	 */
283 	protected ViewState convertFile(HierarchicalConfiguration config)
284 			throws InvalidConnectionInfoException {
285 		HierarchicalConfiguration report = config
286 				.configurationAt("actions.action-definition.component-definition");
287 
288 		String title = report.getString("title");
289 		String query = report.getString("query");
290 		String cube = report.getString("cube");
291 		String catalog = report.getString("model");
292 
293 		if (log.isDebugEnabled()) {
294 			log.debug("	- title : " + title);
295 			log.debug("	- query : " + query);
296 			log.debug("	- cube : " + cube);
297 			log.debug("	- catalog : " + catalog);
298 		}
299 
300 		if (cube == null) {
301 			// NOTE : Pentaho JPivot plugin seems to have a bug with storing
302 			// catalog and cube names when more than one analysis data sources
303 			// are registered. So we need to check for such an error and try to
304 			// guess the correct names.
305 
306 			String originalCubeName = cube;
307 			String originalCatalogName = catalog;
308 
309 			if (log.isWarnEnabled()) {
310 				log.warn("Cube name is not specified. Trying to guess the correct cube and catalog names.");
311 			}
312 
313 			MdxParser parser = new MdxParserImpl();
314 			MdxStatement statement = parser.parse(query);
315 
316 			cube = statement.getCube().getNames().get(0).getUnquotedName();
317 
318 			if (log.isInfoEnabled()) {
319 				log.info("Cube name specified in MDX query : " + cube);
320 			}
321 
322 			List<CubeInfo> cubes = dataSourceManager.getCubes(catalog);
323 
324 			boolean found = false;
325 
326 			for (CubeInfo cubeInfo : cubes) {
327 				found = cubeInfo.getName().equalsIgnoreCase(cube);
328 				if (found) {
329 					cube = cubeInfo.getName();
330 					break;
331 				}
332 			}
333 
334 			if (!found) {
335 				throw new InvalidConnectionInfoException(originalCubeName,
336 						originalCatalogName);
337 			}
338 		}
339 
340 		ConnectionInfo connectionInfo = new ConnectionInfo(catalog, cube);
341 
342 		OlapDataSource dataSource = dataSourceManager
343 				.getDataSource(connectionInfo);
344 
345 		PivotModel model = new PivotModelImpl(dataSource);
346 		model.setMdx(query);
347 
348 		TableRenderer renderer = new TableRenderer();
349 
350 		// NOTE : Pentaho JPivot plugin does not preserve rendering states in
351 		// saved reports, despite they contain related
352 		// tags for such properties(i.e. <hide-spans/>).
353 
354 		ViewState state = viewStateHolder
355 				.createNewState(connectionInfo, viewId);
356 
357 		state.setName(title);
358 		state.setConnectionInfo(connectionInfo);
359 		state.setModel(model);
360 		state.setRendererState(renderer.saveState());
361 
362 		return state;
363 	}
364 
365 	/**
366 	 * @return the viewId
367 	 */
368 	public String getViewId() {
369 		return viewId;
370 	}
371 
372 	/**
373 	 * @param viewId
374 	 *            the viewId to set
375 	 */
376 	public void setViewId(String viewId) {
377 		this.viewId = viewId;
378 	}
379 
380 	/**
381 	 * @return the repository
382 	 */
383 	public ReportRepository getRepository() {
384 		return repository;
385 	}
386 
387 	/**
388 	 * @param repository
389 	 *            the repository to set
390 	 */
391 	public void setRepository(ReportRepository repository) {
392 		this.repository = repository;
393 	}
394 
395 	/**
396 	 * @return the dataSourceManager
397 	 */
398 	public DataSourceManager getDataSourceManager() {
399 		return dataSourceManager;
400 	}
401 
402 	/**
403 	 * @param dataSourceManager
404 	 *            the dataSourceManager to set
405 	 */
406 	public void setDataSourceManager(DataSourceManager dataSourceManager) {
407 		this.dataSourceManager = dataSourceManager;
408 	}
409 
410 	/**
411 	 * @return the settings
412 	 */
413 	public Settings getSettings() {
414 		return settings;
415 	}
416 
417 	/**
418 	 * @param settings
419 	 *            the settings to set
420 	 */
421 	public void setSettings(Settings settings) {
422 		this.settings = settings;
423 	}
424 
425 	/**
426 	 * @return the viewStateHolder
427 	 */
428 	public ViewStateHolder getViewStateHolder() {
429 		return viewStateHolder;
430 	}
431 
432 	/**
433 	 * @param viewStateHolder
434 	 *            the viewStateHolder to set
435 	 */
436 	public void setViewStateHolder(ViewStateHolder viewStateHolder) {
437 		this.viewStateHolder = viewStateHolder;
438 	}
439 
440 	/**
441 	 * @return the rootNode
442 	 */
443 	public TreeNode getRootNode() {
444 		if (rootNode == null) {
445 			this.rootNode = new DefaultTreeNode();
446 			rootNode.setExpanded(true);
447 
448 			RepositoryNode node;
449 
450 			try {
451 				node = new RepositoryNode(repository.getRoot(), repository);
452 			} catch (IOException e) {
453 				throw new FacesException(e);
454 			}
455 
456 			node.setExpanded(true);
457 			node.setFilter(fileFilter);
458 
459 			rootNode.getChildren().add(node);
460 		}
461 
462 		return rootNode;
463 	}
464 
465 	protected RepositoryNode getRepositoryRootNode() {
466 		return (RepositoryNode) getRootNode().getChildren().get(0);
467 	}
468 
469 	public TreeNode getSelection() {
470 		return selection;
471 	}
472 
473 	public void setSelection(TreeNode selection) {
474 		this.selection = selection;
475 	}
476 
477 	/**
478 	 * @return the convertedFiles
479 	 */
480 	public List<Entry> getConvertedFiles() {
481 		return convertedFiles;
482 	}
483 
484 	/**
485 	 * @return the selectedFile
486 	 */
487 	public Entry getSelectedFile() {
488 		return selectedFile;
489 	}
490 
491 	/**
492 	 * @param selectedFile
493 	 *            the selectedFile to set
494 	 */
495 	public void setSelectedFile(Entry selectedFile) {
496 		this.selectedFile = selectedFile;
497 	}
498 
499 	/**
500 	 * @return the migrationDone
501 	 */
502 	public boolean isMigrationDone() {
503 		return migrationDone;
504 	}
505 
506 	static class MigrationTargetFilter implements RepositoryFileFilter,
507 			Serializable {
508 
509 		private static final long serialVersionUID = 4427077075329175626L;
510 
511 		/**
512 		 * @see com.eyeq.pivot4j.analytics.repository.RepositoryFileFilter#accept(com.eyeq.pivot4j.analytics.repository.ReportFile)
513 		 */
514 		@Override
515 		public boolean accept(ReportFile file) {
516 			if (file.isDirectory()) {
517 				return !file.getPath().startsWith(ReportFile.SEPARATOR + "etc");
518 			} else {
519 				return "xaction".equalsIgnoreCase(file.getExtension())
520 						|| "xjpivot".equalsIgnoreCase(file.getExtension());
521 			}
522 		}
523 	}
524 
525 	static class InvalidConnectionInfoException extends Exception {
526 
527 		private static final long serialVersionUID = 8066830074992740616L;
528 
529 		private String cube;
530 
531 		private String catalog;
532 
533 		/**
534 		 * @param cube
535 		 * @param catalog
536 		 */
537 		private InvalidConnectionInfoException(String cube, String catalog) {
538 			this.cube = cube;
539 			this.catalog = catalog;
540 		}
541 
542 		/**
543 		 * @return the cube
544 		 */
545 		public String getCube() {
546 			return cube;
547 		}
548 
549 		/**
550 		 * @return the catalog
551 		 */
552 		public String getCatalog() {
553 			return catalog;
554 		}
555 	}
556 
557 	public static class Entry {
558 
559 		private ReportFile file;
560 
561 		private ReportFile result;
562 
563 		private String error;
564 
565 		/**
566 		 * @param file
567 		 * @param result
568 		 * @param error
569 		 */
570 		private Entry(ReportFile file, ReportFile result, String error) {
571 			this.file = file;
572 			this.error = error;
573 			this.result = result;
574 		}
575 
576 		/**
577 		 * @return the file
578 		 */
579 		public ReportFile getFile() {
580 			return file;
581 		}
582 
583 		/**
584 		 * @return the result
585 		 */
586 		public ReportFile getResult() {
587 			return result;
588 		}
589 
590 		/**
591 		 * @return the error
592 		 */
593 		public String getError() {
594 			return error;
595 		}
596 	}
597 }